@sprucelabs/spruce-cli
Version:
Command line interface for building Spruce skills.
497 lines • 19.8 kB
JavaScript
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
;