UNPKG

@sprucelabs/spruce-cli

Version:

Command line interface for building Spruce skills.

497 lines 19.8 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const path_1 = __importDefault(require("path")); const error_1 = __importDefault(require("@sprucelabs/error")); const globby_1 = __importDefault(require("@sprucelabs/globby")); const schema_1 = require("@sprucelabs/schema"); const spruce_skill_utils_1 = require("@sprucelabs/spruce-skill-utils"); // @ts-ignore const cfonts_1 = __importDefault(require("cfonts")); const chalk_1 = __importDefault(require("chalk")); // @ts-ignore No definition available const cli_table3_1 = __importDefault(require("cli-table3")); // @ts-ignore No definition available const emphasize_1 = __importDefault(require("emphasize")); const fs_extra_1 = __importDefault(require("fs-extra")); const inquirer_1 = __importDefault(require("inquirer")); const lodash_1 = __importDefault(require("lodash")); const lodash_2 = require("lodash"); const ora_1 = __importDefault(require("ora")); const terminal_kit_1 = require("terminal-kit"); const SpruceError_1 = __importDefault(require("../errors/SpruceError")); const feature_utilities_1 = __importDefault(require("../features/feature.utilities")); const graphicsInterface_types_1 = require("../types/graphicsInterface.types"); const duration_utility_1 = __importDefault(require("../utilities/duration.utility")); const isCi_1 = __importDefault(require("../utilities/isCi")); let fieldCount = 0; function generateInquirerFieldName() { fieldCount++; return `field-${fieldCount}`; } /** Remove effects cfonts does not support */ function filterEffectsForCFonts(effects) { return (0, lodash_2.filter)(effects, (effect) => [ graphicsInterface_types_1.GraphicsTextEffect.SpruceHeader, graphicsInterface_types_1.GraphicsTextEffect.Reset, graphicsInterface_types_1.GraphicsTextEffect.Bold, graphicsInterface_types_1.GraphicsTextEffect.Dim, graphicsInterface_types_1.GraphicsTextEffect.Italic, graphicsInterface_types_1.GraphicsTextEffect.Underline, graphicsInterface_types_1.GraphicsTextEffect.Inverse, graphicsInterface_types_1.GraphicsTextEffect.Hidden, graphicsInterface_types_1.GraphicsTextEffect.Strikethrough, graphicsInterface_types_1.GraphicsTextEffect.Visible, ].indexOf(effect) === -1); } class TerminalInterface { static loader; static _doesSupportColor = process?.stdout?.isTTY && !(0, isCi_1.default)(); static ora = ora_1.default; isPromptActive = false; cwd; renderStackTraces = false; progressBar = null; log; constructor(cwd, renderStackTraces = false, log = console.log.bind(console)) { this.cwd = cwd; this.renderStackTraces = renderStackTraces; this.log = log; } static doesSupportColor() { return this._doesSupportColor; } static setDoesSupportColor(isTTy) { this._doesSupportColor = isTTy; } async sendInput() { throw new Error('sendInput not supported on the TerminalInterface!'); } renderLines(lines, effects) { lines.forEach((line) => { this.renderLine(line, effects); }); } renderObject(object, effects = [graphicsInterface_types_1.GraphicsTextEffect.Green]) { this.renderLine(''); this.renderDivider(); this.renderLine(''); Object.keys(object).forEach((key) => { this.renderLine(`${chalk_1.default.bold(key)}: ${typeof object[key] === 'string' ? object[key] : JSON.stringify(object[key])}`, effects); }); this.renderLine(''); this.renderDivider(); this.renderLine(''); } renderSection(options) { const { headline, lines, object, dividerEffects = [], headlineEffects = [ graphicsInterface_types_1.GraphicsTextEffect.Blue, graphicsInterface_types_1.GraphicsTextEffect.Bold, ], bodyEffects = [graphicsInterface_types_1.GraphicsTextEffect.Green], } = options; if (headline) { this.renderHeadline(`${headline} 🌲🤖`, headlineEffects, dividerEffects); } if (lines) { this.renderLines(lines, bodyEffects); this.renderLine(''); this.renderDivider(dividerEffects); } if (object) { this.renderObject(object, bodyEffects); } this.renderLine(''); } renderDivider(effects) { const bar = '=================================================='; this.renderLine(bar, effects); } renderActionSummary(results) { const generatedFiles = results.files?.filter((f) => f.action === 'generated') ?? []; const updatedFiles = results.files?.filter((f) => f.action === 'updated') ?? []; const skippedFiles = results.files?.filter((f) => f.action === 'skipped') ?? []; const errors = results.errors ?? []; const packagesInstalled = results.packagesInstalled ?? []; const namespace = results.namespace; this.renderHero(`${results.headline}`); let summaryLines = [ namespace ? `Namespace: ${namespace}` : null, errors.length > 0 ? `Errors: ${errors.length}` : null, generatedFiles.length > 0 ? `Generated files: ${generatedFiles.length}` : null, updatedFiles.length > 0 ? `Updated files: ${updatedFiles.length}` : null, skippedFiles.length > 0 ? `Skipped files: ${skippedFiles.length}` : null, packagesInstalled.length > 0 ? `NPM packages installed: ${packagesInstalled.length}` : null, ...(results.summaryLines ?? []), ].filter((line) => !!line); if (summaryLines.length === 0) { summaryLines.push('Nothing to report!'); } this.renderSection({ headline: `${feature_utilities_1.default.generateCommand(results.featureCode, results.actionCode)} summary`, lines: summaryLines, }); if (packagesInstalled.length > 0) { const table = new cli_table3_1.default({ head: ['Name', 'Dev'], colWidths: [40, 5], wordWrap: true, colAligns: ['left', 'center'], }); packagesInstalled .sort((one, two) => (one.name > two.name ? 1 : -1)) .forEach((pkg) => { table.push([pkg.name, pkg.isDev ? '√' : '']); }); this.renderSection({ headline: `NPM packages summary`, lines: [table.toString()], }); } for (let files of [generatedFiles, updatedFiles]) { if (files.length > 0) { const table = new cli_table3_1.default({ head: ['File', 'Description'], wordWrap: true, }); files = files.sort(); for (const file of files) { table.push([file.name, file.description ?? '']); } this.renderSection({ headline: `${spruce_skill_utils_1.namesUtil.toPascal(files[0].action)} file summary`, lines: [table.toString()], }); } } if (results.hints) { this.renderSection({ headline: 'Read below 👇', lines: results.hints, }); } if (errors.length > 0) { this.renderHeadline('Errors'); errors.forEach((err) => this.renderError(err)); } if (results.totalTime) { this.renderLine(`Total time: ${duration_utility_1.default.msToFriendly(results.totalTime)}`); } } renderHeadline(message, effects = [ graphicsInterface_types_1.GraphicsTextEffect.Blue, graphicsInterface_types_1.GraphicsTextEffect.Bold, ], dividerEffects = []) { const isSpruce = effects.indexOf(graphicsInterface_types_1.GraphicsTextEffect.SpruceHeader) > -1; if (isSpruce && TerminalInterface.doesSupportColor()) { cfonts_1.default.say(message, { font: graphicsInterface_types_1.GraphicsTextEffect.SpruceHeader, align: 'left', space: false, colors: filterEffectsForCFonts(effects), }); } else { this.renderDivider(dividerEffects); this.renderLine(message, effects); this.renderDivider(dividerEffects); this.renderLine(''); } } setTitle(title) { process.stdout.write('\x1b]2;' + title + '\x07'); } renderHero(message, effects) { if (!TerminalInterface.doesSupportColor()) { this.renderLine(message); return; } const shouldStripVowels = process.stdout.columns < 80; let stripped = shouldStripVowels ? message.replace(/[aeiou]/gi, '') : message; if (shouldStripVowels && ['a', 'e', 'i', 'o', 'u'].indexOf(message[0].toLowerCase()) > -1) { stripped = `${message[0]}${stripped}`; } cfonts_1.default.say(stripped, { align: 'left', gradient: [graphicsInterface_types_1.GraphicsTextEffect.Red, graphicsInterface_types_1.GraphicsTextEffect.Blue], colors: effects ? filterEffectsForCFonts(effects) : undefined, }); } renderHint(message) { return this.renderLine(`👨‍🏫 ${message}`); } renderLine(message, effects = [], options) { let write = chalk_1.default; effects.forEach((effect) => { write = write[effect]; }); if (options?.eraseBeforeRender) { terminal_kit_1.terminal.eraseLine(); } this.log(effects.length > 0 ? write(message) : message); } renderWarning(message) { this.renderLine(`⚠️ ${message}`, [ graphicsInterface_types_1.GraphicsTextEffect.Bold, graphicsInterface_types_1.GraphicsTextEffect.Yellow, ]); } async startLoading(message) { this.stopLoading(); if (!this.isPromptActive) { TerminalInterface.loader = TerminalInterface.ora({ text: message, }).start(); } } stopLoading() { TerminalInterface.loader?.stop(); TerminalInterface.loader = null; } async confirm(question) { const confirmResult = await inquirer_1.default.prompt({ type: 'confirm', name: 'answer', message: question, }); return !!confirmResult.answer; } async waitForEnter(message) { await this.prompt({ type: 'text', label: `${message ? message + ' ' : ''}${chalk_1.default.bgGreenBright.black('hit enter')}`, }); this.renderLine(''); return; } clear() { void this.stopLoading(); console.clear(); } renderCodeSample(code) { try { const colored = emphasize_1.default.highlight('js', code).value; this.renderLine(colored); } catch (err) { this.renderWarning(err); } } async prompt(definition) { this.isPromptActive = true; if ((0, isCi_1.default)()) { throw new SpruceError_1.default({ code: 'CANNOT_PROMPT_IN_CI' }); } const name = generateInquirerFieldName(); const fieldDefinition = definition; const { defaultValue } = fieldDefinition; const promptOptions = { default: defaultValue, name, message: this.generatePromptLabel(fieldDefinition), }; const field = schema_1.FieldFactory.Field('prompt', fieldDefinition); promptOptions.transformer = (value) => { return field.toValueType(value); }; promptOptions.validate = (value) => { return (0, schema_1.areSchemaValuesValid)({ id: 'promptvalidateschema', fields: { prompt: fieldDefinition, }, }, { prompt: value }); // return field.validate(value, {}).length === 0 }; switch (fieldDefinition.type) { // Map select options to prompt list choices case 'boolean': promptOptions.type = 'confirm'; break; case 'select': promptOptions.type = fieldDefinition.isArray ? 'checkbox' : 'list'; promptOptions.choices = fieldDefinition.options.choices.map( // @ts-ignore (choice) => ({ name: choice.label, value: choice.value, checked: lodash_1.default.includes(fieldDefinition.defaultValue, choice.value), })); break; // Directory select // File select case 'directory': { if (fieldDefinition.isArray) { throw new SpruceError_1.default({ code: 'NOT_IMPLEMENTED', friendlyMessage: 'isArray file field not supported, prompt needs to be rewritten with isArray support', }); } const dirPath = path_1.default.join(fieldDefinition.defaultValue?.path ?? this.cwd, '/'); promptOptions.type = 'file'; promptOptions.root = dirPath; promptOptions.onlyShowDir = true; // Only let people select an actual file promptOptions.validate = (value) => { return (spruce_skill_utils_1.diskUtil.doesDirExist(value) && fs_extra_1.default.lstatSync(value).isDirectory()); }; // Strip out cwd from the paths while selecting promptOptions.transformer = (path) => { const cleanedPath = path.replace(promptOptions.root, ''); return cleanedPath.length === 0 ? promptOptions.root : cleanedPath; }; break; } case 'file': { if (fieldDefinition.isArray) { throw new SpruceError_1.default({ code: 'NOT_IMPLEMENTED', friendlyMessage: 'isArray file field not supported, prompt needs to be rewritten with isArray support', }); } const dirPath = path_1.default.join(fieldDefinition.defaultValue?.uri ?? this.cwd, '/'); // Check if directory is empty. const files = await (0, globby_1.default)(`${dirPath}**/*`); if (files.length === 0) { throw new SpruceError_1.default({ code: 'DIRECTORY_EMPTY', directory: dirPath, friendlyMessage: `I wanted to help you select a file, but none exist in ${dirPath}.`, }); } promptOptions.type = 'file'; promptOptions.root = dirPath; // Only let people select an actual file promptOptions.validate = (value) => { return (spruce_skill_utils_1.diskUtil.doesDirExist(value) && !fs_extra_1.default.lstatSync(value).isDirectory() && path_1.default.extname(value) === '.ts'); }; // Strip out cwd from the paths while selecting promptOptions.transformer = (path) => { const cleanedPath = path.replace(promptOptions.root, ''); return cleanedPath.length === 0 ? promptOptions.root : cleanedPath; }; break; } // Defaults to input default: promptOptions.type = 'input'; } const response = (await inquirer_1.default.prompt(promptOptions)); this.isPromptActive = false; const result = typeof response[name] !== 'undefined' ? field.toValueType(response[name]) : response[name]; return result; } generatePromptLabel(fieldDefinition) { let label = fieldDefinition.label; if (fieldDefinition.hint) { label = `${label} ${chalk_1.default.italic.dim(`(${fieldDefinition.hint})`)}`; } label = label + ': '; return label; } renderError(err) { this.stopLoading(); const message = err.message; // Remove message from stack so the message is not doubled up const stackLines = this.cleanStack(err); this.renderSection({ headline: message, lines: this.renderStackTraces ? stackLines.splice(0, 100) : undefined, headlineEffects: [graphicsInterface_types_1.GraphicsTextEffect.Bold, graphicsInterface_types_1.GraphicsTextEffect.Red], dividerEffects: [graphicsInterface_types_1.GraphicsTextEffect.Red], bodyEffects: [graphicsInterface_types_1.GraphicsTextEffect.Red], }); } cleanStack(err) { const message = err.message; let stack = err.stack ? err.stack.replace(message, '') : ''; if (err instanceof error_1.default) { let original = err.originalError; while (original) { stack = stack.replace('Error: ' + original.message, ''); original = original.originalError; } } const stackLines = stack.split('\n'); return stackLines; } renderProgressBar(options) { this.removeProgressBar(); this.progressBar = terminal_kit_1.terminal.progressBar({ ...options, percent: options.showPercent, eta: options.showEta, items: options.totalItems, inline: options.renderInline, }); } removeProgressBar() { if (this.progressBar) { this.progressBar.stop(); this.progressBar = null; } } updateProgressBar(options) { if (this.progressBar) { this.progressBar.update({ ...options, items: options.totalItems, }); } } async renderImage(_path, _options) { // const image = await terminalImage.file(path, options) this.renderLine('Images not supported....'); } async getCursorPosition() { return new Promise((resolve) => { terminal_kit_1.terminal.requestCursorLocation(); terminal_kit_1.terminal.getCursorLocation((err, x, y) => { resolve(err ? null : { x: x ?? 0, y: y ?? 0 }); }); }); } saveCursor() { terminal_kit_1.terminal.saveCursor(); } restoreCursor() { terminal_kit_1.terminal.restoreCursor(); } moveCursorTo(x, y) { terminal_kit_1.terminal.moveTo(x, y); } clearBelowCursor() { terminal_kit_1.terminal.eraseDisplayBelow(); } eraseLine() { terminal_kit_1.terminal.eraseLine(); } } exports.default = TerminalInterface; //# sourceMappingURL=TerminalInterface.js.map