UNPKG

appcenter-cli

Version:

Command line tool for Visual Studio App Center

575 lines (574 loc) 19.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.reportObjectAsTitledTables = exports.reportTitledGroupsOfTables = exports.reportNewLineSeparatedArray = exports.report = exports.getOptionsForTwoColumnTableWithNoBorders = exports.getCommandOutputTableOptions = exports.table = exports.text = exports.help = exports.list = exports.progress = void 0; // Functions to support outputting stuff to the user const util_1 = require("util"); const io_options_1 = require("./io-options"); const os = require("os"); const wrap = require("wordwrap"); const tty = require("tty"); const Table = require("cli-table3"); const Spinner = require("cli-spinner").Spinner; const terminal_1 = require("./terminal"); const _ = require("lodash"); // // Display a progress spinner while waiting for the provided promise // to complete. // function progress(title, action) { const stdoutIsTerminal = tty.isatty(1); if (!io_options_1.formatIsParsingCompatible() && !io_options_1.isQuiet() && stdoutIsTerminal) { const spinner = new Spinner(title); spinner.start(); return action .then((result) => { spinner.stop(true); return result; }) .catch((ex) => { spinner.stop(true); throw ex; }); } else { return action; } } exports.progress = progress; // // Output an array of items, passing each item through a formatting // function. // function list(formatter, items) { console.assert(!io_options_1.formatIsCsv(), "this function doesn't support CSV mode"); if (!items || Object.keys(items).length === 0) { return; } if (!io_options_1.formatIsJson()) { items.map(formatter).forEach((text) => console.log(text)); } else { console.log(JSON.stringify(items)); } } exports.list = list; function help(...args) { console.assert(!io_options_1.formatIsCsv(), "this function doesn't support CSV mode"); let t; if (args.length === 0) { t = ""; } else { t = args[0]; } console.log(t); } exports.help = help; function text(...args) { console.assert(!io_options_1.formatIsCsv(), "this function doesn't support CSV mode"); let converter; let data; if (args.length === 1) { converter = null; data = args[0]; } else { [converter, data] = args; } if (io_options_1.formatIsJson()) { if (converter) { console.log(JSON.stringify(data)); } } else { converter = converter || ((s) => s); console.log(converter(data)); } } exports.text = text; // // Output tabular data. // By default, does a simple default table using cli-table3. // If you want to, you can pass in explicit table initialization // options. See https://github.com/cli-table/cli-table3 for docs // on the module. // function table(options, data) { console.assert(!io_options_1.formatIsCsv(), "this function doesn't support CSV mode"); if (!data) { data = options; options = undefined; } if (!io_options_1.formatIsJson()) { const cliTable = new Table(options); data.forEach((item) => cliTable.push(item)); console.log(cliTable.toString()); } else { console.log(JSON.stringify(data, null, " ")); } } exports.table = table; // Formatting helper for cli-table3 - default command output table style function getCommandOutputTableOptions(header) { return { head: header, style: { head: [], }, }; } exports.getCommandOutputTableOptions = getCommandOutputTableOptions; // // Formatting helper for cli-table3 - two columns with no table outlines. Used by // help commands for formatting lists of options, commands, etc. // function getOptionsForTwoColumnTableWithNoBorders(firstColumnWidth) { const consoleWidth = terminal_1.terminal.columns(); // There will be a single whitespace to the right from the each column, count it as unavailable const availableWidth = consoleWidth - 2; const secondColumnWidth = availableWidth - firstColumnWidth; return { chars: { top: "", "top-mid": "", "top-left": "", "top-right": "", bottom: "", "bottom-mid": "", "bottom-left": "", "bottom-right": "", left: "", "left-mid": "", mid: "", "mid-mid": "", right: "", "right-mid": "", middle: "", }, style: { "padding-left": 0, "padding-right": 0 }, colWidths: [firstColumnWidth, secondColumnWidth], wordWrap: true, }; } exports.getOptionsForTwoColumnTableWithNoBorders = getOptionsForTwoColumnTableWithNoBorders; // // Output a "report", which is a formatted output of a single object // with ability to control naming of fields in the output, lets you // output subobjects formatted nicely, and aligns everything for you. // // Usage looks like: // out.report([ // // Report format here, one array entry per field to output // [ "Field name to display", "path.to.property.to.display.in.data", optionalFormatter ], // [ "Second field name", "second.path.to.display", /* No formatter on this one */ ] // ], // "Optional string to print if no data is available", // theDataToFormat); // // The paths to properties are simple dotted property names like you'd use in javascript. // For example, in the profile list command, there's this line to display some of the // current profile properties: // // out.report([ // ["Username", "userName" ], // [ "Display Name", "displayName" ], // [ "Email", "email"] // ], "No logged in user. Use 'appcenter login' command to log in.", // user); // // "userName", "displayName", and "email" are names of properties on the user object being // passed in. If there were subobjects, for example if the input object looked like this: // // let user = { // name: { // userName: "chris", // displayName: "christav" // }, // email: "not.giving@real.email.here" // }; // // This format could be displayed in a report like so: // // out.report([ // [ "Username", "name.userName" ], // [ "Display Name", "name.displayName" ], // [ "Email", "email"] // ], "No logged in user. Use 'appcenter login' command to log in.", // user); // // Each report format entry can have a formatter supplied with it. This is a function that // takes the field's value and returns the appropriate string for display. By default // report just calls 'toString' on the value, but you can use a formatter to customize // to whatever you like. // // There are a few supplied formatters you can use out of the box attached to the report // function. They are: // // out.report.asDate: takes an input string, parses it as a Date object, then outputs the result. // out.report.inspect: takes any input object and returns the result of calling util.inspect on it. // out.report.allProperties: Takes an object with properties itself, and runs report // recursively on that object. This results in a nicely indented subreport // in the final output. // // In addition, if the formatter is itself an array, it becomes the report format for the subobjects. // So you can nest arbitrary reports. For exmaple, asssuming the same user field, then using this: // // out.report( // [ // [ "Email", "email" ], // // Nested subobject // [ "Names", "name", // [ // // report format for each of the subobject's fields // [ "User Name", "userName" ], // [ "Display Name", "displayName" ] // ] // ] // ], // { // // reformat our user to show subobjects // name: { // displayName: user.displayName, // userName: user.userName // }, // email: user.email // }); // // The resulting output looks like this: // // Email: not.giving@real.email.here // Names: // User Name: christav-yngr // Display Name: christav // // // Support functions for "report" output // function spaces(num) { if (num > 0) { return new Array(num + 1).join(" "); } return ""; } function toWidth(s, width) { const pad = width - s.length; return s + spaces(pad); } function defaultFormat(data) { if (typeof data === "undefined" || data === null) { return ""; } if (data instanceof Array) { if (data.length === 0) { return "[]"; } return data.join(", "); } return data.toString(); } function getProperty(value, propertyName) { if (typeof value === "undefined" || value === null) { return ""; } if (!propertyName) { return value; } const first = propertyName.split(".")[0]; const rest = propertyName.slice(first.length + 1); return getProperty(value[first], rest); } function doReport(indentation, reportFormat, data, outfn) { if (reportFormat.length === 0) { return; } let maxWidth = 80; if (process.stdout.isTTY) { maxWidth = process.stdout.columns; } const headerWidth = Math.max.apply(null, reportFormat.map(function (item) { return item[0].length; })) + 2; reportFormat.forEach(function (item) { const title = item[0] + ":"; const field = item[1]; const formatter = item[2] || defaultFormat; const value = getProperty(data, field); if (formatter instanceof Array) { outfn(spaces(indentation) + toWidth(title, headerWidth)); doReport(indentation + headerWidth, formatter, value, outfn); } else { const leftIndentation = "verbose: ".length + indentation + headerWidth; let formatted = wrap.hard(leftIndentation, maxWidth)(formatter(value)); formatted = spaces(indentation) + toWidth(title, headerWidth) + formatted.slice(leftIndentation); outfn(formatted); } }); } function makeReport(...args) { console.assert(!io_options_1.formatIsCsv(), "this function doesn't support CSV mode"); let reportFormat; let nullMessage; let data; if (args.length === 3) { [reportFormat, nullMessage, data] = args; } else { [reportFormat, data] = args; nullMessage = "No data available"; } if (!io_options_1.formatIsJson()) { if (data === null || data === undefined) { console.log(nullMessage); } else { doReport(0, reportFormat, data, console.log); } } else { console.log(JSON.stringify(data)); } } exports.report = makeReport; exports.report.allProperties = function (data) { if (typeof data === "undefined" || data === null || data === "") { return "[]"; } const subreport = Object.keys(data).map(function (key) { return [key, key]; }); const result = []; doReport(0, subreport, data, function (o) { result.push(o); }); result.push(""); return result.join(os.EOL); }; exports.report.asDate = function (data) { const date = new Date(data); if (io_options_1.formatIsJson()) { return date.toJSON(); } else { return date.toString(); } }; exports.report.inspect = function (data) { return util_1.inspect(data, { depth: null }); }; function reportNewLineSeparatedArray(reportFormat, data) { console.assert(!io_options_1.formatIsCsv(), "this function doesn't support CSV mode"); if (!io_options_1.formatIsJson()) { data.forEach((item, index) => { if (index) { console.log(""); } exports.report(reportFormat, item); }); } else { console.log(JSON.stringify(data)); } } exports.reportNewLineSeparatedArray = reportNewLineSeparatedArray; function reportTitledGroupsOfTables(dataGroups) { console.assert(!io_options_1.formatIsCsv(), "this function doesn't support CSV mode"); if (!io_options_1.formatIsJson()) { dataGroups.forEach((dataGroup, index) => { if (index) { console.log(""); } console.log(dataGroup.title); console.log(""); reportNewLineSeparatedArray(dataGroup.reportFormat, dataGroup.tables); }); } else { console.log(JSON.stringify(dataGroups)); } } exports.reportTitledGroupsOfTables = reportTitledGroupsOfTables; function getMarginStringFromLevel(level) { return _.repeat(" ", level * 4); } // // Formatting helper for cli-table3 - table with borders which can be moved to the right // It is used to show sub-tables // function getTableWithLeftMarginOptions(leftMargin) { return { chars: { top: "─", "top-mid": "┬", "top-left": leftMargin + "┌", "top-right": "┐", bottom: "─", "bottom-mid": "┴", "bottom-left": leftMargin + "└", "bottom-right": "┘", left: leftMargin + "│", "left-mid": leftMargin + "├", mid: "─", "mid-mid": "┼", right: "│", "right-mid": "┤", middle: "│", }, style: { "padding-left": 0, "padding-right": 0 }, wordWrap: true, }; } function convertNamedTablesToCsvString(stringTables) { const columnsCount = calculateNumberOfColumns(stringTables); const delimitersCount = columnsCount - 1; const delimitersString = _.repeat(",", delimitersCount); function outputTable(table) { let tableOutput = ""; // table name tableOutput += table.name + delimitersString + os.EOL; // table contents const contents = _.cloneDeep(table.content); contents.forEach((row, index) => { if (index) { tableOutput += os.EOL; } if (isINamedTable(row)) { tableOutput += outputTable(row); } else { row.length = columnsCount; tableOutput += row.join(","); } }); return tableOutput; } return stringTables.map((table) => outputTable(table)).join(os.EOL + delimitersString + os.EOL); } function convertNamedTablesToListString(stringTables) { function outputTable(table, level) { const paddedTable = padTableCells(table); let tableOutput = ""; const marginString = getMarginStringFromLevel(level); // table name tableOutput += marginString + paddedTable.name + os.EOL; // table contents const tableWithMergedStringArrays = []; // merging continuous string[] chains into Table objects for (const row of paddedTable.content) { if (isINamedTable(row)) { tableWithMergedStringArrays.push(row); } else { const lastElement = _.last(tableWithMergedStringArrays); let tableObject; if (_.isUndefined(lastElement) || isINamedTable(lastElement)) { tableObject = new Table(getTableWithLeftMarginOptions(marginString)); tableWithMergedStringArrays.push(tableObject); } else { tableObject = lastElement; } tableObject.push(row); } } tableWithMergedStringArrays.forEach((rowObject, rowIndex) => { if (rowIndex) { tableOutput += os.EOL; } if (isINamedTable(rowObject)) { tableOutput += outputTable(rowObject, level + 2); } else { tableOutput += rowObject.toString(); } }); return tableOutput; } return stringTables.map((table) => outputTable(table, 0)).join(os.EOL + os.EOL); } function getMapKey(level, cellIndex) { return [level, cellIndex].join(); } // returns map of max table cell width across tables on the same level - it is used to nicely align table columns vertically // key is [level, cellIndex].join() function calculateTableCellsMaxWidthAcrossLevels(wholeTable) { function calculate(table, level, levelAndCellIndexToMaxWidth) { for (const entry of table.content) { if (entry instanceof Array) { // row entry.forEach((cell, cellIndex) => { const key = getMapKey(level, cellIndex); levelAndCellIndexToMaxWidth.set(key, _.max([levelAndCellIndexToMaxWidth.get(key), cell.length])); }); } else { // inner table calculate(entry, level + 1, levelAndCellIndexToMaxWidth); } } return levelAndCellIndexToMaxWidth; } return calculate(wholeTable, 0, new Map()); } function padTableCells(wholeTable) { // calculating max widths for the cells const levelAndCellIndexToMaxWidth = calculateTableCellsMaxWidthAcrossLevels(wholeTable); // recursively pad content function pad(table, level) { const paddedContent = table.content.map((entry) => { if (entry instanceof Array) { // row return entry.map((cellContent, cellIndex) => _.padEnd(cellContent, levelAndCellIndexToMaxWidth.get(getMapKey(level, cellIndex)))); } else { // inner table return pad(entry, level + 1); } }); return { name: table.name, content: paddedContent, }; } return pad(wholeTable, 0); } function calculateNumberOfColumns(tables) { if (tables.length) { return _.max(tables.map((table) => { if (table instanceof Array) { return table.length || 1; } else { return calculateNumberOfColumns(table.content); } })); } else { return 1; } } function isINamedTable(object) { return (object != null && typeof object.name === "string" && object.content instanceof Array && object.content.every((item) => isINamedTable(item) || (item instanceof Array && item.every((itemComponent) => typeof itemComponent === "string")))); } function reportObjectAsTitledTables(toNamedTables, object) { if (io_options_1.formatIsJson()) { console.log(JSON.stringify(object)); } else { let output; if (io_options_1.formatIsCsv()) { const stringTables = toNamedTables(object, (num) => num.toString(), (date) => date.toISOString(), (percentage) => percentage.toString()); output = convertNamedTablesToCsvString(stringTables); } else { const stringTables = toNamedTables(object, (num) => _.round(num, 2).toString(), (date) => date.toString(), (percentage) => _.round(percentage, 2).toString() + "%"); output = convertNamedTablesToListString(stringTables); } console.log(output); } } exports.reportObjectAsTitledTables = reportObjectAsTitledTables;