appcenter-cli
Version:
Command line tool for Visual Studio App Center
575 lines (574 loc) • 19.4 kB
JavaScript
;
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;