From 5ece939de59ff219b8ae2c8b621a3f6cafe6169c Mon Sep 17 00:00:00 2001 From: Peter Hale Date: Fri, 14 Jan 2022 08:09:28 -0700 Subject: [PATCH 01/28] feat: merge cli-ux library with oclif/core Removes the circlular dependency between oclif/core and cli-ux @W-10255961@ --- .gitignore | 1 + package.json | 21 +- src/cli-ux/action/base.ts | 201 +++++++++++++++ src/cli-ux/action/pride-spinner.ts | 32 +++ src/cli-ux/action/simple.ts | 44 ++++ src/cli-ux/action/spinner.ts | 91 +++++++ src/cli-ux/action/spinners.ts | 372 +++++++++++++++++++++++++++ src/cli-ux/config.ts | 65 +++++ src/cli-ux/deps.ts | 45 ++++ src/cli-ux/exit.ts | 17 ++ src/cli-ux/global.d.ts | 7 + src/cli-ux/index.ts | 157 ++++++++++++ src/cli-ux/list.ts | 34 +++ src/cli-ux/open.ts | 88 +++++++ src/cli-ux/prompt.ts | 167 ++++++++++++ src/cli-ux/styled/header.ts | 7 + src/cli-ux/styled/json.ts | 17 ++ src/cli-ux/styled/object.ts | 40 +++ src/cli-ux/styled/progress.ts | 14 + src/cli-ux/styled/table.ts | 384 ++++++++++++++++++++++++++++ src/cli-ux/styled/tree.ts | 40 +++ src/cli-ux/wait.ts | 6 + src/command.ts | 3 +- src/index.ts | 2 + test/cli-ux/fancy.ts | 24 ++ test/cli-ux/helpers/init.js | 3 + test/cli-ux/index.test.ts | 24 ++ test/cli-ux/prompt.test.ts | 75 ++++++ test/cli-ux/styled/object.test.ts | 19 ++ test/cli-ux/styled/progress.test.ts | 29 +++ test/cli-ux/styled/table.test.ts | 321 +++++++++++++++++++++++ test/cli-ux/styled/tree.test.ts | 24 ++ test/helpers/init.js | 1 + tsconfig.json | 3 +- yarn.lock | 206 +++++---------- 35 files changed, 2432 insertions(+), 152 deletions(-) create mode 100644 src/cli-ux/action/base.ts create mode 100644 src/cli-ux/action/pride-spinner.ts create mode 100644 src/cli-ux/action/simple.ts create mode 100644 src/cli-ux/action/spinner.ts create mode 100644 src/cli-ux/action/spinners.ts create mode 100644 src/cli-ux/config.ts create mode 100644 src/cli-ux/deps.ts create mode 100644 src/cli-ux/exit.ts create mode 100644 src/cli-ux/global.d.ts create mode 100644 src/cli-ux/index.ts create mode 100644 src/cli-ux/list.ts create mode 100644 src/cli-ux/open.ts create mode 100644 src/cli-ux/prompt.ts create mode 100644 src/cli-ux/styled/header.ts create mode 100644 src/cli-ux/styled/json.ts create mode 100644 src/cli-ux/styled/object.ts create mode 100644 src/cli-ux/styled/progress.ts create mode 100644 src/cli-ux/styled/table.ts create mode 100644 src/cli-ux/styled/tree.ts create mode 100644 src/cli-ux/wait.ts create mode 100644 test/cli-ux/fancy.ts create mode 100644 test/cli-ux/helpers/init.js create mode 100644 test/cli-ux/index.test.ts create mode 100644 test/cli-ux/prompt.test.ts create mode 100644 test/cli-ux/styled/object.test.ts create mode 100644 test/cli-ux/styled/progress.test.ts create mode 100644 test/cli-ux/styled/table.test.ts create mode 100644 test/cli-ux/styled/tree.test.ts diff --git a/.gitignore b/.gitignore index bb0a634b4..54209a862 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ /node_modules /tmp /test/tmp +.DS_Store diff --git a/package.json b/package.json index 6c40a5e0e..3324120a8 100644 --- a/package.json +++ b/package.json @@ -6,19 +6,31 @@ "bugs": "https://github.com/oclif/core/issues", "dependencies": { "@oclif/linewrap": "^1.0.0", + "@oclif/screen": "^3.0.2", + "ansi-escapes": "^4.3.0", + "ansi-styles": "^4.2.0", + "cardinal": "^2.1.1", "chalk": "^4.1.2", "clean-stack": "^3.0.1", - "cli-ux": "^6.0.6", + "cli-progress": "^3.10.0", "debug": "^4.3.3", "ejs": "^3.1.6", "fs-extra": "^9.1.0", "get-package-type": "^0.1.0", "globby": "^11.0.4", + "hyperlinker": "^1.0.0", "indent-string": "^4.0.0", "is-wsl": "^2.2.0", + "js-yaml": "^3.13.1", + "lodash": "^4.17.21", + "natural-orderby": "^2.0.3", + "object-treeify": "^1.1.4", + "password-prompt": "^1.1.2", "semver": "^7.3.5", "string-width": "^4.2.3", "strip-ansi": "^6.0.1", + "supports-color": "^8.1.1", + "supports-hyperlinks": "^2.2.0", "tslib": "^2.3.1", "widest-line": "^3.1.0", "wrap-ansi": "^7.0.0" @@ -28,12 +40,16 @@ "@oclif/plugin-help": "^5.1.7", "@oclif/plugin-plugins": "^2.0.8", "@oclif/test": "^1.2.8", + "@types/ansi-styles": "^3.2.1", "@types/chai": "^4.2.22", "@types/chai-as-promised": "^7.1.4", "@types/clean-stack": "^2.1.1", + "@types/cli-progress": "^3.9.2", "@types/ejs": "^3.1.0", "@types/fs-extra": "^9.0.13", "@types/indent-string": "^4.0.1", + "@types/js-yaml": "^3.12.1", + "@types/lodash": "^4.14.117", "@types/mocha": "^8.2.3", "@types/nock": "^11.1.0", "@types/node": "^15.14.9", @@ -42,6 +58,7 @@ "@types/semver": "^7.3.9", "@types/shelljs": "^0.8.10", "@types/strip-ansi": "^5.2.1", + "@types/supports-color": "^8.1.1", "@types/wrap-ansi": "^3.0.0", "chai": "^4.3.4", "chai-as-promised": "^7.1.1", @@ -59,7 +76,7 @@ "shx": "^0.3.3", "sinon": "^11.1.2", "ts-node": "^9.1.1", - "typescript": "4.5.2" + "typescript": "4.4.4" }, "resolutions": { "@oclif/command": "1.8.9", diff --git a/src/cli-ux/action/base.ts b/src/cli-ux/action/base.ts new file mode 100644 index 000000000..cfb51b50b --- /dev/null +++ b/src/cli-ux/action/base.ts @@ -0,0 +1,201 @@ +import castArray from 'lodash/castArray' +import {inspect} from 'util' + +export interface ITask { + action: string; + status: string | undefined; + active: boolean; +} + +export type ActionType = 'spinner' | 'simple' | 'debug' + +export interface Options { + stdout?: boolean; +} + +export class ActionBase { + type!: ActionType + + std: 'stdout' | 'stderr' = 'stderr' + + protected stdmocks?: ['stdout' | 'stderr', string[]][] + + private stdmockOrigs = { + stdout: process.stdout.write, + stderr: process.stderr.write, + } + + public start(action: string, status?: string, opts: Options = {}) { + this.std = opts.stdout ? 'stdout' : 'stderr' + const task = {action, status, active: Boolean(this.task && this.task.active)} + this.task = task + + this._start() + task.active = true + this._stdout(true) + } + + public stop(msg = 'done') { + const task = this.task + if (!task) { + return + } + + this._stop(msg) + task.active = false + this.task = undefined + this._stdout(false) + } + + private get globals(): { action: { task?: ITask }; output: string | undefined } { + (global as any)['cli-ux'] = (global as any)['cli-ux'] || {} + const globals = (global as any)['cli-ux'] + globals.action = globals.action || {} + return globals + } + + public get task(): ITask | undefined { + return this.globals.action.task + } + + public set task(task: ITask | undefined) { + this.globals.action.task = task + } + + protected get output(): string | undefined { + return this.globals.output + } + + protected set output(output: string | undefined) { + this.globals.output = output + } + + get running(): boolean { + return Boolean(this.task) + } + + get status(): string | undefined { + return this.task ? this.task.status : undefined + } + + set status(status: string | undefined) { + const task = this.task + if (!task) { + return + } + + if (task.status === status) { + return + } + + this._updateStatus(status, task.status) + task.status = status + } + + public async pauseAsync(fn: () => Promise, icon?: string) { + const task = this.task + const active = task && task.active + if (task && active) { + this._pause(icon) + this._stdout(false) + task.active = false + } + + const ret = await fn() + if (task && active) { + this._resume() + } + + return ret + } + + public pause(fn: () => any, icon?: string) { + const task = this.task + const active = task && task.active + if (task && active) { + this._pause(icon) + this._stdout(false) + task.active = false + } + + const ret = fn() + if (task && active) { + this._resume() + } + + return ret + } + + protected _start() { + throw new Error('not implemented') + } + + protected _stop(_: string) { + throw new Error('not implemented') + } + + protected _resume() { + if (this.task) this.start(this.task.action, this.task.status) + } + + protected _pause(_?: string) { + throw new Error('not implemented') + } + + protected _updateStatus(_: string | undefined, __?: string) {} + + // mock out stdout/stderr so it doesn't screw up the rendering + protected _stdout(toggle: boolean) { + try { + const outputs: ['stdout', 'stderr'] = ['stdout', 'stderr'] + if (toggle) { + if (this.stdmocks) return + this.stdmockOrigs = { + stdout: process.stdout.write, + stderr: process.stderr.write, + } + + this.stdmocks = [] + for (const std of outputs) { + (process[std] as any).write = (...args: any[]) => { + this.stdmocks!.push([std, args] as ['stdout' | 'stderr', string[]]) + } + } + } else { + if (!this.stdmocks) return + // this._write('stderr', '\nresetstdmock\n\n\n') + delete this.stdmocks + for (const std of outputs) process[std].write = this.stdmockOrigs[std] as any + } + } catch (error) { + this._write('stderr', inspect(error)) + } + } + + // flush mocked stdout/stderr + protected _flushStdout() { + try { + let output = '' + let std: 'stdout' | 'stderr' | undefined + while (this.stdmocks && this.stdmocks.length > 0) { + const cur = this.stdmocks.shift() as ['stdout' | 'stderr', string[]] + std = cur[0] + this._write(std, cur[1]) + output += (cur[1][0] as any).toString('utf8') + } + // add newline if there isn't one already + // otherwise we'll just overwrite it when we render + + if (output && std && output[output.length - 1] !== '\n') { + this._write(std, '\n') + } + } catch (error) { + this._write('stderr', inspect(error)) + } + } + + // write to the real stdout/stderr + protected _write(std: 'stdout' | 'stderr', s: string | string[]) { + this.stdmockOrigs[std].apply(process[std], castArray(s) as [string]) + } +} diff --git a/src/cli-ux/action/pride-spinner.ts b/src/cli-ux/action/pride-spinner.ts new file mode 100644 index 000000000..776e13d1c --- /dev/null +++ b/src/cli-ux/action/pride-spinner.ts @@ -0,0 +1,32 @@ +// tslint:disable restrict-plus-operands + +import chalk from 'chalk' +import * as supportsColor from 'supports-color' + +import SpinnerAction from './spinner' + +function color(s: string, frameIndex: number): string { + const prideColors = [ + chalk.keyword('pink'), + chalk.red, + chalk.keyword('orange'), + chalk.yellow, + chalk.green, + chalk.cyan, + chalk.blue, + chalk.magenta, + ] + + if (!supportsColor) return s + const has256 = supportsColor.stdout ? supportsColor.stdout.has256 : (process.env.TERM || '').includes('256') + const prideColor = prideColors[frameIndex] || prideColors[0] + return has256 ? prideColor(s) : chalk.magenta(s) +} + +export default class PrideSpinnerAction extends SpinnerAction { + protected _frame(): string { + const frame = this.frames[this.frameIndex] + this.frameIndex = ++this.frameIndex % this.frames.length + return color(frame, this.frameIndex) + } +} diff --git a/src/cli-ux/action/simple.ts b/src/cli-ux/action/simple.ts new file mode 100644 index 000000000..48722ae32 --- /dev/null +++ b/src/cli-ux/action/simple.ts @@ -0,0 +1,44 @@ +import {ActionBase, ActionType} from './base' + +export default class SimpleAction extends ActionBase { + public type: ActionType = 'simple' + + protected _start() { + const task = this.task + if (!task) return + this._render(task.action, task.status) + } + + protected _pause(icon?: string) { + if (icon) this._updateStatus(icon) + else this._flush() + } + + protected _resume() {} + + protected _updateStatus(status: string, prevStatus?: string, newline = false) { + const task = this.task + if (!task) return + if (task.active && !prevStatus) this._write(this.std, ` ${status}`) + else this._write(this.std, `${task.action}... ${status}`) + if (newline || !prevStatus) this._flush() + } + + protected _stop(status: string) { + const task = this.task + if (!task) return + this._updateStatus(status, task.status, true) + } + + private _render(action: string, status?: string) { + const task = this.task + if (!task) return + if (task.active) this._flush() + this._write(this.std, status ? `${action}... ${status}` : `${action}...`) + } + + private _flush() { + this._write(this.std, '\n') + this._flushStdout() + } +} diff --git a/src/cli-ux/action/spinner.ts b/src/cli-ux/action/spinner.ts new file mode 100644 index 000000000..7a68007d2 --- /dev/null +++ b/src/cli-ux/action/spinner.ts @@ -0,0 +1,91 @@ +// tslint:disable restrict-plus-operands + +import chalk from 'chalk' +import * as supportsColor from 'supports-color' + +import deps from '../deps' + +import {ActionBase, ActionType} from './base' +/* eslint-disable-next-line node/no-missing-require */ +const spinners = require('./spinners') + +function color(s: string): string { + if (!supportsColor) return s + const has256 = supportsColor.stdout ? supportsColor.stdout.has256 : (process.env.TERM || '').includes('256') + return has256 ? `\u001B[38;5;104m${s}${deps.ansiStyles.reset.open}` : chalk.magenta(s) +} + +export default class SpinnerAction extends ActionBase { + public type: ActionType = 'spinner' + + spinner?: NodeJS.Timeout + + frames: any + + frameIndex: number + + constructor() { + super() + this.frames = spinners[process.platform === 'win32' ? 'line' : 'dots2'].frames + this.frameIndex = 0 + } + + protected _start() { + this._reset() + if (this.spinner) clearInterval(this.spinner) + this._render() + this.spinner = setInterval(icon => + this._render.bind(this)(icon), + process.platform === 'win32' ? 500 : 100, + 'spinner', + ) + const interval = this.spinner + interval.unref() + } + + protected _stop(status: string) { + if (this.task) this.task.status = status + if (this.spinner) clearInterval(this.spinner) + this._render() + this.output = undefined + } + + protected _pause(icon?: string) { + if (this.spinner) clearInterval(this.spinner) + this._reset() + if (icon) this._render(` ${icon}`) + this.output = undefined + } + + protected _frame(): string { + const frame = this.frames[this.frameIndex] + this.frameIndex = ++this.frameIndex % this.frames.length + return color(frame) + } + + private _render(icon?: string) { + const task = this.task + if (!task) return + this._reset() + this._flushStdout() + const frame = icon === 'spinner' ? ` ${this._frame()}` : icon || '' + const status = task.status ? ` ${task.status}` : '' + this.output = `${task.action}...${frame}${status}\n` + this._write(this.std, this.output) + } + + private _reset() { + if (!this.output) return + const lines = this._lines(this.output) + this._write(this.std, deps.ansiEscapes.cursorLeft + deps.ansiEscapes.cursorUp(lines) + deps.ansiEscapes.eraseDown) + this.output = undefined + } + + private _lines(s: string): number { + return deps + .stripAnsi(s) + .split('\n') + .map(l => Math.ceil(l.length / deps.screen.errtermwidth)) + .reduce((c, i) => c + i, 0) + } +} diff --git a/src/cli-ux/action/spinners.ts b/src/cli-ux/action/spinners.ts new file mode 100644 index 000000000..e953a876d --- /dev/null +++ b/src/cli-ux/action/spinners.ts @@ -0,0 +1,372 @@ +module.exports = { + hexagon: { + interval: 400, + frames: ['⬡', '⬢'], + }, + dots: { + interval: 80, + frames: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'], + }, + dots2: { + interval: 80, + frames: ['⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷'], + }, + dots3: { + interval: 80, + frames: ['⠋', '⠙', '⠚', '⠞', '⠖', '⠦', '⠴', '⠲', '⠳', '⠓'], + }, + dots4: { + interval: 80, + frames: ['⠄', '⠆', '⠇', '⠋', '⠙', '⠸', '⠰', '⠠', '⠰', '⠸', '⠙', '⠋', '⠇', '⠆'], + }, + dots5: { + interval: 80, + frames: ['⠋', '⠙', '⠚', '⠒', '⠂', '⠂', '⠒', '⠲', '⠴', '⠦', '⠖', '⠒', '⠐', '⠐', '⠒', '⠓', '⠋'], + }, + dots6: { + interval: 80, + frames: [ + '⠁', + '⠉', + '⠙', + '⠚', + '⠒', + '⠂', + '⠂', + '⠒', + '⠲', + '⠴', + '⠤', + '⠄', + '⠄', + '⠤', + '⠴', + '⠲', + '⠒', + '⠂', + '⠂', + '⠒', + '⠚', + '⠙', + '⠉', + '⠁', + ], + }, + dots7: { + interval: 80, + frames: [ + '⠈', + '⠉', + '⠋', + '⠓', + '⠒', + '⠐', + '⠐', + '⠒', + '⠖', + '⠦', + '⠤', + '⠠', + '⠠', + '⠤', + '⠦', + '⠖', + '⠒', + '⠐', + '⠐', + '⠒', + '⠓', + '⠋', + '⠉', + '⠈', + ], + }, + dots8: { + interval: 80, + frames: [ + '⠁', + '⠁', + '⠉', + '⠙', + '⠚', + '⠒', + '⠂', + '⠂', + '⠒', + '⠲', + '⠴', + '⠤', + '⠄', + '⠄', + '⠤', + '⠠', + '⠠', + '⠤', + '⠦', + '⠖', + '⠒', + '⠐', + '⠐', + '⠒', + '⠓', + '⠋', + '⠉', + '⠈', + '⠈', + ], + }, + dots9: { + interval: 80, + frames: ['⢹', '⢺', '⢼', '⣸', '⣇', '⡧', '⡗', '⡏'], + }, + dots10: { + interval: 80, + frames: ['⢄', '⢂', '⢁', '⡁', '⡈', '⡐', '⡠'], + }, + dots11: { + interval: 100, + frames: ['⠁', '⠂', '⠄', '⡀', '⢀', '⠠', '⠐', '⠈'], + }, + line: { + interval: 130, + frames: ['-', '\\', '|', '/'], + }, + line2: { + interval: 100, + frames: ['⠂', '-', '–', '—', '–', '-'], + }, + pipe: { + interval: 100, + frames: ['┤', '┘', '┴', '└', '├', '┌', '┬', '┐'], + }, + simpleDots: { + interval: 400, + frames: ['. ', '.. ', '...', ' '], + }, + simpleDotsScrolling: { + interval: 200, + frames: ['. ', '.. ', '...', ' ..', ' .', ' '], + }, + star: { + interval: 70, + frames: ['✶', '✸', '✹', '✺', '✹', '✷'], + }, + star2: { + interval: 80, + frames: ['+', 'x', '*'], + }, + flip: { + interval: 70, + frames: ['_', '_', '_', '-', '`', '`', '\'', '´', '-', '_', '_', '_'], + }, + hamburger: { + interval: 100, + frames: ['☱', '☲', '☴'], + }, + growVertical: { + interval: 120, + frames: ['▁', '▃', '▄', '▅', '▆', '▇', '▆', '▅', '▄', '▃'], + }, + growHorizontal: { + interval: 120, + frames: ['▏', '▎', '▍', '▌', '▋', '▊', '▉', '▊', '▋', '▌', '▍', '▎'], + }, + balloon: { + interval: 140, + frames: [' ', '.', 'o', 'O', '@', '*', ' '], + }, + balloon2: { + interval: 120, + frames: ['.', 'o', 'O', '°', 'O', 'o', '.'], + }, + noise: { + interval: 100, + frames: ['▓', '▒', '░'], + }, + bounce: { + interval: 120, + frames: ['⠁', '⠂', '⠄', '⠂'], + }, + boxBounce: { + interval: 120, + frames: ['▖', '▘', '▝', '▗'], + }, + boxBounce2: { + interval: 100, + frames: ['▌', '▀', '▐', '▄'], + }, + triangle: { + interval: 50, + frames: ['◢', '◣', '◤', '◥'], + }, + arc: { + interval: 100, + frames: ['◜', '◠', '◝', '◞', '◡', '◟'], + }, + circle: { + interval: 120, + frames: ['◡', '⊙', '◠'], + }, + squareCorners: { + interval: 180, + frames: ['◰', '◳', '◲', '◱'], + }, + circleQuarters: { + interval: 120, + frames: ['◴', '◷', '◶', '◵'], + }, + circleHalves: { + interval: 50, + frames: ['◐', '◓', '◑', '◒'], + }, + squish: { + interval: 100, + frames: ['╫', '╪'], + }, + toggle: { + interval: 250, + frames: ['⊶', '⊷'], + }, + toggle2: { + interval: 80, + frames: ['▫', '▪'], + }, + toggle3: { + interval: 120, + frames: ['□', '■'], + }, + toggle4: { + interval: 100, + frames: ['■', '□', '▪', '▫'], + }, + toggle5: { + interval: 100, + frames: ['▮', '▯'], + }, + toggle6: { + interval: 300, + frames: ['ဝ', '၀'], + }, + toggle7: { + interval: 80, + frames: ['⦾', '⦿'], + }, + toggle8: { + interval: 100, + frames: ['◍', '◌'], + }, + toggle9: { + interval: 100, + frames: ['◉', '◎'], + }, + toggle10: { + interval: 100, + frames: ['㊂', '㊀', '㊁'], + }, + toggle11: { + interval: 50, + frames: ['⧇', '⧆'], + }, + toggle12: { + interval: 120, + frames: ['☗', '☖'], + }, + toggle13: { + interval: 80, + frames: ['=', '*', '-'], + }, + arrow: { + interval: 100, + frames: ['←', '↖', '↑', '↗', '→', '↘', '↓', '↙'], + }, + arrow2: { + interval: 80, + frames: ['⬆️ ', '↗️ ', '➡️ ', '↘️ ', '⬇️ ', '↙️ ', '⬅️ ', '↖️ '], + }, + arrow3: { + interval: 120, + frames: ['▹▹▹▹▹', '▸▹▹▹▹', '▹▸▹▹▹', '▹▹▸▹▹', '▹▹▹▸▹', '▹▹▹▹▸'], + }, + bouncingBar: { + interval: 80, + frames: ['[ ]', '[ =]', '[ ==]', '[ ===]', '[====]', '[=== ]', '[== ]', '[= ]'], + }, + bouncingBall: { + interval: 80, + frames: [ + '( ● )', + '( ● )', + '( ● )', + '( ● )', + '( ●)', + '( ● )', + '( ● )', + '( ● )', + '( ● )', + '(● )', + ], + }, + smiley: { + interval: 200, + frames: ['😄 ', '😝 '], + }, + monkey: { + interval: 300, + frames: ['🙈 ', '🙈 ', '🙉 ', '🙊 '], + }, + hearts: { + interval: 100, + frames: ['💛 ', '💙 ', '💜 ', '💚 ', '❤️ '], + }, + clock: { + interval: 100, + frames: ['🕐 ', '🕑 ', '🕒 ', '🕓 ', '🕔 ', '🕕 ', '🕖 ', '🕗 ', '🕘 ', '🕙 ', '🕚 '], + }, + earth: { + interval: 180, + frames: ['🌍 ', '🌎 ', '🌏 '], + }, + moon: { + interval: 80, + frames: ['🌑 ', '🌒 ', '🌓 ', '🌔 ', '🌕 ', '🌖 ', '🌗 ', '🌘 '], + }, + runner: { + interval: 140, + frames: ['🚶 ', '🏃 '], + }, + pong: { + interval: 80, + frames: [ + '▐⠂ ▌', + '▐⠈ ▌', + '▐ ⠂ ▌', + '▐ ⠠ ▌', + '▐ ⡀ ▌', + '▐ ⠠ ▌', + '▐ ⠂ ▌', + '▐ ⠈ ▌', + '▐ ⠂ ▌', + '▐ ⠠ ▌', + '▐ ⡀ ▌', + '▐ ⠠ ▌', + '▐ ⠂ ▌', + '▐ ⠈ ▌', + '▐ ⠂▌', + '▐ ⠠▌', + '▐ ⡀▌', + '▐ ⠠ ▌', + '▐ ⠂ ▌', + '▐ ⠈ ▌', + '▐ ⠂ ▌', + '▐ ⠠ ▌', + '▐ ⡀ ▌', + '▐ ⠠ ▌', + '▐ ⠂ ▌', + '▐ ⠈ ▌', + '▐ ⠂ ▌', + '▐ ⠠ ▌', + '▐ ⡀ ▌', + '▐⠠ ▌', + ], + }, +} diff --git a/src/cli-ux/config.ts b/src/cli-ux/config.ts new file mode 100644 index 000000000..e3cb40f13 --- /dev/null +++ b/src/cli-ux/config.ts @@ -0,0 +1,65 @@ +import * as semver from 'semver' + +import {ActionBase} from './action/base' + +const version = semver.parse(require('../../package.json').version)! + +export type Levels = 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'trace' + +export interface ConfigMessage { + type: 'config'; + prop: string; + value: any; +} + +const g: any = global +const globals = g['cli-ux'] || (g['cli-ux'] = {}) + +const actionType = ( + Boolean(process.stderr.isTTY) && + !process.env.CI && + !['dumb', 'emacs-color'].includes(process.env.TERM!) && + 'spinner' +) || 'simple' + +/* eslint-disable node/no-missing-require */ +const Action = actionType === 'spinner' ? require('./action/spinner').default : require('./action/simple').default +const PrideAction = actionType === 'spinner' ? require('./action/pride-spinner').default : require('./action/simple').default +/* eslint-enable node/no-missing-require */ + +export class Config { + outputLevel: Levels = 'info' + + action: ActionBase = new Action() + + prideAction: ActionBase = new PrideAction() + + errorsHandled = false + + showStackTrace = true + + get debug(): boolean { + return globals.debug || process.env.DEBUG === '*' + } + + set debug(v: boolean) { + globals.debug = v + } + + get context(): any { + return globals.context || {} + } + + set context(v: any) { + globals.context = v + } +} + +function fetch() { + if (globals[version.major]) return globals[version.major] + globals[version.major] = new Config() + return globals[version.major] +} + +export const config: Config = fetch() +export default config diff --git a/src/cli-ux/deps.ts b/src/cli-ux/deps.ts new file mode 100644 index 000000000..329486e84 --- /dev/null +++ b/src/cli-ux/deps.ts @@ -0,0 +1,45 @@ +/* eslint-disable node/no-missing-require */ +export default { + get stripAnsi(): (string: string) => string { + return require('strip-ansi') + }, + get ansiStyles(): typeof import('ansi-styles') { + return require('ansi-styles') + }, + get ansiEscapes(): any { + return require('ansi-escapes') + }, + get passwordPrompt(): any { + return require('password-prompt') + }, + get screen(): typeof import('@oclif/screen') { + return require('@oclif/screen') + }, + get open(): typeof import('./open').default { + return require('./open').default + }, + get prompt(): typeof import('./prompt') { + return require('./prompt') + }, + get styledObject(): typeof import('./styled/object').default { + return require('./styled/object').default + }, + get styledHeader(): typeof import('./styled/header').default { + return require('./styled/header').default + }, + get styledJSON(): typeof import('./styled/json').default { + return require('./styled/json').default + }, + get table(): typeof import('./styled/table').table { + return require('./styled/table').table + }, + get tree(): typeof import('./styled/tree').default { + return require('./styled/tree').default + }, + get wait(): typeof import('./wait').default { + return require('./wait').default + }, + get progress(): typeof import ('./styled/progress').default { + return require('./styled/progress').default + }, +} diff --git a/src/cli-ux/exit.ts b/src/cli-ux/exit.ts new file mode 100644 index 000000000..4f036df30 --- /dev/null +++ b/src/cli-ux/exit.ts @@ -0,0 +1,17 @@ +export class ExitError extends Error { + public 'cli-ux': { + exit: number; + } + + public code: 'EEXIT' + + public error?: Error + + constructor(status: number, error?: Error) { + const code = 'EEXIT' + super(error ? error.message : `${code}: ${status}`) + this.error = error + this['cli-ux'] = {exit: status} + this.code = code + } +} diff --git a/src/cli-ux/global.d.ts b/src/cli-ux/global.d.ts new file mode 100644 index 000000000..9b45c3a36 --- /dev/null +++ b/src/cli-ux/global.d.ts @@ -0,0 +1,7 @@ +// tslint:disable + +declare namespace NodeJS { + interface Global { + 'cli-ux': any; + } +} diff --git a/src/cli-ux/index.ts b/src/cli-ux/index.ts new file mode 100644 index 000000000..b292d9f40 --- /dev/null +++ b/src/cli-ux/index.ts @@ -0,0 +1,157 @@ +import * as Errors from '../../src/errors' +import * as util from 'util' + +import {ActionBase} from './action/base' +import {config, Config} from './config' +import deps from './deps' +import {ExitError} from './exit' +import {IPromptOptions} from './prompt' +import * as Table from './styled/table' + +const hyperlinker = require('hyperlinker') + +function timeout(p: Promise, ms: number) { + function wait(ms: number, unref = false) { + return new Promise(resolve => { + const t: any = setTimeout(() => resolve(null), ms) + if (unref) t.unref() + }) + } + + return Promise.race([p, wait(ms, true).then(() => ux.error('timed out'))]) +} + +async function flush() { + const p = new Promise(resolve => { + process.stdout.once('drain', () => resolve(null)) + }) + process.stdout.write('') + return p +} + +export const ux = { + config, + + warn: Errors.warn, + error: Errors.error, + exit: Errors.exit, + + get prompt() { + return deps.prompt.prompt + }, + /** + * "press anykey to continue" + */ + get anykey() { + return deps.prompt.anykey + }, + get confirm() { + return deps.prompt.confirm + }, + get action() { + return config.action + }, + get prideAction() { + return config.prideAction + }, + styledObject(obj: any, keys?: string[]) { + ux.info(deps.styledObject(obj, keys)) + }, + get styledHeader() { + return deps.styledHeader + }, + get styledJSON() { + return deps.styledJSON + }, + get table() { + return deps.table + }, + get tree() { + return deps.tree + }, + get open() { + return deps.open + }, + get wait() { + return deps.wait + }, + get progress() { + return deps.progress + }, + + async done() { + config.action.stop() + // await flushStdout() + }, + + trace(format: string, ...args: string[]) { + if (this.config.outputLevel === 'trace') { + process.stdout.write(util.format(format, ...args) + '\n') + } + }, + + debug(format: string, ...args: string[]) { + if (['trace', 'debug'].includes(this.config.outputLevel)) { + process.stdout.write(util.format(format, ...args) + '\n') + } + }, + + info(format: string, ...args: string[]) { + process.stdout.write(util.format(format, ...args) + '\n') + }, + + log(format?: string, ...args: string[]) { + this.info(format || '', ...args) + }, + + url(text: string, uri: string, params = {}) { + const supports = require('supports-hyperlinks') + if (supports.stdout) { + this.log(hyperlinker(text, uri, params)) + } else { + this.log(uri) + } + }, + + annotation(text: string, annotation: string) { + const supports = require('supports-hyperlinks') + if (supports.stdout) { + // \u001b]8;;https://google.com\u0007sometext\u001b]8;;\u0007 + this.log(`\u001B]1337;AddAnnotation=${text.length}|${annotation}\u0007${text}`) + } else { + this.log(text) + } + }, + + async flush() { + await timeout(flush(), 10_000) + }, +} +export default ux +export const cli = ux + +export { + config, + ActionBase, + Config, + ExitError, + IPromptOptions, + Table, +} + +const cliuxProcessExitHandler = async () => { + try { + await ux.done() + } catch (error) { + // tslint:disable no-console + console.error(error) + process.exitCode = 1 + } +} + +// to avoid MaxListenersExceededWarning +// only attach named listener once +const cliuxListener = process.listeners('exit').find(fn => fn.name === cliuxProcessExitHandler.name) +if (!cliuxListener) { + process.once('exit', cliuxProcessExitHandler) +} diff --git a/src/cli-ux/list.ts b/src/cli-ux/list.ts new file mode 100644 index 000000000..ee9d72e94 --- /dev/null +++ b/src/cli-ux/list.ts @@ -0,0 +1,34 @@ +// tslint:disable + +import maxBy from 'lodash/maxBy' + +import deps from './deps' + +function linewrap(length: number, s: string): string { + const lw = require('@oclif/linewrap') + return lw(length, deps.screen.stdtermwidth, { + skipScheme: 'ansi-color', + })(s).trim() +} + +export type IListItem = [string, string | undefined] +export type IList = IListItem[] +export function renderList(items: IListItem[]): string { + if (items.length === 0) { + return '' + } + + const maxLength = (maxBy(items, '[0].length') as any)[0].length + const lines = items.map(i => { + let left = i[0] + let right = i[1] + if (!right) { + return left + } + + left = left.padEnd(maxLength) + right = linewrap(maxLength + 2, right) + return `${left} ${right}` + }) + return lines.join('\n') +} diff --git a/src/cli-ux/open.ts b/src/cli-ux/open.ts new file mode 100644 index 000000000..9a4de57d5 --- /dev/null +++ b/src/cli-ux/open.ts @@ -0,0 +1,88 @@ +// this code is largely taken from opn +import * as childProcess from 'child_process' +import _ from 'lodash' +const isWsl = require('is-wsl') + +export namespace open { + export type Options = { + // wait: boolean + app?: string | string[]; + } +} + +export default function open(target: string, opts: open.Options = {}) { + // opts = {wait: true, ...opts} + + let cmd + let appArgs: string[] = [] + let args: string[] = [] + const cpOpts: childProcess.SpawnOptions = {} + + if (Array.isArray(opts.app)) { + appArgs = opts.app.slice(1) + opts.app = opts.app[0] + } + + if (process.platform === 'darwin') { + cmd = 'open' + + // if (opts.wait) { + // args.push('-W') + // } + + if (opts.app) { + args.push('-a', opts.app) + } + } else if (process.platform === 'win32' || isWsl) { + cmd = 'cmd' + (isWsl ? '.exe' : '') + args.push('/c', 'start', '""', '/b') + target = target.replace(/&/g, '^&') + + // if (opts.wait) { + // args.push('/wait') + // } + + if (opts.app) { + args.push(opts.app) + } + + if (appArgs.length > 0) { + args = [...args, ...appArgs] + } + } else { + cmd = opts.app ? opts.app : 'xdg-open' + if (appArgs.length > 0) { + args = [...args, ...appArgs] + } + + // if (!opts.wait) { + // `xdg-open` will block the process unless + // stdio is ignored and it's detached from the parent + // even if it's unref'd + cpOpts.stdio = 'ignore' + cpOpts.detached = true + // } + } + + args.push(target) + + if (process.platform === 'darwin' && appArgs.length > 0) { + args.push('--args') + args = [...args, ...appArgs] + } + + const cp = childProcess.spawn(cmd, args, cpOpts) + + return new Promise((resolve, reject) => { + cp.once('error', reject) + + cp.once('close', code => { + if (_.isNumber(code) && code! > 0) { + reject(new Error('Exited with code ' + code)) + return + } + + resolve(cp) + }) + }) +} diff --git a/src/cli-ux/prompt.ts b/src/cli-ux/prompt.ts new file mode 100644 index 000000000..ab4958622 --- /dev/null +++ b/src/cli-ux/prompt.ts @@ -0,0 +1,167 @@ +import * as Errors from '../../src/errors' +import * as chalk from 'chalk' + +import config from './config' +import deps from './deps' + +export interface IPromptOptions { + prompt?: string; + type?: 'normal' | 'mask' | 'hide' | 'single'; + timeout?: number; + /** + * Requires user input if true, otherwise allows empty input + */ + required?: boolean; + default?: string; +} + +interface IPromptConfig { + name: string; + prompt: string; + type: 'normal' | 'mask' | 'hide' | 'single'; + isTTY: boolean; + required: boolean; + default?: string; + timeout?: number; +} + +function normal(options: IPromptConfig, retries = 100): Promise { + if (retries < 0) throw new Error('no input') + return new Promise((resolve, reject) => { + let timer: NodeJS.Timer + if (options.timeout) { + timer = setTimeout(() => { + process.stdin.pause() + reject(new Error('Prompt timeout')) + }, options.timeout) + timer.unref() + } + + process.stdin.setEncoding('utf8') + process.stderr.write(options.prompt) + process.stdin.resume() + process.stdin.once('data', b => { + if (timer) clearTimeout(timer) + process.stdin.pause() + const data: string = (typeof b === 'string' ? b : b.toString()).trim() + if (!options.default && options.required && data === '') { + resolve(normal(options, retries - 1)) + } else { + resolve(data || options.default as string) + } + }) + }) +} + +function getPrompt(name: string, type?: string, defaultValue?: string) { + let prompt = '> ' + + if (defaultValue && type === 'hide') { + defaultValue = '*'.repeat(defaultValue.length) + } + + if (name && defaultValue) prompt = name + ' ' + chalk.yellow('[' + defaultValue + ']') + ': ' + else if (name) prompt = `${name}: ` + + return prompt +} + +async function single(options: IPromptConfig): Promise { + const raw = process.stdin.isRaw + if (process.stdin.setRawMode) process.stdin.setRawMode(true) + options.required = options.required ?? false + const response = await normal(options) + if (process.stdin.setRawMode) process.stdin.setRawMode(Boolean(raw)) + return response +} + +function replacePrompt(prompt: string) { + process.stderr.write(deps.ansiEscapes.cursorHide + deps.ansiEscapes.cursorUp(1) + deps.ansiEscapes.cursorLeft + prompt + + deps.ansiEscapes.cursorDown(1) + deps.ansiEscapes.cursorLeft + deps.ansiEscapes.cursorShow) +} + +function _prompt(name: string, inputOptions: Partial = {}): Promise { + const prompt = getPrompt(name, inputOptions.type, inputOptions.default) + const options: IPromptConfig = { + isTTY: Boolean(process.env.TERM !== 'dumb' && process.stdin.isTTY), + name, + prompt, + type: 'normal', + required: true, + default: '', + ...inputOptions, + } + switch (options.type) { + case 'normal': + return normal(options) + case 'single': + return single(options) + case 'mask': + return deps.passwordPrompt(options.prompt, { + method: options.type, + required: options.required, + default: options.default, + }).then((value: string) => { + replacePrompt(getPrompt(name, 'hide', inputOptions.default)) + return value + }) + case 'hide': + return deps.passwordPrompt(options.prompt, { + method: options.type, + required: options.required, + default: options.default, + }) + default: + throw new Error(`unexpected type ${options.type}`) + } +} + +/** + * prompt for input + * @param name - prompt text + * @param options - @see IPromptOptions + * @returns void + */ +export function prompt(name: string, options: IPromptOptions = {}) { + return config.action.pauseAsync(() => { + return _prompt(name, options) + }, chalk.cyan('?')) +} + +/** + * confirmation prompt (yes/no) + * @param message - confirmation text + * @returns Promise + */ +export function confirm(message: string): Promise { + return config.action.pauseAsync(async () => { + const confirm = async (): Promise => { + const response = (await _prompt(message)).toLowerCase() + if (['n', 'no'].includes(response)) return false + if (['y', 'yes'].includes(response)) return true + return confirm() + } + + return confirm() + }, chalk.cyan('?')) +} + +/** + * "press anykey to continue" + * @param message - optional message to display to user + * @returns Promise + */ +export async function anykey(message?: string): Promise { + const tty = Boolean(process.stdin.setRawMode) + if (!message) { + message = tty ? + `Press any key to continue or ${chalk.yellow('q')} to exit` : + `Press enter to continue or ${chalk.yellow('q')} to exit` + } + + const char = await prompt(message, {type: 'single', required: false}) + if (tty) process.stderr.write('\n') + if (char === 'q') Errors.error('quit') + if (char === '\u0003') Errors.error('ctrl-c') + return char +} diff --git a/src/cli-ux/styled/header.ts b/src/cli-ux/styled/header.ts new file mode 100644 index 000000000..dd3db8385 --- /dev/null +++ b/src/cli-ux/styled/header.ts @@ -0,0 +1,7 @@ +// tslint:disable restrict-plus-operands + +import chalk from 'chalk' + +export default function styledHeader(header: string) { + process.stdout.write(chalk.dim('=== ') + chalk.bold(header) + '\n') +} diff --git a/src/cli-ux/styled/json.ts b/src/cli-ux/styled/json.ts new file mode 100644 index 000000000..4da122da9 --- /dev/null +++ b/src/cli-ux/styled/json.ts @@ -0,0 +1,17 @@ +// tslint:disable restrict-plus-operands + +import chalk from 'chalk' + +import cli from '..' + +export default function styledJSON(obj: any) { + const json = JSON.stringify(obj, null, 2) + if (!chalk.level) { + cli.info(json) + return + } + + const cardinal = require('cardinal') + const theme = require('cardinal/themes/jq') + cli.info(cardinal.highlight(json, {json: true, theme})) +} diff --git a/src/cli-ux/styled/object.ts b/src/cli-ux/styled/object.ts new file mode 100644 index 000000000..87b827141 --- /dev/null +++ b/src/cli-ux/styled/object.ts @@ -0,0 +1,40 @@ +// tslint:disable + +import * as chalk from 'chalk' +import * as util from 'util' + +export default function styledObject(obj: any, keys?: string[]): string { + const output: string[] = [] + const keyLengths = Object.keys(obj).map(key => key.toString().length) + const maxKeyLength = Math.max(...keyLengths) + 2 + function pp(obj: any) { + if (typeof obj === 'string' || typeof obj === 'number') return obj + if (typeof obj === 'object') { + return Object.keys(obj) + .map(k => k + ': ' + util.inspect(obj[k])) + .join(', ') + } + + return util.inspect(obj) + } + + const logKeyValue = (key: string, value: any): string => { + return `${chalk.blue(key)}:` + ' '.repeat(maxKeyLength - key.length - 1) + pp(value) + } + + for (const key of keys || Object.keys(obj).sort()) { + const value = obj[key] + if (Array.isArray(value)) { + if (value.length > 0) { + output.push(logKeyValue(key, value[0])) + for (const e of value.slice(1)) { + output.push(' '.repeat(maxKeyLength) + pp(e)) + } + } + } else if (value !== null && value !== undefined) { + output.push(logKeyValue(key, value)) + } + } + + return output.join('\n') +} diff --git a/src/cli-ux/styled/progress.ts b/src/cli-ux/styled/progress.ts new file mode 100644 index 000000000..4184c81c6 --- /dev/null +++ b/src/cli-ux/styled/progress.ts @@ -0,0 +1,14 @@ +// 3pp +import * as cliProgress from 'cli-progress' + +export default function progress(options?: any): any { + // if no options passed, create empty options + if (!options) { + options = {} + } + + // set noTTYOutput for options + options.noTTYOutput = Boolean(process.env.TERM === 'dumb' || !process.stdin.isTTY) + + return new cliProgress.SingleBar(options) +} diff --git a/src/cli-ux/styled/table.ts b/src/cli-ux/styled/table.ts new file mode 100644 index 000000000..ff71d765e --- /dev/null +++ b/src/cli-ux/styled/table.ts @@ -0,0 +1,384 @@ +import * as Interfaces from '../../../src/interfaces' +import * as F from '../../../src/flags' +import {stdtermwidth} from '@oclif/screen' +import * as chalk from 'chalk' +import {capitalize, sumBy} from 'lodash' +import {safeDump} from 'js-yaml' +import {inspect} from 'util' + +const sw = require('string-width') +const {orderBy} = require('natural-orderby') + +class Table> { + options: table.Options & { printLine(s: any): any } + + columns: (table.Column & { key: string; width?: number; maxWidth?: number })[] + + constructor(private data: T[], columns: table.Columns, options: table.Options = {}) { + // assign columns + this.columns = Object.keys(columns).map((key: string) => { + const col = columns[key] + const extended = col.extended || false + const get = col.get || ((row: any) => row[key]) + const header = typeof col.header === 'string' ? col.header : capitalize(key.replace(/_/g, ' ')) + const minWidth = Math.max(col.minWidth || 0, sw(header) + 1) + + return { + extended, + get, + header, + key, + minWidth, + } + }) + + // assign options + const {columns: cols, filter, csv, output, extended, sort, title, printLine} = options + this.options = { + columns: cols, + output: csv ? 'csv' : output, + extended, + filter, + 'no-header': options['no-header'] || false, + 'no-truncate': options['no-truncate'] || false, + printLine: printLine || ((s: any) => process.stdout.write(s + '\n')), + rowStart: ' ', + sort, + title, + } + } + + display() { + // build table rows from input array data + let rows = this.data.map(d => { + const row: any = {} + for (const col of this.columns) { + let val = col.get(d) + if (typeof val !== 'string') val = inspect(val, {breakLength: Number.POSITIVE_INFINITY}) + row[col.key] = val + } + + return row + }) + + // filter rows + if (this.options.filter) { + /* eslint-disable-next-line prefer-const */ + let [header, regex] = this.options.filter!.split('=') + const isNot = header[0] === '-' + if (isNot) header = header.slice(1) + const col = this.findColumnFromHeader(header) + if (!col || !regex) throw new Error('Filter flag has an invalid value') + rows = rows.filter((d: any) => { + const re = new RegExp(regex) + const val = d[col!.key] + const match = val.match(re) + return isNot ? !match : match + }) + } + + // sort rows + if (this.options.sort) { + const sorters = this.options.sort!.split(',') + const sortHeaders = sorters.map(k => k[0] === '-' ? k.slice(1) : k) + const sortKeys = this.filterColumnsFromHeaders(sortHeaders).map(c => { + return ((v: any) => v[c.key]) + }) + const sortKeysOrder = sorters.map(k => k[0] === '-' ? 'desc' : 'asc') + rows = orderBy(rows, sortKeys, sortKeysOrder) + } + + // and filter columns + if (this.options.columns) { + const filters = this.options.columns!.split(',') + this.columns = this.filterColumnsFromHeaders(filters) + } else if (!this.options.extended) { + // show extented columns/properties + this.columns = this.columns.filter(c => !c.extended) + } + + this.data = rows + + switch (this.options.output) { + case 'csv': + this.outputCSV() + break + case 'json': + this.outputJSON() + break + case 'yaml': + this.outputYAML() + break + default: + this.outputTable() + } + } + + private findColumnFromHeader(header: string): (table.Column & { key: string; width?: number; maxWidth?: number }) | undefined { + return this.columns.find(c => c.header.toLowerCase() === header.toLowerCase()) + } + + private filterColumnsFromHeaders(filters: string[]): (table.Column & { key: string; width?: number; maxWidth?: number })[] { + // unique + filters = [...(new Set(filters))] + const cols: (table.Column & {key: string; width?: number; maxWidth?: number})[] = [] + for (const f of filters) { + const c = this.columns.find(c => c.header.toLowerCase() === f.toLowerCase()) + if (c) cols.push(c) + } + + return cols + } + + private getCSVRow(d: any): string[] { + const values = this.columns.map(col => d[col.key] || '') + const lineToBeEscaped = values.find((e: string) => e.includes('"') || e.includes('\n') || e.includes('\r\n') || e.includes('\r') || e.includes(',')) + return values.map(e => lineToBeEscaped ? `"${e.replace('"', '""')}"` : e) + } + + private resolveColumnsToObjectArray() { + // tslint:disable-next-line:no-this-assignment + const {data, columns} = this + return data.map((d: any) => { + // eslint-disable-next-line unicorn/prefer-object-from-entries + return columns.reduce((obj, col) => { + return { + ...obj, + [col.key]: d[col.key] || '', + } + }, {}) + }) + } + + private outputJSON() { + this.options.printLine(JSON.stringify(this.resolveColumnsToObjectArray(), undefined, 2)) + } + + private outputYAML() { + this.options.printLine(safeDump(this.resolveColumnsToObjectArray())) + } + + private outputCSV() { + // tslint:disable-next-line:no-this-assignment + const {data, columns, options} = this + + if (!options['no-header']) { + options.printLine(columns.map(c => c.header).join(',')) + } + + for (const d of data) { + const row = this.getCSVRow(d) + options.printLine(row.join(',')) + } + } + + private outputTable() { + // tslint:disable-next-line:no-this-assignment + const {data, columns, options} = this + + // column truncation + // + // find max width for each column + for (const col of columns) { + // convert multi-line cell to single longest line + // for width calculations + const widthData = data.map((row: any) => { + const d = row[col.key] + const manyLines = d.split('\n') + if (manyLines.length > 1) { + return '*'.repeat(Math.max(...manyLines.map((r: string) => sw(r)))) + } + + return d + }) + const widths = ['.'.padEnd(col.minWidth! - 1), col.header, ...widthData.map((row: any) => row)].map(r => sw(r)) + col.maxWidth = Math.max(...widths) + 1 + col.width = col.maxWidth! + } + + // terminal width + const maxWidth = stdtermwidth - 2 + // truncation logic + const shouldShorten = () => { + // don't shorten if full mode + if (options['no-truncate'] || (!process.stdout.isTTY && !process.env.CLI_UX_SKIP_TTY_CHECK)) return + + // don't shorten if there is enough screen width + const dataMaxWidth = sumBy(columns, c => c.width!) + const overWidth = dataMaxWidth - maxWidth + if (overWidth <= 0) return + + // not enough room, short all columns to minWidth + for (const col of columns) { + col.width = col.minWidth + } + + // if sum(minWidth's) is greater than term width + // nothing can be done so + // display all as minWidth + const dataMinWidth = sumBy(columns, c => c.minWidth!) + if (dataMinWidth >= maxWidth) return + + // some wiggle room left, add it back to "needy" columns + let wiggleRoom = maxWidth - dataMinWidth + const needyCols = columns.map(c => ({key: c.key, needs: c.maxWidth! - c.width!})).sort((a, b) => a.needs - b.needs) + for (const {key, needs} of needyCols) { + if (!needs) continue + const col = columns.find(c => key === c.key) + if (!col) continue + if (wiggleRoom > needs) { + col.width = col.width! + needs + wiggleRoom -= needs + } else if (wiggleRoom) { + col.width = col.width! + wiggleRoom + wiggleRoom = 0 + } + } + } + + shouldShorten() + + // print table title + if (options.title) { + options.printLine(options.title) + // print title divider + options.printLine(''.padEnd(columns.reduce((sum, col) => sum + col.width!, 1), '=')) + + options.rowStart = '| ' + } + + // print headers + if (!options['no-header']) { + let headers = options.rowStart + for (const col of columns) { + const header = col.header! + headers += header.padEnd(col.width!) + } + + options.printLine(chalk.bold(headers)) + + // print header dividers + let dividers = options.rowStart + for (const col of columns) { + const divider = ''.padEnd(col.width! - 1, '─') + ' ' + dividers += divider.padEnd(col.width!) + } + + options.printLine(chalk.bold(dividers)) + } + + // print rows + for (const row of data) { + // find max number of lines + // for all cells in a row + // with multi-line strings + let numOfLines = 1 + for (const col of columns) { + const d = (row as any)[col.key] + const lines = d.split('\n').length + if (lines > numOfLines) numOfLines = lines + } + + // eslint-disable-next-line unicorn/no-new-array + const linesIndexess = [...new Array(numOfLines).keys()] + + // print row + // including multi-lines + for (const i of linesIndexess) { + let l = options.rowStart + for (const col of columns) { + const width = col.width! + let d = (row as any)[col.key] + d = d.split('\n')[i] || '' + const visualWidth = sw(d) + const colorWidth = (d.length - visualWidth) + let cell = d.padEnd(width + colorWidth) + if ((cell.length - colorWidth) > width || visualWidth === width) { + cell = cell.slice(0, width - 2) + '… ' + } + + l += cell + } + + options.printLine(l) + } + } + } +} + +export function table>(data: T[], columns: table.Columns, options: table.Options = {}) { + new Table(data, columns, options).display() +} + +export namespace table { + export const Flags: { + columns: Interfaces.OptionFlag; + sort: Interfaces.OptionFlag; + filter: Interfaces.OptionFlag; + csv: Interfaces.Flag; + output: Interfaces.OptionFlag; + extended: Interfaces.Flag; + 'no-truncate': Interfaces.Flag; + 'no-header': Interfaces.Flag; + } = { + columns: F.string({exclusive: ['extended'], description: 'only show provided columns (comma-separated)'}), + sort: F.string({description: 'property to sort by (prepend \'-\' for descending)'}), + filter: F.string({description: 'filter property by partial string matching, ex: name=foo'}), + csv: F.boolean({exclusive: ['no-truncate'], description: 'output is csv format [alias: --output=csv]'}), + output: F.string({ + exclusive: ['no-truncate', 'csv'], + description: 'output in a more machine friendly format', + options: ['csv', 'json', 'yaml'], + }), + extended: F.boolean({exclusive: ['columns'], char: 'x', description: 'show extra columns'}), + 'no-truncate': F.boolean({exclusive: ['csv'], description: 'do not truncate output to fit screen'}), + 'no-header': F.boolean({exclusive: ['csv'], description: 'hide table header from output'}), + } + + type IFlags = typeof Flags + type ExcludeFlags = Pick> + type IncludeFlags = Pick + + export function flags(): IFlags + export function flags(opts: { except: Z | Z[] }): ExcludeFlags + export function flags(opts: { only: K | K[] }): IncludeFlags + export function flags(opts?: any): any { + if (opts) { + const f = {} + const o = (opts.only && typeof opts.only === 'string' ? [opts.only] : opts.only) || Object.keys(Flags) + const e = (opts.except && typeof opts.except === 'string' ? [opts.except] : opts.except) || [] + for (const key of o) { + if (!(e as any[]).includes(key)) { + (f as any)[key] = (Flags as any)[key] + } + } + + return f + } + + return Flags + } + + export interface Column> { + header: string; + extended: boolean; + minWidth: number; + get(row: T): any; + } + + export type Columns> = { [key: string]: Partial> } + + // export type OutputType = 'csv' | 'json' | 'yaml' + + export interface Options { + [key: string]: any; + sort?: string; + filter?: string; + columns?: string; + extended?: boolean; + 'no-truncate'?: boolean; + output?: string; + 'no-header'?: boolean; + printLine?(s: any): any; + } +} diff --git a/src/cli-ux/styled/tree.ts b/src/cli-ux/styled/tree.ts new file mode 100644 index 000000000..d14e98494 --- /dev/null +++ b/src/cli-ux/styled/tree.ts @@ -0,0 +1,40 @@ +const treeify = require('object-treeify') + +export class Tree { + nodes: { [key: string]: Tree } = {} + + insert(child: string, value: Tree = new Tree()): Tree { + this.nodes[child] = value + return this + } + + search(key: string): Tree | undefined { + for (const child of Object.keys(this.nodes)) { + if (child === key) { + return this.nodes[child] + } + + const c = this.nodes[child].search(key) + if (c) return c + } + } + + // tslint:disable-next-line:no-console + display(logger: any = console.log) { + const addNodes = function (nodes: any) { + const tree: { [key: string]: any } = {} + for (const p of Object.keys(nodes)) { + tree[p] = addNodes(nodes[p].nodes) + } + + return tree + } + + const tree = addNodes(this.nodes) + logger(treeify(tree)) + } +} + +export default function tree() { + return new Tree() +} diff --git a/src/cli-ux/wait.ts b/src/cli-ux/wait.ts new file mode 100644 index 000000000..769321a42 --- /dev/null +++ b/src/cli-ux/wait.ts @@ -0,0 +1,6 @@ +// tslint:disable no-string-based-set-timeout +export default (ms = 1000) => { + return new Promise(resolve => { + setTimeout(resolve, ms) + }) +} diff --git a/src/command.ts b/src/command.ts index 42b1296d9..10c17bcd3 100644 --- a/src/command.ts +++ b/src/command.ts @@ -1,7 +1,7 @@ import {fileURLToPath} from 'url' import {format, inspect} from 'util' -import {cli} from 'cli-ux' +import {cli} from './cli-ux' import {Config} from './config' import * as Interfaces from './interfaces' import * as Errors from './errors' @@ -231,7 +231,6 @@ export default abstract class Command { } else { if (!err.message) throw err try { - const {cli} = require('cli-ux') const chalk = require('chalk') cli.action.stop(chalk.bold.red('!')) } catch {} diff --git a/src/index.ts b/src/index.ts index fc6012890..8ce78e447 100644 --- a/src/index.ts +++ b/src/index.ts @@ -42,6 +42,8 @@ export { flush, } +export * from './cli-ux/index' + function checkCWD() { try { process.cwd() diff --git a/test/cli-ux/fancy.ts b/test/cli-ux/fancy.ts new file mode 100644 index 000000000..ea7e0610a --- /dev/null +++ b/test/cli-ux/fancy.ts @@ -0,0 +1,24 @@ +import {expect, fancy as base, FancyTypes} from 'fancy-test' +import * as fs from 'fs-extra' +import * as path from 'path' + +import cli from '../../src/cli-ux' + +export { + expect, + FancyTypes, +} + +let count = 0 + +export const fancy = base +.do(async (ctx: {count: number; base: string}) => { + ctx.count = count++ + ctx.base = path.join(__dirname, '../tmp', `test-${ctx.count}`) + await fs.remove(ctx.base) + const chalk = require('chalk') + chalk.level = 0 +}) +.finally(async () => { + await cli.done() +}) diff --git a/test/cli-ux/helpers/init.js b/test/cli-ux/helpers/init.js new file mode 100644 index 000000000..cd57a3d48 --- /dev/null +++ b/test/cli-ux/helpers/init.js @@ -0,0 +1,3 @@ +const path = require('path') +process.env.TS_NODE_PROJECT = path.resolve('test/tsconfig.json') +global.columns = '80' diff --git a/test/cli-ux/index.test.ts b/test/cli-ux/index.test.ts new file mode 100644 index 000000000..46791c200 --- /dev/null +++ b/test/cli-ux/index.test.ts @@ -0,0 +1,24 @@ +import ux from '../../src/cli-ux' + +import {expect, fancy} from './fancy' +const hyperlinker = require('hyperlinker') + +describe('url', () => { + fancy + .env({FORCE_HYPERLINK: '1'}, {clear: true}) + .stdout() + .do(() => ux.url('sometext', 'https://google.com')) + .it('renders hyperlink', async ({stdout}) => { + expect(stdout).to.equal('sometext\n') + }) +}) + +describe('hyperlinker', () => { + fancy + .it('renders hyperlink', async () => { + const link = hyperlinker('sometext', 'https://google.com', {}) + // eslint-disable-next-line unicorn/escape-case + const expected = '\u001b]8;;https://google.com\u0007sometext\u001b]8;;\u0007' + expect(link).to.equal(expected) + }) +}) diff --git a/test/cli-ux/prompt.test.ts b/test/cli-ux/prompt.test.ts new file mode 100644 index 000000000..c6fa8c013 --- /dev/null +++ b/test/cli-ux/prompt.test.ts @@ -0,0 +1,75 @@ +import * as chai from 'chai' + +const expect = chai.expect + +import cli from '../../src/cli-ux' + +import {fancy} from './fancy' + +describe('prompt', () => { + fancy + .stdout() + .stderr() + .end('requires input', async () => { + const promptPromise = cli.prompt('Require input?') + process.stdin.emit('data', '') + process.stdin.emit('data', 'answer') + const answer = await promptPromise + await cli.done() + expect(answer).to.equal('answer') + }) + + fancy + .stdout() + .stderr() + .stdin('y') + .end('confirm', async () => { + const promptPromise = cli.confirm('yes/no?') + const answer = await promptPromise + await cli.done() + expect(answer).to.equal(true) + }) + + fancy + .stdout() + .stderr() + .stdin('n') + .end('confirm', async () => { + const promptPromise = cli.confirm('yes/no?') + const answer = await promptPromise + await cli.done() + expect(answer).to.equal(false) + }) + + fancy + .stdout() + .stderr() + .stdin('x') + .end('gets anykey', async () => { + const promptPromise = cli.anykey() + const answer = await promptPromise + await cli.done() + expect(answer).to.equal('x') + }) + + fancy + .stdout() + .stderr() + .end('does not require input', async () => { + const promptPromise = cli.prompt('Require input?', { + required: false, + }) + process.stdin.emit('data', '') + const answer = await promptPromise + await cli.done() + expect(answer).to.equal('') + }) + + fancy + .stdout() + .stderr() + .it('timeouts with no input', async () => { + await expect(cli.prompt('Require input?', {timeout: 1})) + .to.eventually.be.rejectedWith('Prompt timeout') + }) +}) diff --git a/test/cli-ux/styled/object.test.ts b/test/cli-ux/styled/object.test.ts new file mode 100644 index 000000000..87718294e --- /dev/null +++ b/test/cli-ux/styled/object.test.ts @@ -0,0 +1,19 @@ +import {expect, fancy} from 'fancy-test' + +import cli from '../../../src/cli-ux' + +describe('styled/object', () => { + fancy + .stdout() + .end('shows a table', output => { + cli.styledObject([ + {foo: 1, bar: 1}, + {foo: 2, bar: 2}, + {foo: 3, bar: 3}, + ]) + expect(output.stdout).to.equal(`0: foo: 1, bar: 1 +1: foo: 2, bar: 2 +2: foo: 3, bar: 3 +`) + }) +}) diff --git a/test/cli-ux/styled/progress.test.ts b/test/cli-ux/styled/progress.test.ts new file mode 100644 index 000000000..75bb6071c --- /dev/null +++ b/test/cli-ux/styled/progress.test.ts @@ -0,0 +1,29 @@ +import {expect, fancy} from 'fancy-test' + +// import {BarType, Progress} from '../../src/progress' +import cli from '../../../src/cli-ux' + +describe('progress', () => { + // single bar + fancy + .end('single bar has default settings', _ => { + const b1 = cli.progress({format: 'Example 1: Progress {bar} | {percentage}%'}) + expect(b1.options.format).to.contain('Example 1: Progress') + expect(b1.bars).to.not.have + }) + + // testing no settings passed, default settings created + fancy + .end('single bar, no bars array', _ => { + const b1 = cli.progress({}) + expect(b1.options.format).to.contain('progress') + expect(b1.bars).to.not.have + expect(b1.options.noTTYOutput).to.not.be.null + }) + // testing getProgressBar returns correct type + fancy + .end('typeof progress bar is object', _ => { + const b1 = cli.progress({format: 'Example 1: Progress {bar} | {percentage}%'}) + expect(typeof (b1)).to.equal('object') + }) +}) diff --git a/test/cli-ux/styled/table.test.ts b/test/cli-ux/styled/table.test.ts new file mode 100644 index 000000000..cd4f7ca90 --- /dev/null +++ b/test/cli-ux/styled/table.test.ts @@ -0,0 +1,321 @@ +import {expect, fancy} from 'fancy-test' + +import cli from '../../../src/cli-ux' + +/* eslint-disable camelcase */ +const apps = [ + { + build_stack: { + id: '123', + name: 'heroku-16', + }, + created_at: '2000-01-01T22:34:46Z', + id: '123', + git_url: 'https://git.heroku.com/supertable-test-1.git', + name: 'supertable-test-1', + owner: { + email: 'example@heroku.com', + id: '1', + }, + region: {id: '123', name: 'us'}, + released_at: '2000-01-01T22:34:46Z', + stack: { + id: '123', + name: 'heroku-16', + }, + updated_at: '2000-01-01T22:34:46Z', + web_url: 'https://supertable-test-1.herokuapp.com/', + }, + { + build_stack: { + id: '321', + name: 'heroku-16', + }, + created_at: '2000-01-01T22:34:46Z', + id: '321', + git_url: 'https://git.heroku.com/phishing-demo.git', + name: 'supertable-test-2', + owner: { + email: 'example@heroku.com', + id: '1', + }, + region: {id: '321', name: 'us'}, + released_at: '2000-01-01T22:34:46Z', + stack: { + id: '321', + name: 'heroku-16', + }, + updated_at: '2000-01-01T22:34:46Z', + web_url: 'https://supertable-test-2.herokuapp.com/', + }, +] + +const columns = { + id: {header: 'ID'}, + name: {}, + web_url: {extended: true}, + stack: {extended: true, get: (r: any) => r.stack && r.stack.name}, +} +/* eslint-enable camelcase */ + +const ws = ' ' + +// ignore me +// stored up here for line wrapping reasons +const extendedHeader = `ID Name${ws.padEnd(14)}Web url${ws.padEnd(34)}Stack${ws.padEnd(5)}` + +// tests to-do: +// no-truncate +// truncation rules? + +describe('styled/table', () => { + fancy + .end('export flags and display()', () => { + expect(typeof (cli.table.flags())).to.eq('object') + expect(typeof (cli.table)).to.eq('function') + }) + + fancy + .end('has optional flags', _ => { + const flags = cli.table.flags() + expect(flags.columns).to.exist + expect(flags.sort).to.exist + expect(flags.filter).to.exist + expect(flags.csv).to.exist + expect(flags.output).to.exist + expect(flags.extended).to.exist + expect(flags['no-truncate']).to.exist + expect(flags['no-header']).to.exist + }) + + fancy + .stdout() + .end('displays table', output => { + cli.table(apps, columns) + expect(output.stdout).to.equal(` ID Name${ws.padEnd(14)} + ─── ─────────────────${ws} + 123 supertable-test-1${ws} + 321 supertable-test-2${ws}\n`) + }) + + describe('columns', () => { + fancy + .stdout() + .end('use header value for id', output => { + cli.table(apps, columns) + expect(output.stdout.slice(1, 3)).to.equal('ID') + }) + + fancy + .stdout() + .end('shows extended columns/uses get() for value', output => { + cli.table(apps, columns, {extended: true}) + expect(output.stdout).to.equal(`${ws}${extendedHeader} + ─── ───────────────── ──────────────────────────────────────── ─────────${ws} + 123 supertable-test-1 https://supertable-test-1.herokuapp.com/ heroku-16${ws} + 321 supertable-test-2 https://supertable-test-2.herokuapp.com/ heroku-16${ws}\n`) + }) + }) + + describe('options', () => { + fancy + .stdout() + .end('shows extended columns', output => { + cli.table(apps, columns, {extended: true}) + expect(output.stdout).to.contain(extendedHeader) + }) + + fancy + .stdout() + .end('shows title with divider', output => { + cli.table(apps, columns, {title: 'testing'}) + expect(output.stdout).to.equal(`testing +======================= +| ID Name${ws.padEnd(14)} +| ─── ─────────────────${ws} +| 123 supertable-test-1${ws} +| 321 supertable-test-2${ws}\n`) + }) + + fancy + .stdout() + .end('skips header', output => { + cli.table(apps, columns, {'no-header': true}) + expect(output.stdout).to.equal(` 123 supertable-test-1${ws} + 321 supertable-test-2${ws}\n`) + }) + + fancy + .stdout() + .end('only displays given columns', output => { + cli.table(apps, columns, {columns: 'id'}) + expect(output.stdout).to.equal(` ID${ws}${ws} + ───${ws} + 123${ws} + 321${ws}\n`) + }) + + fancy + .stdout() + .end('outputs in csv', output => { + cli.table(apps, columns, {output: 'csv'}) + expect(output.stdout).to.equal(`ID,Name +123,supertable-test-1 +321,supertable-test-2\n`) + }) + + fancy + .stdout() + .end('outputs in csv with escaped values', output => { + cli.table([ + { + id: '123\n2', + name: 'supertable-test-1', + }, + { + id: '12"3', + name: 'supertable-test-2', + }, + { + id: '123', + name: 'supertable-test-3,comma', + }, + { + id: '123', + name: 'supertable-test-4', + }, + ], columns, {output: 'csv'}) + expect(output.stdout).to.equal(`ID,Name +"123\n2","supertable-test-1" +"12""3","supertable-test-2" +"123","supertable-test-3,comma" +123,supertable-test-4\n`) + }) + + fancy + .stdout() + .end('outputs in csv without headers', output => { + cli.table(apps, columns, {output: 'csv', 'no-header': true}) + expect(output.stdout).to.equal(`123,supertable-test-1 +321,supertable-test-2\n`) + }) + + fancy + .stdout() + .end('outputs in csv with alias flag', output => { + cli.table(apps, columns, {csv: true}) + expect(output.stdout).to.equal(`ID,Name +123,supertable-test-1 +321,supertable-test-2\n`) + }) + + fancy + .stdout() + .end('outputs in json', output => { + cli.table(apps, columns, {output: 'json'}) + expect(output.stdout).to.equal(`[ + { + "id": "123", + "name": "supertable-test-1" + }, + { + "id": "321", + "name": "supertable-test-2" + } +] +`) + }) + + fancy + .stdout() + .end('outputs in yaml', output => { + cli.table(apps, columns, {output: 'yaml'}) + expect(output.stdout).to.equal(`- id: '123' + name: supertable-test-1 +- id: '321' + name: supertable-test-2 + +`) + }) + + fancy + .stdout() + .end('sorts by property', output => { + cli.table(apps, columns, {sort: '-name'}) + expect(output.stdout).to.equal(` ID Name${ws.padEnd(14)} + ─── ─────────────────${ws} + 321 supertable-test-2${ws} + 123 supertable-test-1${ws}\n`) + }) + + fancy + .stdout() + .end('filters by property & value (partial string match)', output => { + cli.table(apps, columns, {filter: 'id=123'}) + expect(output.stdout).to.equal(` ID Name${ws.padEnd(14)} + ─── ─────────────────${ws} + 123 supertable-test-1${ws}\n`) + }) + + fancy + .stdout() + .end('does not truncate', output => { + const three = {...apps[0], id: '0'.repeat(80), name: 'supertable-test-3'} + cli.table([...apps, three], columns, {filter: 'id=0', 'no-truncate': true}) + expect(output.stdout).to.equal(` ID${ws.padEnd(78)} Name${ws.padEnd(14)} + ${''.padEnd(three.id.length, '─')} ─────────────────${ws} + ${three.id} supertable-test-3${ws}\n`) + }) + }) + + describe('#flags', () => { + fancy + .end('includes only flags', _ => { + const flags = cli.table.flags({only: 'columns'}) + expect(flags.columns).to.be.a('object') + expect((flags as any).sort).to.be.undefined + }) + + fancy + .end('excludes except flags', _ => { + const flags = cli.table.flags({except: 'columns'}) + expect((flags as any).columns).to.be.undefined + expect(flags.sort).to.be.a('object') + }) + }) + + describe('edge cases', () => { + fancy + .stdout() + .end('ignores header case', output => { + cli.table(apps, columns, {columns: 'iD,Name', filter: 'nAMe=supertable-test', sort: '-ID'}) + expect(output.stdout).to.equal(` ID Name${ws.padEnd(14)} + ─── ─────────────────${ws} + 321 supertable-test-2${ws} + 123 supertable-test-1${ws}\n`) + }) + + fancy + .stdout() + .end('displays multiline cell', output => { + /* eslint-disable camelcase */ + const app3 = { + build_stack: { + name: 'heroku-16', + }, + id: '456', + name: 'supertable-test\n3', + web_url: 'https://supertable-test-1.herokuapp.com/', + } + /* eslint-enable camelcase */ + + cli.table([...apps, app3 as any], columns, {sort: '-ID'}) + expect(output.stdout).to.equal(` ID Name${ws.padEnd(14)} + ─── ─────────────────${ws} + 456 supertable-test${ws.padEnd(3)} + 3${ws.padEnd(17)} + 321 supertable-test-2${ws} + 123 supertable-test-1${ws}\n`) + }) + }) +}) diff --git a/test/cli-ux/styled/tree.test.ts b/test/cli-ux/styled/tree.test.ts new file mode 100644 index 000000000..be4ddae26 --- /dev/null +++ b/test/cli-ux/styled/tree.test.ts @@ -0,0 +1,24 @@ +import {expect, fancy} from 'fancy-test' + +import cli from '../../../src/cli-ux' + +describe('styled/tree', () => { + fancy + .stdout() + .end('shows the tree', output => { + const tree = cli.tree() + tree.insert('foo') + tree.insert('bar') + + const subtree = cli.tree() + subtree.insert('qux') + tree.nodes.bar.insert('baz', subtree) + + tree.display() + expect(output.stdout).to.equal(`├─ foo +└─ bar + └─ baz + └─ qux +`) + }) +}) diff --git a/test/helpers/init.js b/test/helpers/init.js index dce826a39..1b68eb65c 100644 --- a/test/helpers/init.js +++ b/test/helpers/init.js @@ -9,3 +9,4 @@ chai.use(chaiAsPromised) global.oclif = global.oclif || {} global.oclif.columns = 80 +global.columns = '80' diff --git a/tsconfig.json b/tsconfig.json index 24c61a738..4e44a525b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,7 +10,8 @@ ], "strict": true, "target": "es2019", - "lib": ["es2019"] + "lib": ["es2019"], + "allowSyntheticDefaultImports": true }, "include": [ "./src/**/*" diff --git a/yarn.lock b/yarn.lock index 4bcd24df1..3887df093 100644 --- a/yarn.lock +++ b/yarn.lock @@ -444,29 +444,6 @@ is-wsl "^2.1.1" tslib "^2.0.0" -"@oclif/core@1.0.10": - version "1.0.10" - resolved "https://registry.npmjs.org/@oclif/core/-/core-1.0.10.tgz#5fd01d572e44d372b7279ee0f49b4860e14b6e4e" - integrity sha512-L+IcNU3NoYxwz5hmHfcUlOJ3dpgHRsIj1kAmI9CKEJHq5gBVKlP44Ot179Jke1jKRKX2g9N42izbmlh0SNpkkw== - dependencies: - "@oclif/linewrap" "^1.0.0" - chalk "^4.1.2" - clean-stack "^3.0.1" - cli-ux "6.0.5" - debug "^4.3.3" - fs-extra "^9.1.0" - get-package-type "^0.1.0" - globby "^11.0.4" - indent-string "^4.0.0" - is-wsl "^2.2.0" - lodash "^4.17.21" - semver "^7.3.5" - string-width "^4.2.3" - strip-ansi "^6.0.1" - tslib "^2.3.1" - widest-line "^3.1.0" - wrap-ansi "^7.0.0" - "@oclif/core@^1.0.4": version "1.0.4" resolved "https://registry.yarnpkg.com/@oclif/core/-/core-1.0.4.tgz#79ba3ed554441c3c08de38c3109275f3d9a1c188" @@ -536,29 +513,6 @@ widest-line "^3.1.0" wrap-ansi "^7.0.0" -"@oclif/core@^1.0.8": - version "1.0.9" - resolved "https://registry.yarnpkg.com/@oclif/core/-/core-1.0.9.tgz#5b9db438f5f1f458f9b54114983d73c1f52c9ef0" - integrity sha512-1YQLYWlMdpxHE2W70WrUSgBkWyEsyI8SNST4FAq7kJ+wQCym/XSQeZ1TF+jInHl9aq6DZkG0MAA19hXn4sIIqw== - dependencies: - "@oclif/linewrap" "^1.0.0" - chalk "^4.1.2" - clean-stack "^3.0.1" - cli-ux "^6.0.4" - debug "^4.3.3" - fs-extra "^9.1.0" - get-package-type "^0.1.0" - globby "^11.0.4" - indent-string "^4.0.0" - is-wsl "^2.2.0" - lodash "^4.17.21" - semver "^7.3.5" - string-width "^4.2.3" - strip-ansi "^6.0.1" - tslib "^2.3.1" - widest-line "^3.1.0" - wrap-ansi "^7.0.0" - "@oclif/errors@^1.2.1", "@oclif/errors@^1.2.2", "@oclif/errors@^1.3.3", "@oclif/errors@^1.3.5": version "1.3.5" resolved "https://registry.yarnpkg.com/@oclif/errors/-/errors-1.3.5.tgz#a1e9694dbeccab10fe2fe15acb7113991bed636c" @@ -626,11 +580,16 @@ tslib "^2.0.0" yarn "^1.21.1" -"@oclif/screen@^1.0.3", "@oclif/screen@^1.0.4 ": +"@oclif/screen@^1.0.3": version "1.0.4" resolved "https://registry.yarnpkg.com/@oclif/screen/-/screen-1.0.4.tgz#b740f68609dfae8aa71c3a6cab15d816407ba493" integrity sha512-60CHpq+eqnTxLZQ4PGHYNwUX572hgpMHGPtTWMjdTMsAvlm69lZV/4ly6O3sAYkomo4NggGcomrDpBe34rxUqw== +"@oclif/screen@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@oclif/screen/-/screen-3.0.2.tgz#969054308fe98d130c02844a45cc792199b75670" + integrity sha512-S/SF/XYJeevwIgHFmVDAFRUvM3m+OjhvCAYMk78ZJQCYCQ5wS7j+LTt1ZEv2jpEEGg2tx/F6TYYWxddNAYHrFQ== + "@oclif/test@^1.2.8": version "1.2.8" resolved "https://registry.yarnpkg.com/@oclif/test/-/test-1.2.8.tgz#a5b2ebd747832217d9af65ac30b58780c4c17c5e" @@ -666,6 +625,13 @@ resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz#8da5c6530915653f3a1f38fd5f101d8c3f8079c5" integrity sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ== +"@types/ansi-styles@^3.2.1": + version "3.2.1" + resolved "https://registry.yarnpkg.com/@types/ansi-styles/-/ansi-styles-3.2.1.tgz#49e996bb6e0b7957ca831205df31eb9a0702492c" + integrity sha512-UFa7mfKgSutXdT+elzJo8Ulr7FHgLNAyglVIOZYXFNJVQERm8DPrcwPret5BYk66LBE7fwm1XoVGi76MJkQ6ow== + dependencies: + "@types/color-name" "*" + "@types/chai-as-promised@^7.1.4": version "7.1.4" resolved "https://registry.yarnpkg.com/@types/chai-as-promised/-/chai-as-promised-7.1.4.tgz#caf64e76fb056b8c8ced4b761ed499272b737601" @@ -690,6 +656,18 @@ dependencies: clean-stack "*" +"@types/cli-progress@^3.9.2": + version "3.9.2" + resolved "https://registry.yarnpkg.com/@types/cli-progress/-/cli-progress-3.9.2.tgz#6ca355f96268af39bee9f9307f0ac96145639c26" + integrity sha512-VO5/X5Ij+oVgEVjg5u0IXVe3JQSKJX+Ev8C5x+0hPy0AuWyW+bF8tbajR7cPFnDGhs7pidztcac+ccrDtk5teA== + dependencies: + "@types/node" "*" + +"@types/color-name@*": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" + integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== + "@types/ejs@^3.1.0": version "3.1.0" resolved "https://registry.yarnpkg.com/@types/ejs/-/ejs-3.1.0.tgz#ab8109208106b5e764e5a6c92b2ba1c625b73020" @@ -717,6 +695,11 @@ dependencies: indent-string "*" +"@types/js-yaml@^3.12.1": + version "3.12.7" + resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-3.12.7.tgz#330c5d97a3500e9c903210d6e49f02964af04a0e" + integrity sha512-S6+8JAYTE1qdsc9HMVsfY7+SgSuUU/Tp6TYTmITW0PZxiyIMvol3Gy//y69Wkhs0ti4py5qgR3uZH6uz/DNzJQ== + "@types/json-schema@^7.0.7": version "7.0.9" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" @@ -727,6 +710,16 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.172.tgz#aad774c28e7bfd7a67de25408e03ee5a8c3d028a" integrity sha512-/BHF5HAx3em7/KkzVKm3LrsD6HZAXuXO1AJZQ3cRRBZj4oHZDviWPYu0aEplAqDFNHZPW6d3G7KN+ONcCCC7pw== +"@types/lodash@^4.14.117": + version "4.14.178" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.178.tgz#341f6d2247db528d4a13ddbb374bcdc80406f4f8" + integrity sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw== + +"@types/minimatch@*": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40" + integrity sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ== + "@types/minimist@^1.2.0": version "1.2.2" resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c" @@ -803,6 +796,11 @@ dependencies: strip-ansi "*" +"@types/supports-color@^8.1.1": + version "8.1.1" + resolved "https://registry.yarnpkg.com/@types/supports-color/-/supports-color-8.1.1.tgz#1b44b1b096479273adf7f93c75fc4ecc40a61ee4" + integrity sha512-dPWnWsf+kzIG140B8z2w3fr5D03TLWbOAFQl45xUpI3vcizeXriNR5VYkWZ+WTMsUHqZ9Xlt3hrxGNANFyNQfw== + "@types/wrap-ansi@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz#18b97a972f94f60a679fd5c796d96421b9abb9fd" @@ -1221,6 +1219,13 @@ clean-stack@^3.0.0, clean-stack@^3.0.1: dependencies: escape-string-regexp "4.0.0" +cli-progress@^3.10.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/cli-progress/-/cli-progress-3.10.0.tgz#63fd9d6343c598c93542fdfa3563a8b59887d78a" + integrity sha512-kLORQrhYCAtUPLZxqsAt2YJGOvRdt34+O6jl5cQGb7iF3dM55FQZlTR+rQyIK9JUcO9bBMwZsTlND+3dmFU2Cw== + dependencies: + string-width "^4.2.0" + cli-progress@^3.4.0: version "3.9.0" resolved "https://registry.yarnpkg.com/cli-progress/-/cli-progress-3.9.0.tgz#25db83447deb812e62d05bac1af9aec5387ef3d4" @@ -1237,37 +1242,6 @@ cli-progress@^3.9.1: colors "^1.1.2" string-width "^4.2.0" -cli-ux@6.0.5: - version "6.0.5" - resolved "https://registry.yarnpkg.com/cli-ux/-/cli-ux-6.0.5.tgz#5461dffb6c29e4a727e071f8b74bbebcc6b7be08" - integrity sha512-q2pvzDiXMNISMqCBh0P2dkofQ/8OiWlEAjl6MDNk5oUZ6p54Fnk1rOaXxohYm+YkLX5YNUonGOrwkvuiwVreIg== - dependencies: - "@oclif/core" "^1.0.8" - "@oclif/linewrap" "^1.0.0" - "@oclif/screen" "^1.0.4 " - ansi-escapes "^4.3.0" - ansi-styles "^4.2.0" - cardinal "^2.1.1" - chalk "^4.1.0" - clean-stack "^3.0.0" - cli-progress "^3.9.1" - extract-stack "^2.0.0" - fs-extra "^8.1" - hyperlinker "^1.0.0" - indent-string "^4.0.0" - is-wsl "^2.2.0" - js-yaml "^3.13.1" - lodash "^4.17.21" - natural-orderby "^2.0.1" - object-treeify "^1.1.4" - password-prompt "^1.1.2" - semver "^7.3.2" - string-width "^4.2.0" - strip-ansi "^6.0.0" - supports-color "^8.1.0" - supports-hyperlinks "^2.1.0" - tslib "^2.0.0" - cli-ux@^5.1.0: version "5.6.3" resolved "https://registry.yarnpkg.com/cli-ux/-/cli-ux-5.6.3.tgz#eecdb2e0261171f2b28f2be6b18c490291c3a287" @@ -1362,68 +1336,6 @@ cli-ux@^6.0.3: supports-hyperlinks "^2.1.0" tslib "^2.0.0" -cli-ux@^6.0.4: - version "6.0.4" - resolved "https://registry.yarnpkg.com/cli-ux/-/cli-ux-6.0.4.tgz#36acb0a30fda27a6c59686a44d783b7c92a3dad0" - integrity sha512-9B7pLM1kPXQTHHvZtEKHLpJc9BDHIHZOWuPmJX/O+ZSO8zKmfV9A1rRiq8HNOMG7+Yuy3rtCgFqFl/KB7ZnYiQ== - dependencies: - "@oclif/core" "^1.0.7" - "@oclif/linewrap" "^1.0.0" - "@oclif/screen" "^1.0.3" - ansi-escapes "^4.3.0" - ansi-styles "^4.2.0" - cardinal "^2.1.1" - chalk "^4.1.0" - clean-stack "^3.0.0" - cli-progress "^3.9.1" - extract-stack "^2.0.0" - fs-extra "^8.1" - hyperlinker "^1.0.0" - indent-string "^4.0.0" - is-wsl "^2.2.0" - js-yaml "^3.13.1" - lodash "^4.17.21" - natural-orderby "^2.0.1" - object-treeify "^1.1.4" - password-prompt "^1.1.2" - semver "^7.3.2" - string-width "^4.2.0" - strip-ansi "^6.0.0" - supports-color "^8.1.0" - supports-hyperlinks "^2.1.0" - tslib "^2.0.0" - -cli-ux@^6.0.6: - version "6.0.6" - resolved "https://registry.npmjs.org/cli-ux/-/cli-ux-6.0.6.tgz#00536bf6038f195b0a1a2589f61ce5e625e75870" - integrity sha512-CvL4qmV78VhnbyHTswGjpDSQtU+oj3hT9DP9L6yMOwiTiNv0nMjMEV/8zou4CSqO6PtZ2A8qnlZDgAc07Js+aw== - dependencies: - "@oclif/core" "1.0.10" - "@oclif/linewrap" "^1.0.0" - "@oclif/screen" "^1.0.4 " - ansi-escapes "^4.3.0" - ansi-styles "^4.2.0" - cardinal "^2.1.1" - chalk "^4.1.0" - clean-stack "^3.0.0" - cli-progress "^3.9.1" - extract-stack "^2.0.0" - fs-extra "^8.1" - hyperlinker "^1.0.0" - indent-string "^4.0.0" - is-wsl "^2.2.0" - js-yaml "^3.13.1" - lodash "^4.17.21" - natural-orderby "^2.0.1" - object-treeify "^1.1.4" - password-prompt "^1.1.2" - semver "^7.3.2" - string-width "^4.2.0" - strip-ansi "^6.0.0" - supports-color "^8.1.0" - supports-hyperlinks "^2.1.0" - tslib "^2.0.0" - cliui@^7.0.2: version "7.0.4" resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" @@ -2749,7 +2661,7 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= -natural-orderby@^2.0.1: +natural-orderby@^2.0.1, natural-orderby@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/natural-orderby/-/natural-orderby-2.0.3.tgz#8623bc518ba162f8ff1cdb8941d74deb0fdcc016" integrity sha512-p7KTHxU0CUrcOXe62Zfrb5Z13nLvPhSWR/so3kFulUQU0sgUll2Z0LwpsLN351eOOD+hRGu/F1g+6xDfPeD++Q== @@ -3408,7 +3320,7 @@ strip-json-comments@3.1.1, strip-json-comments@^3.1.0, strip-json-comments@^3.1. resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== -supports-color@8.1.1, supports-color@^8.1.0: +supports-color@8.1.1, supports-color@^8.1.0, supports-color@^8.1.1: version "8.1.1" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== @@ -3429,7 +3341,7 @@ supports-color@^7.0.0, supports-color@^7.1.0, supports-color@^7.2.0: dependencies: has-flag "^4.0.0" -supports-hyperlinks@^2.1.0: +supports-hyperlinks@^2.1.0, supports-hyperlinks@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz#4f77b42488765891774b70c79babd87f9bd594bb" integrity sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ== @@ -3576,10 +3488,10 @@ type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== -typescript@4.5.2: - version "4.5.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.2.tgz#8ac1fba9f52256fdb06fb89e4122fa6a346c2998" - integrity sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw== +typescript@4.4.4: + version "4.4.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.4.tgz#2cd01a1a1f160704d3101fd5a58ff0f9fcb8030c" + integrity sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA== universalify@^0.1.0: version "0.1.2" From 936a380a9e7526fb02c415a83ee5b8dd08cd641b Mon Sep 17 00:00:00 2001 From: Peter Hale Date: Fri, 14 Jan 2022 09:08:28 -0700 Subject: [PATCH 02/28] chore: fix relative paths --- src/cli-ux/index.ts | 2 +- src/cli-ux/styled/table.ts | 4 +-- yarn.lock | 56 +++++++++++++++++++++++++++++++++++++- 3 files changed, 58 insertions(+), 4 deletions(-) diff --git a/src/cli-ux/index.ts b/src/cli-ux/index.ts index b292d9f40..cffc88d8d 100644 --- a/src/cli-ux/index.ts +++ b/src/cli-ux/index.ts @@ -1,4 +1,4 @@ -import * as Errors from '../../src/errors' +import * as Errors from '../errors' import * as util from 'util' import {ActionBase} from './action/base' diff --git a/src/cli-ux/styled/table.ts b/src/cli-ux/styled/table.ts index ff71d765e..08da2dc8a 100644 --- a/src/cli-ux/styled/table.ts +++ b/src/cli-ux/styled/table.ts @@ -1,5 +1,5 @@ -import * as Interfaces from '../../../src/interfaces' -import * as F from '../../../src/flags' +import * as Interfaces from '../../interfaces' +import * as F from '../../flags' import {stdtermwidth} from '@oclif/screen' import * as chalk from 'chalk' import {capitalize, sumBy} from 'lodash' diff --git a/yarn.lock b/yarn.lock index 3887df093..4e01799b7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -513,6 +513,29 @@ widest-line "^3.1.0" wrap-ansi "^7.0.0" +"@oclif/core@^1.1.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@oclif/core/-/core-1.1.2.tgz#e8cddb248ce49daa6f49b01f593d420522a75fd1" + integrity sha512-hoox4ygjazblt9icBs3dc0Zaw9m5S6nwzjgSnlqzJ9Kb6PzrwGtc1YOYxnJFB8z/Fyv3hpiQo3Ze2DIeGsF/wg== + dependencies: + "@oclif/linewrap" "^1.0.0" + chalk "^4.1.2" + clean-stack "^3.0.1" + cli-ux "^6.0.6" + debug "^4.3.3" + ejs "^3.1.6" + fs-extra "^9.1.0" + get-package-type "^0.1.0" + globby "^11.0.4" + indent-string "^4.0.0" + is-wsl "^2.2.0" + semver "^7.3.5" + string-width "^4.2.3" + strip-ansi "^6.0.1" + tslib "^2.3.1" + widest-line "^3.1.0" + wrap-ansi "^7.0.0" + "@oclif/errors@^1.2.1", "@oclif/errors@^1.2.2", "@oclif/errors@^1.3.3", "@oclif/errors@^1.3.5": version "1.3.5" resolved "https://registry.yarnpkg.com/@oclif/errors/-/errors-1.3.5.tgz#a1e9694dbeccab10fe2fe15acb7113991bed636c" @@ -580,7 +603,7 @@ tslib "^2.0.0" yarn "^1.21.1" -"@oclif/screen@^1.0.3": +"@oclif/screen@^1.0.3", "@oclif/screen@^1.0.4 ": version "1.0.4" resolved "https://registry.yarnpkg.com/@oclif/screen/-/screen-1.0.4.tgz#b740f68609dfae8aa71c3a6cab15d816407ba493" integrity sha512-60CHpq+eqnTxLZQ4PGHYNwUX572hgpMHGPtTWMjdTMsAvlm69lZV/4ly6O3sAYkomo4NggGcomrDpBe34rxUqw== @@ -1336,6 +1359,37 @@ cli-ux@^6.0.3: supports-hyperlinks "^2.1.0" tslib "^2.0.0" +cli-ux@^6.0.6, cli-ux@^6.0.8: + version "6.0.8" + resolved "https://registry.yarnpkg.com/cli-ux/-/cli-ux-6.0.8.tgz#a3943e889df827ceab2ffbe3e46c1fff194099e9" + integrity sha512-ERJ61QDVS1fqnWhzp3cPFHbfucmkzWh/K6SlMlf5GweIb0wB4G/wtZiAeWK6TOTSFXGQdGczVHzWrG1BMqTmSw== + dependencies: + "@oclif/core" "^1.1.1" + "@oclif/linewrap" "^1.0.0" + "@oclif/screen" "^1.0.4 " + ansi-escapes "^4.3.0" + ansi-styles "^4.2.0" + cardinal "^2.1.1" + chalk "^4.1.0" + clean-stack "^3.0.0" + cli-progress "^3.10.0" + extract-stack "^2.0.0" + fs-extra "^8.1" + hyperlinker "^1.0.0" + indent-string "^4.0.0" + is-wsl "^2.2.0" + js-yaml "^3.13.1" + lodash "^4.17.21" + natural-orderby "^2.0.1" + object-treeify "^1.1.4" + password-prompt "^1.1.2" + semver "^7.3.2" + string-width "^4.2.0" + strip-ansi "^6.0.0" + supports-color "^8.1.0" + supports-hyperlinks "^2.1.0" + tslib "^2.0.0" + cliui@^7.0.2: version "7.0.4" resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" From a4a43a4049640359f2b86e5316b221f0285fd042 Mon Sep 17 00:00:00 2001 From: Peter Hale Date: Fri, 14 Jan 2022 12:20:44 -0700 Subject: [PATCH 03/28] feat: merge cli-ux library with oclif/core Removes the circlular dependency between oclif/core and cli-ux --- src/cli-ux/action/pride-spinner.ts | 2 +- src/cli-ux/action/spinner.ts | 2 +- src/cli-ux/styled/header.ts | 2 +- src/cli-ux/styled/json.ts | 2 +- test/integration/sf.e2e.ts | 20 ++++++++--- yarn.lock | 56 +----------------------------- 6 files changed, 20 insertions(+), 64 deletions(-) diff --git a/src/cli-ux/action/pride-spinner.ts b/src/cli-ux/action/pride-spinner.ts index 776e13d1c..28731da96 100644 --- a/src/cli-ux/action/pride-spinner.ts +++ b/src/cli-ux/action/pride-spinner.ts @@ -1,6 +1,6 @@ // tslint:disable restrict-plus-operands -import chalk from 'chalk' +import * as chalk from 'chalk' import * as supportsColor from 'supports-color' import SpinnerAction from './spinner' diff --git a/src/cli-ux/action/spinner.ts b/src/cli-ux/action/spinner.ts index 7a68007d2..936d602c2 100644 --- a/src/cli-ux/action/spinner.ts +++ b/src/cli-ux/action/spinner.ts @@ -1,6 +1,6 @@ // tslint:disable restrict-plus-operands -import chalk from 'chalk' +import * as chalk from 'chalk' import * as supportsColor from 'supports-color' import deps from '../deps' diff --git a/src/cli-ux/styled/header.ts b/src/cli-ux/styled/header.ts index dd3db8385..ecc2c8bac 100644 --- a/src/cli-ux/styled/header.ts +++ b/src/cli-ux/styled/header.ts @@ -1,6 +1,6 @@ // tslint:disable restrict-plus-operands -import chalk from 'chalk' +import * as chalk from 'chalk' export default function styledHeader(header: string) { process.stdout.write(chalk.dim('=== ') + chalk.bold(header) + '\n') diff --git a/src/cli-ux/styled/json.ts b/src/cli-ux/styled/json.ts index 4da122da9..d5c728151 100644 --- a/src/cli-ux/styled/json.ts +++ b/src/cli-ux/styled/json.ts @@ -1,6 +1,6 @@ // tslint:disable restrict-plus-operands -import chalk from 'chalk' +import * as chalk from 'chalk' import cli from '..' diff --git a/test/integration/sf.e2e.ts b/test/integration/sf.e2e.ts index 0b984a830..eae051f57 100644 --- a/test/integration/sf.e2e.ts +++ b/test/integration/sf.e2e.ts @@ -1,6 +1,15 @@ import * as os from 'os' import {expect} from 'chai' import {Executor, setup} from './util' +import StripAnsi = require('strip-ansi') +const stripAnsi: typeof StripAnsi = require('strip-ansi') + +const chalk = require('chalk') +chalk.level = 0 + +function parseJson(json: string) { + return JSON.parse(stripAnsi(json)) +} describe('Salesforce CLI (sf)', () => { let executor: Executor @@ -41,7 +50,7 @@ describe('Salesforce CLI (sf)', () => { * ENVIRONMENT VARIABLES * */ - const regex = /^[A-Z].*\n\nUSAGE[\S\s]*\n\nFLAGS[\S\s]*\n\nGLOBAL FLAGS[\S\s]*\n\nDESCRIPTION[\S\s]*\n\nEXAMPLES[\S\s]*\n\nFLAG DESCRIPTIONS[\S\s]*\n\nCONFIGURATION VARIABLES[\S\s]*\n\nENVIRONMENT VARIABLES[\S\s]*$/g + const regex = /^.*?USAGE.*?FLAGS.*?GLOBAL FLAGS.*?DESCRIPTION.*?EXAMPLES.*?FLAG DESCRIPTIONS.*?CONFIGURATION VARIABLES.*?ENVIRONMENT VARIABLES.*$/gs expect(regex.test(help.output!)).to.be.true }) @@ -62,7 +71,7 @@ describe('Salesforce CLI (sf)', () => { * GLOBAL FLAGS * */ - const regex = /^[A-Z].*\n\nUSAGE[\S\s]*\n\nFLAGS[\S\s]*\n\nGLOBAL FLAGS[\S\s]*$/g + const regex = /^.*?USAGE.*?FLAGS.*?GLOBAL FLAGS.*?(?!DESCRIPTION).*?(?!EXAMPLES).*?(?!FLAG DESCRIPTIONS).*?(?!CONFIGURATION VARIABLES).*?(?!ENVIRONMENT VARIABLES).*$/gs expect(regex.test(help.output!)).to.be.true }) @@ -76,7 +85,8 @@ describe('Salesforce CLI (sf)', () => { it('should have formatted json success output', async () => { const config = await executor.executeCommand('config list --json') - const result = JSON.parse(config.output!) + console.log(config.output!) + const result = parseJson(config.output!) expect(result).to.have.property('status') expect(result).to.have.property('result') expect(result).to.have.property('warnings') @@ -84,7 +94,7 @@ describe('Salesforce CLI (sf)', () => { it('should have formatted json error output', async () => { const config = await executor.executeCommand('config set DOES_NOT_EXIST --json') - const result = JSON.parse(config.output!) + const result = parseJson(config.output!) expect(result).to.have.property('status') expect(result).to.have.property('stack') expect(result).to.have.property('name') @@ -94,7 +104,7 @@ describe('Salesforce CLI (sf)', () => { it('should handle varags', async () => { const config = await executor.executeCommand('config set disableTelemetry=true restDeploy=true --global --json') - const parsed = JSON.parse(config.output!) + const parsed = parseJson(config.output!) expect(parsed.status).to.equal(0) const results = parsed.result as Array<{success: boolean}> for (const result of results) { diff --git a/yarn.lock b/yarn.lock index 4e01799b7..3887df093 100644 --- a/yarn.lock +++ b/yarn.lock @@ -513,29 +513,6 @@ widest-line "^3.1.0" wrap-ansi "^7.0.0" -"@oclif/core@^1.1.1": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@oclif/core/-/core-1.1.2.tgz#e8cddb248ce49daa6f49b01f593d420522a75fd1" - integrity sha512-hoox4ygjazblt9icBs3dc0Zaw9m5S6nwzjgSnlqzJ9Kb6PzrwGtc1YOYxnJFB8z/Fyv3hpiQo3Ze2DIeGsF/wg== - dependencies: - "@oclif/linewrap" "^1.0.0" - chalk "^4.1.2" - clean-stack "^3.0.1" - cli-ux "^6.0.6" - debug "^4.3.3" - ejs "^3.1.6" - fs-extra "^9.1.0" - get-package-type "^0.1.0" - globby "^11.0.4" - indent-string "^4.0.0" - is-wsl "^2.2.0" - semver "^7.3.5" - string-width "^4.2.3" - strip-ansi "^6.0.1" - tslib "^2.3.1" - widest-line "^3.1.0" - wrap-ansi "^7.0.0" - "@oclif/errors@^1.2.1", "@oclif/errors@^1.2.2", "@oclif/errors@^1.3.3", "@oclif/errors@^1.3.5": version "1.3.5" resolved "https://registry.yarnpkg.com/@oclif/errors/-/errors-1.3.5.tgz#a1e9694dbeccab10fe2fe15acb7113991bed636c" @@ -603,7 +580,7 @@ tslib "^2.0.0" yarn "^1.21.1" -"@oclif/screen@^1.0.3", "@oclif/screen@^1.0.4 ": +"@oclif/screen@^1.0.3": version "1.0.4" resolved "https://registry.yarnpkg.com/@oclif/screen/-/screen-1.0.4.tgz#b740f68609dfae8aa71c3a6cab15d816407ba493" integrity sha512-60CHpq+eqnTxLZQ4PGHYNwUX572hgpMHGPtTWMjdTMsAvlm69lZV/4ly6O3sAYkomo4NggGcomrDpBe34rxUqw== @@ -1359,37 +1336,6 @@ cli-ux@^6.0.3: supports-hyperlinks "^2.1.0" tslib "^2.0.0" -cli-ux@^6.0.6, cli-ux@^6.0.8: - version "6.0.8" - resolved "https://registry.yarnpkg.com/cli-ux/-/cli-ux-6.0.8.tgz#a3943e889df827ceab2ffbe3e46c1fff194099e9" - integrity sha512-ERJ61QDVS1fqnWhzp3cPFHbfucmkzWh/K6SlMlf5GweIb0wB4G/wtZiAeWK6TOTSFXGQdGczVHzWrG1BMqTmSw== - dependencies: - "@oclif/core" "^1.1.1" - "@oclif/linewrap" "^1.0.0" - "@oclif/screen" "^1.0.4 " - ansi-escapes "^4.3.0" - ansi-styles "^4.2.0" - cardinal "^2.1.1" - chalk "^4.1.0" - clean-stack "^3.0.0" - cli-progress "^3.10.0" - extract-stack "^2.0.0" - fs-extra "^8.1" - hyperlinker "^1.0.0" - indent-string "^4.0.0" - is-wsl "^2.2.0" - js-yaml "^3.13.1" - lodash "^4.17.21" - natural-orderby "^2.0.1" - object-treeify "^1.1.4" - password-prompt "^1.1.2" - semver "^7.3.2" - string-width "^4.2.0" - strip-ansi "^6.0.0" - supports-color "^8.1.0" - supports-hyperlinks "^2.1.0" - tslib "^2.0.0" - cliui@^7.0.2: version "7.0.4" resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" From efe54585f90fb5938f709a0aea580f2c00032e13 Mon Sep 17 00:00:00 2001 From: Peter Hale Date: Tue, 18 Jan 2022 12:33:27 -0700 Subject: [PATCH 04/28] v1.1.2-test-02 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3324120a8..518631a97 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@oclif/core", "description": "base library for oclif CLIs", - "version": "1.1.2", + "version": "1.1.2-test-02", "author": "Salesforce", "bugs": "https://github.com/oclif/core/issues", "dependencies": { From 754b2563df5e7157439fa4578f5f29a07f161302 Mon Sep 17 00:00:00 2001 From: Peter Hale Date: Tue, 18 Jan 2022 12:57:04 -0700 Subject: [PATCH 05/28] chore: resolve name collision for core/Config and cli-ux/Config --- src/cli-ux/config.ts | 6 +++--- src/cli-ux/index.ts | 5 ++--- src/index.ts | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/cli-ux/config.ts b/src/cli-ux/config.ts index e3cb40f13..e0a110e1c 100644 --- a/src/cli-ux/config.ts +++ b/src/cli-ux/config.ts @@ -27,7 +27,7 @@ const Action = actionType === 'spinner' ? require('./action/spinner').default : const PrideAction = actionType === 'spinner' ? require('./action/pride-spinner').default : require('./action/simple').default /* eslint-enable node/no-missing-require */ -export class Config { +export class UxConfig { outputLevel: Levels = 'info' action: ActionBase = new Action() @@ -57,9 +57,9 @@ export class Config { function fetch() { if (globals[version.major]) return globals[version.major] - globals[version.major] = new Config() + globals[version.major] = new UxConfig() return globals[version.major] } -export const config: Config = fetch() +export const config: UxConfig = fetch() export default config diff --git a/src/cli-ux/index.ts b/src/cli-ux/index.ts index cffc88d8d..68f56e395 100644 --- a/src/cli-ux/index.ts +++ b/src/cli-ux/index.ts @@ -2,7 +2,7 @@ import * as Errors from '../errors' import * as util from 'util' import {ActionBase} from './action/base' -import {config, Config} from './config' +import {config, UxConfig} from './config' import deps from './deps' import {ExitError} from './exit' import {IPromptOptions} from './prompt' @@ -127,13 +127,12 @@ export const ux = { await timeout(flush(), 10_000) }, } -export default ux export const cli = ux export { config, ActionBase, - Config, + UxConfig, ExitError, IPromptOptions, Table, diff --git a/src/index.ts b/src/index.ts index 8ce78e447..016363056 100644 --- a/src/index.ts +++ b/src/index.ts @@ -42,7 +42,7 @@ export { flush, } -export * from './cli-ux/index' +export * from './cli-ux' function checkCWD() { try { From dec70044685255043c9bcd58dd0a8ee66777578b Mon Sep 17 00:00:00 2001 From: Peter Hale Date: Tue, 18 Jan 2022 12:58:15 -0700 Subject: [PATCH 06/28] chore: revert to latest version number --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 518631a97..3324120a8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@oclif/core", "description": "base library for oclif CLIs", - "version": "1.1.2-test-02", + "version": "1.1.2", "author": "Salesforce", "bugs": "https://github.com/oclif/core/issues", "dependencies": { From 31321d4bff356e77a96e966b9d0bcceaf6c604d1 Mon Sep 17 00:00:00 2001 From: Peter Hale Date: Tue, 18 Jan 2022 13:04:24 -0700 Subject: [PATCH 07/28] chore: fix build error --- src/cli-ux/styled/json.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli-ux/styled/json.ts b/src/cli-ux/styled/json.ts index d5c728151..eec1748cd 100644 --- a/src/cli-ux/styled/json.ts +++ b/src/cli-ux/styled/json.ts @@ -2,7 +2,7 @@ import * as chalk from 'chalk' -import cli from '..' +import {cli} from '..' export default function styledJSON(obj: any) { const json = JSON.stringify(obj, null, 2) From 263ff55cd7cfb527fc6bfec77272f9a3e5cf56db Mon Sep 17 00:00:00 2001 From: Peter Hale Date: Tue, 18 Jan 2022 13:16:22 -0700 Subject: [PATCH 08/28] chore: fix imports in tests --- test/cli-ux/fancy.ts | 2 +- test/cli-ux/index.test.ts | 2 +- test/cli-ux/prompt.test.ts | 2 +- test/cli-ux/styled/object.test.ts | 2 +- test/cli-ux/styled/progress.test.ts | 2 +- test/cli-ux/styled/table.test.ts | 2 +- test/cli-ux/styled/tree.test.ts | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/test/cli-ux/fancy.ts b/test/cli-ux/fancy.ts index ea7e0610a..ea217fe5c 100644 --- a/test/cli-ux/fancy.ts +++ b/test/cli-ux/fancy.ts @@ -2,7 +2,7 @@ import {expect, fancy as base, FancyTypes} from 'fancy-test' import * as fs from 'fs-extra' import * as path from 'path' -import cli from '../../src/cli-ux' +import {cli} from '../../src/cli-ux' export { expect, diff --git a/test/cli-ux/index.test.ts b/test/cli-ux/index.test.ts index 46791c200..81f6b0d52 100644 --- a/test/cli-ux/index.test.ts +++ b/test/cli-ux/index.test.ts @@ -1,4 +1,4 @@ -import ux from '../../src/cli-ux' +import {ux} from '../../src/cli-ux' import {expect, fancy} from './fancy' const hyperlinker = require('hyperlinker') diff --git a/test/cli-ux/prompt.test.ts b/test/cli-ux/prompt.test.ts index c6fa8c013..2d1ff3e2a 100644 --- a/test/cli-ux/prompt.test.ts +++ b/test/cli-ux/prompt.test.ts @@ -2,7 +2,7 @@ import * as chai from 'chai' const expect = chai.expect -import cli from '../../src/cli-ux' +import {cli} from '../../src/cli-ux' import {fancy} from './fancy' diff --git a/test/cli-ux/styled/object.test.ts b/test/cli-ux/styled/object.test.ts index 87718294e..21819d527 100644 --- a/test/cli-ux/styled/object.test.ts +++ b/test/cli-ux/styled/object.test.ts @@ -1,6 +1,6 @@ import {expect, fancy} from 'fancy-test' -import cli from '../../../src/cli-ux' +import {cli} from '../../../src/cli-ux' describe('styled/object', () => { fancy diff --git a/test/cli-ux/styled/progress.test.ts b/test/cli-ux/styled/progress.test.ts index 75bb6071c..76efc58f8 100644 --- a/test/cli-ux/styled/progress.test.ts +++ b/test/cli-ux/styled/progress.test.ts @@ -1,7 +1,7 @@ import {expect, fancy} from 'fancy-test' // import {BarType, Progress} from '../../src/progress' -import cli from '../../../src/cli-ux' +import {cli} from '../../../src/cli-ux' describe('progress', () => { // single bar diff --git a/test/cli-ux/styled/table.test.ts b/test/cli-ux/styled/table.test.ts index cd4f7ca90..3f9636211 100644 --- a/test/cli-ux/styled/table.test.ts +++ b/test/cli-ux/styled/table.test.ts @@ -1,6 +1,6 @@ import {expect, fancy} from 'fancy-test' -import cli from '../../../src/cli-ux' +import {cli} from '../../../src/cli-ux' /* eslint-disable camelcase */ const apps = [ diff --git a/test/cli-ux/styled/tree.test.ts b/test/cli-ux/styled/tree.test.ts index be4ddae26..cb602495c 100644 --- a/test/cli-ux/styled/tree.test.ts +++ b/test/cli-ux/styled/tree.test.ts @@ -1,6 +1,6 @@ import {expect, fancy} from 'fancy-test' -import cli from '../../../src/cli-ux' +import {cli} from '../../../src/cli-ux' describe('styled/tree', () => { fancy From 905e5d5a2d8f4542536a9ea561181b5bbf5322c1 Mon Sep 17 00:00:00 2001 From: Peter Hale Date: Tue, 18 Jan 2022 15:03:07 -0700 Subject: [PATCH 09/28] chore: wrap cli-ux exported members in namespace --- src/cli-ux/action/base.ts | 304 ++++++++++++++-------------- src/cli-ux/action/simple.ts | 6 +- src/cli-ux/action/spinner.ts | 7 +- src/cli-ux/config.ts | 57 +++--- src/cli-ux/deps.ts | 1 + src/cli-ux/exit.ts | 26 +-- src/cli-ux/index.ts | 209 ++++++++++--------- src/cli-ux/prompt.ts | 113 ++++++----- src/cli-ux/styled/json.ts | 6 +- src/command.ts | 8 +- test/cli-ux/fancy.ts | 4 +- test/cli-ux/index.test.ts | 4 +- test/cli-ux/prompt.test.ts | 24 +-- test/cli-ux/styled/object.test.ts | 4 +- test/cli-ux/styled/progress.test.ts | 8 +- test/cli-ux/styled/table.test.ts | 48 ++--- test/cli-ux/styled/tree.test.ts | 6 +- 17 files changed, 421 insertions(+), 414 deletions(-) diff --git a/src/cli-ux/action/base.ts b/src/cli-ux/action/base.ts index cfb51b50b..a93f7161b 100644 --- a/src/cli-ux/action/base.ts +++ b/src/cli-ux/action/base.ts @@ -1,201 +1,203 @@ import castArray from 'lodash/castArray' import {inspect} from 'util' -export interface ITask { - action: string; - status: string | undefined; - active: boolean; +export namespace CliUx { + export interface ITask { + action: string; + status: string | undefined; + active: boolean; + } + export type ActionType = 'spinner' | 'simple' | 'debug' + export interface Options { + stdout?: boolean; + } } -export type ActionType = 'spinner' | 'simple' | 'debug' +export namespace CliUx { + export class ActionBase { + type!: ActionType -export interface Options { - stdout?: boolean; -} + std: 'stdout' | 'stderr' = 'stderr' -export class ActionBase { - type!: ActionType + protected stdmocks?: ['stdout' | 'stderr', string[]][] - std: 'stdout' | 'stderr' = 'stderr' + private stdmockOrigs = { + stdout: process.stdout.write, + stderr: process.stderr.write, + } - protected stdmocks?: ['stdout' | 'stderr', string[]][] + public start(action: string, status?: string, opts: Options = {}) { + this.std = opts.stdout ? 'stdout' : 'stderr' + const task = {action, status, active: Boolean(this.task && this.task.active)} + this.task = task - private stdmockOrigs = { - stdout: process.stdout.write, - stderr: process.stderr.write, - } + this._start() + task.active = true + this._stdout(true) + } - public start(action: string, status?: string, opts: Options = {}) { - this.std = opts.stdout ? 'stdout' : 'stderr' - const task = {action, status, active: Boolean(this.task && this.task.active)} - this.task = task + public stop(msg = 'done') { + const task = this.task + if (!task) { + return + } - this._start() - task.active = true - this._stdout(true) - } + this._stop(msg) + task.active = false + this.task = undefined + this._stdout(false) + } - public stop(msg = 'done') { - const task = this.task - if (!task) { - return + private get globals(): { action: { task?: ITask }; output: string | undefined } { + (global as any)['cli-ux'] = (global as any)['cli-ux'] || {} + const globals = (global as any)['cli-ux'] + globals.action = globals.action || {} + return globals } - this._stop(msg) - task.active = false - this.task = undefined - this._stdout(false) - } + public get task(): ITask | undefined { + return this.globals.action.task + } - private get globals(): { action: { task?: ITask }; output: string | undefined } { - (global as any)['cli-ux'] = (global as any)['cli-ux'] || {} - const globals = (global as any)['cli-ux'] - globals.action = globals.action || {} - return globals - } + public set task(task: ITask | undefined) { + this.globals.action.task = task + } - public get task(): ITask | undefined { - return this.globals.action.task - } + protected get output(): string | undefined { + return this.globals.output + } - public set task(task: ITask | undefined) { - this.globals.action.task = task - } + protected set output(output: string | undefined) { + this.globals.output = output + } - protected get output(): string | undefined { - return this.globals.output - } + get running(): boolean { + return Boolean(this.task) + } - protected set output(output: string | undefined) { - this.globals.output = output - } + get status(): string | undefined { + return this.task ? this.task.status : undefined + } - get running(): boolean { - return Boolean(this.task) - } + set status(status: string | undefined) { + const task = this.task + if (!task) { + return + } - get status(): string | undefined { - return this.task ? this.task.status : undefined - } + if (task.status === status) { + return + } - set status(status: string | undefined) { - const task = this.task - if (!task) { - return + this._updateStatus(status, task.status) + task.status = status } - if (task.status === status) { - return - } + public async pauseAsync(fn: () => Promise, icon?: string) { + const task = this.task + const active = task && task.active + if (task && active) { + this._pause(icon) + this._stdout(false) + task.active = false + } - this._updateStatus(status, task.status) - task.status = status - } + const ret = await fn() + if (task && active) { + this._resume() + } - public async pauseAsync(fn: () => Promise, icon?: string) { - const task = this.task - const active = task && task.active - if (task && active) { - this._pause(icon) - this._stdout(false) - task.active = false + return ret } - const ret = await fn() - if (task && active) { - this._resume() - } + public pause(fn: () => any, icon?: string) { + const task = this.task + const active = task && task.active + if (task && active) { + this._pause(icon) + this._stdout(false) + task.active = false + } - return ret - } + const ret = fn() + if (task && active) { + this._resume() + } - public pause(fn: () => any, icon?: string) { - const task = this.task - const active = task && task.active - if (task && active) { - this._pause(icon) - this._stdout(false) - task.active = false + return ret } - const ret = fn() - if (task && active) { - this._resume() + protected _start() { + throw new Error('not implemented') } - return ret - } - - protected _start() { - throw new Error('not implemented') - } - - protected _stop(_: string) { - throw new Error('not implemented') - } + protected _stop(_: string) { + throw new Error('not implemented') + } - protected _resume() { - if (this.task) this.start(this.task.action, this.task.status) - } + protected _resume() { + if (this.task) this.start(this.task.action, this.task.status) + } - protected _pause(_?: string) { - throw new Error('not implemented') - } + protected _pause(_?: string) { + throw new Error('not implemented') + } - protected _updateStatus(_: string | undefined, __?: string) {} - - // mock out stdout/stderr so it doesn't screw up the rendering - protected _stdout(toggle: boolean) { - try { - const outputs: ['stdout', 'stderr'] = ['stdout', 'stderr'] - if (toggle) { - if (this.stdmocks) return - this.stdmockOrigs = { - stdout: process.stdout.write, - stderr: process.stderr.write, - } + protected _updateStatus(_: string | undefined, __?: string) {} + + // mock out stdout/stderr so it doesn't screw up the rendering + protected _stdout(toggle: boolean) { + try { + const outputs: ['stdout', 'stderr'] = ['stdout', 'stderr'] + if (toggle) { + if (this.stdmocks) return + this.stdmockOrigs = { + stdout: process.stdout.write, + stderr: process.stderr.write, + } - this.stdmocks = [] - for (const std of outputs) { - (process[std] as any).write = (...args: any[]) => { - this.stdmocks!.push([std, args] as ['stdout' | 'stderr', string[]]) + this.stdmocks = [] + for (const std of outputs) { + (process[std] as any).write = (...args: any[]) => { + this.stdmocks!.push([std, args] as ['stdout' | 'stderr', string[]]) + } } + } else { + if (!this.stdmocks) return + // this._write('stderr', '\nresetstdmock\n\n\n') + delete this.stdmocks + for (const std of outputs) process[std].write = this.stdmockOrigs[std] as any } - } else { - if (!this.stdmocks) return - // this._write('stderr', '\nresetstdmock\n\n\n') - delete this.stdmocks - for (const std of outputs) process[std].write = this.stdmockOrigs[std] as any + } catch (error) { + this._write('stderr', inspect(error)) } - } catch (error) { - this._write('stderr', inspect(error)) } - } - // flush mocked stdout/stderr - protected _flushStdout() { - try { - let output = '' - let std: 'stdout' | 'stderr' | undefined - while (this.stdmocks && this.stdmocks.length > 0) { - const cur = this.stdmocks.shift() as ['stdout' | 'stderr', string[]] - std = cur[0] - this._write(std, cur[1]) - output += (cur[1][0] as any).toString('utf8') - } - // add newline if there isn't one already - // otherwise we'll just overwrite it when we render + // flush mocked stdout/stderr + protected _flushStdout() { + try { + let output = '' + let std: 'stdout' | 'stderr' | undefined + while (this.stdmocks && this.stdmocks.length > 0) { + const cur = this.stdmocks.shift() as ['stdout' | 'stderr', string[]] + std = cur[0] + this._write(std, cur[1]) + output += (cur[1][0] as any).toString('utf8') + } + // add newline if there isn't one already + // otherwise we'll just overwrite it when we render - if (output && std && output[output.length - 1] !== '\n') { - this._write(std, '\n') + if (output && std && output[output.length - 1] !== '\n') { + this._write(std, '\n') + } + } catch (error) { + this._write('stderr', inspect(error)) } - } catch (error) { - this._write('stderr', inspect(error)) } - } - // write to the real stdout/stderr - protected _write(std: 'stdout' | 'stderr', s: string | string[]) { - this.stdmockOrigs[std].apply(process[std], castArray(s) as [string]) + // write to the real stdout/stderr + protected _write(std: 'stdout' | 'stderr', s: string | string[]) { + this.stdmockOrigs[std].apply(process[std], castArray(s) as [string]) + } } } diff --git a/src/cli-ux/action/simple.ts b/src/cli-ux/action/simple.ts index 48722ae32..223eaeeb8 100644 --- a/src/cli-ux/action/simple.ts +++ b/src/cli-ux/action/simple.ts @@ -1,7 +1,7 @@ -import {ActionBase, ActionType} from './base' +import {CliUx} from './base' -export default class SimpleAction extends ActionBase { - public type: ActionType = 'simple' +export default class SimpleAction extends CliUx.ActionBase { + public type: CliUx.ActionType = 'simple' protected _start() { const task = this.task diff --git a/src/cli-ux/action/spinner.ts b/src/cli-ux/action/spinner.ts index 936d602c2..ba08ce475 100644 --- a/src/cli-ux/action/spinner.ts +++ b/src/cli-ux/action/spinner.ts @@ -5,18 +5,17 @@ import * as supportsColor from 'supports-color' import deps from '../deps' -import {ActionBase, ActionType} from './base' +import {CliUx} from './base' /* eslint-disable-next-line node/no-missing-require */ const spinners = require('./spinners') - function color(s: string): string { if (!supportsColor) return s const has256 = supportsColor.stdout ? supportsColor.stdout.has256 : (process.env.TERM || '').includes('256') return has256 ? `\u001B[38;5;104m${s}${deps.ansiStyles.reset.open}` : chalk.magenta(s) } -export default class SpinnerAction extends ActionBase { - public type: ActionType = 'spinner' +export default class SpinnerAction extends CliUx.ActionBase { + public type: CliUx.ActionType = 'spinner' spinner?: NodeJS.Timeout diff --git a/src/cli-ux/config.ts b/src/cli-ux/config.ts index e0a110e1c..a17ae1554 100644 --- a/src/cli-ux/config.ts +++ b/src/cli-ux/config.ts @@ -1,17 +1,18 @@ import * as semver from 'semver' -import {ActionBase} from './action/base' +import {CliUx as AB} from './action/base' const version = semver.parse(require('../../package.json').version)! export type Levels = 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'trace' -export interface ConfigMessage { - type: 'config'; - prop: string; - value: any; +export namespace CliUx { + export interface ConfigMessage { + type: 'config'; + prop: string; + value: any; + } } - const g: any = global const globals = g['cli-ux'] || (g['cli-ux'] = {}) @@ -27,39 +28,41 @@ const Action = actionType === 'spinner' ? require('./action/spinner').default : const PrideAction = actionType === 'spinner' ? require('./action/pride-spinner').default : require('./action/simple').default /* eslint-enable node/no-missing-require */ -export class UxConfig { - outputLevel: Levels = 'info' +export namespace CliUx { + export class Config { + outputLevel: Levels = 'info' - action: ActionBase = new Action() + action: AB.ActionBase = new Action() - prideAction: ActionBase = new PrideAction() + prideAction: AB.ActionBase = new PrideAction() - errorsHandled = false + errorsHandled = false - showStackTrace = true + showStackTrace = true - get debug(): boolean { - return globals.debug || process.env.DEBUG === '*' - } + get debug(): boolean { + return globals.debug || process.env.DEBUG === '*' + } - set debug(v: boolean) { - globals.debug = v - } + set debug(v: boolean) { + globals.debug = v + } - get context(): any { - return globals.context || {} - } + get context(): any { + return globals.context || {} + } - set context(v: any) { - globals.context = v + set context(v: any) { + globals.context = v + } } } - function fetch() { if (globals[version.major]) return globals[version.major] - globals[version.major] = new UxConfig() + globals[version.major] = new CliUx.Config() return globals[version.major] } -export const config: UxConfig = fetch() -export default config +export namespace CliUx { + export const config: Config = fetch() +} diff --git a/src/cli-ux/deps.ts b/src/cli-ux/deps.ts index 329486e84..02d0ff863 100644 --- a/src/cli-ux/deps.ts +++ b/src/cli-ux/deps.ts @@ -1,4 +1,5 @@ /* eslint-disable node/no-missing-require */ + export default { get stripAnsi(): (string: string) => string { return require('strip-ansi') diff --git a/src/cli-ux/exit.ts b/src/cli-ux/exit.ts index 4f036df30..6174d0d4a 100644 --- a/src/cli-ux/exit.ts +++ b/src/cli-ux/exit.ts @@ -1,17 +1,19 @@ -export class ExitError extends Error { - public 'cli-ux': { - exit: number; - } +export namespace CliUx { + export class ExitError extends Error { + public 'cli-ux': { + exit: number; + } - public code: 'EEXIT' + public code: 'EEXIT' - public error?: Error + public error?: Error - constructor(status: number, error?: Error) { - const code = 'EEXIT' - super(error ? error.message : `${code}: ${status}`) - this.error = error - this['cli-ux'] = {exit: status} - this.code = code + constructor(status: number, error?: Error) { + const code = 'EEXIT' + super(error ? error.message : `${code}: ${status}`) + this.error = error + this['cli-ux'] = {exit: status} + this.code = code + } } } diff --git a/src/cli-ux/index.ts b/src/cli-ux/index.ts index 68f56e395..d2d855062 100644 --- a/src/cli-ux/index.ts +++ b/src/cli-ux/index.ts @@ -1,12 +1,10 @@ import * as Errors from '../errors' import * as util from 'util' -import {ActionBase} from './action/base' -import {config, UxConfig} from './config' +import * as uxConfig from './config' import deps from './deps' -import {ExitError} from './exit' -import {IPromptOptions} from './prompt' import * as Table from './styled/table' +import {CliUx as BaseCliUx} from './action/base' const hyperlinker = require('hyperlinker') @@ -18,7 +16,7 @@ function timeout(p: Promise, ms: number) { }) } - return Promise.race([p, wait(ms, true).then(() => ux.error('timed out'))]) + return Promise.race([p, wait(ms, true).then(() => CliUx.ux.error('timed out'))]) } async function flush() { @@ -29,118 +27,117 @@ async function flush() { return p } -export const ux = { - config, - - warn: Errors.warn, - error: Errors.error, - exit: Errors.exit, - - get prompt() { - return deps.prompt.prompt - }, - /** +export namespace CliUx { + export const ux = { + config: uxConfig.CliUx.config, + warn: Errors.warn, + error: Errors.error, + exit: Errors.exit, + + get prompt() { + return deps.prompt.CliUx.prompt + }, + /** * "press anykey to continue" */ - get anykey() { - return deps.prompt.anykey - }, - get confirm() { - return deps.prompt.confirm - }, - get action() { - return config.action - }, - get prideAction() { - return config.prideAction - }, - styledObject(obj: any, keys?: string[]) { - ux.info(deps.styledObject(obj, keys)) - }, - get styledHeader() { - return deps.styledHeader - }, - get styledJSON() { - return deps.styledJSON - }, - get table() { - return deps.table - }, - get tree() { - return deps.tree - }, - get open() { - return deps.open - }, - get wait() { - return deps.wait - }, - get progress() { - return deps.progress - }, - - async done() { - config.action.stop() - // await flushStdout() - }, - - trace(format: string, ...args: string[]) { - if (this.config.outputLevel === 'trace') { - process.stdout.write(util.format(format, ...args) + '\n') - } - }, - - debug(format: string, ...args: string[]) { - if (['trace', 'debug'].includes(this.config.outputLevel)) { + get anykey() { + return deps.prompt.CliUx.anykey + }, + get confirm() { + return deps.prompt.CliUx.confirm + }, + get action() { + return uxConfig.CliUx.config.action + }, + get prideAction() { + return uxConfig.CliUx.config.prideAction + }, + styledObject(obj: any, keys?: string[]) { + ux.info(deps.styledObject(obj, keys)) + }, + get styledHeader() { + return deps.styledHeader + }, + get styledJSON() { + return deps.styledJSON + }, + get table() { + return deps.table + }, + get tree() { + return deps.tree + }, + get open() { + return deps.open + }, + get wait() { + return deps.wait + }, + get progress() { + return deps.progress + }, + + async done() { + uxConfig.CliUx.config.action.stop() + // await flushStdout() + }, + + trace(format: string, ...args: string[]) { + if (this.config.outputLevel === 'trace') { + process.stdout.write(util.format(format, ...args) + '\n') + } + }, + + debug(format: string, ...args: string[]) { + if (['trace', 'debug'].includes(this.config.outputLevel)) { + process.stdout.write(util.format(format, ...args) + '\n') + } + }, + + info(format: string, ...args: string[]) { process.stdout.write(util.format(format, ...args) + '\n') - } - }, - - info(format: string, ...args: string[]) { - process.stdout.write(util.format(format, ...args) + '\n') - }, - - log(format?: string, ...args: string[]) { - this.info(format || '', ...args) - }, - - url(text: string, uri: string, params = {}) { - const supports = require('supports-hyperlinks') - if (supports.stdout) { - this.log(hyperlinker(text, uri, params)) - } else { - this.log(uri) - } - }, - - annotation(text: string, annotation: string) { - const supports = require('supports-hyperlinks') - if (supports.stdout) { - // \u001b]8;;https://google.com\u0007sometext\u001b]8;;\u0007 - this.log(`\u001B]1337;AddAnnotation=${text.length}|${annotation}\u0007${text}`) - } else { - this.log(text) - } - }, - - async flush() { - await timeout(flush(), 10_000) - }, + }, + + log(format?: string, ...args: string[]) { + this.info(format || '', ...args) + }, + + url(text: string, uri: string, params = {}) { + const supports = require('supports-hyperlinks') + if (supports.stdout) { + this.log(hyperlinker(text, uri, params)) + } else { + this.log(uri) + } + }, + + annotation(text: string, annotation: string) { + const supports = require('supports-hyperlinks') + if (supports.stdout) { + // \u001b]8;;https://google.com\u0007sometext\u001b]8;;\u0007 + this.log(`\u001B]1337;AddAnnotation=${text.length}|${annotation}\u0007${text}`) + } else { + this.log(text) + } + }, + + async flush() { + await timeout(flush(), 10_000) + }, + } } -export const cli = ux export { - config, - ActionBase, - UxConfig, - ExitError, - IPromptOptions, Table, } +export namespace CliUx { + export const cli = ux +} + const cliuxProcessExitHandler = async () => { try { - await ux.done() + await CliUx.ux.done() } catch (error) { // tslint:disable no-console console.error(error) diff --git a/src/cli-ux/prompt.ts b/src/cli-ux/prompt.ts index ab4958622..d8ab06c73 100644 --- a/src/cli-ux/prompt.ts +++ b/src/cli-ux/prompt.ts @@ -1,18 +1,19 @@ import * as Errors from '../../src/errors' import * as chalk from 'chalk' - -import config from './config' +import * as config from './config' import deps from './deps' -export interface IPromptOptions { - prompt?: string; - type?: 'normal' | 'mask' | 'hide' | 'single'; - timeout?: number; - /** - * Requires user input if true, otherwise allows empty input - */ - required?: boolean; - default?: string; +export namespace CliUx { + export interface IPromptOptions { + prompt?: string; + type?: 'normal' | 'mask' | 'hide' | 'single'; + timeout?: number; + /** + * Requires user input if true, otherwise allows empty input + */ + required?: boolean; + default?: string; + } } interface IPromptConfig { @@ -80,7 +81,7 @@ function replacePrompt(prompt: string) { deps.ansiEscapes.cursorDown(1) + deps.ansiEscapes.cursorLeft + deps.ansiEscapes.cursorShow) } -function _prompt(name: string, inputOptions: Partial = {}): Promise { +function _prompt(name: string, inputOptions: Partial = {}): Promise { const prompt = getPrompt(name, inputOptions.type, inputOptions.default) const options: IPromptConfig = { isTTY: Boolean(process.env.TERM !== 'dumb' && process.stdin.isTTY), @@ -116,52 +117,54 @@ function _prompt(name: string, inputOptions: Partial = {}): Prom } } -/** - * prompt for input - * @param name - prompt text - * @param options - @see IPromptOptions - * @returns void - */ -export function prompt(name: string, options: IPromptOptions = {}) { - return config.action.pauseAsync(() => { - return _prompt(name, options) - }, chalk.cyan('?')) -} +export namespace CliUx { + /** + * prompt for input + * @param name - prompt text + * @param options - @see IPromptOptions + * @returns void + */ + export function prompt(name: string, options: CliUx.IPromptOptions = {}) { + return config.CliUx.config.action.pauseAsync(() => { + return _prompt(name, options) + }, chalk.cyan('?')) + } + + /** + * confirmation prompt (yes/no) + * @param message - confirmation text + * @returns Promise + */ + export function confirm(message: string): Promise { + return config.CliUx.config.action.pauseAsync(async () => { + const confirm = async (): Promise => { + const response = (await _prompt(message)).toLowerCase() + if (['n', 'no'].includes(response)) return false + if (['y', 'yes'].includes(response)) return true + return confirm() + } -/** - * confirmation prompt (yes/no) - * @param message - confirmation text - * @returns Promise - */ -export function confirm(message: string): Promise { - return config.action.pauseAsync(async () => { - const confirm = async (): Promise => { - const response = (await _prompt(message)).toLowerCase() - if (['n', 'no'].includes(response)) return false - if (['y', 'yes'].includes(response)) return true return confirm() - } + }, chalk.cyan('?')) + } - return confirm() - }, chalk.cyan('?')) -} + /** + * "press anykey to continue" + * @param message - optional message to display to user + * @returns Promise + */ + export async function anykey(message?: string): Promise { + const tty = Boolean(process.stdin.setRawMode) + if (!message) { + message = tty ? + `Press any key to continue or ${chalk.yellow('q')} to exit` : + `Press enter to continue or ${chalk.yellow('q')} to exit` + } -/** - * "press anykey to continue" - * @param message - optional message to display to user - * @returns Promise - */ -export async function anykey(message?: string): Promise { - const tty = Boolean(process.stdin.setRawMode) - if (!message) { - message = tty ? - `Press any key to continue or ${chalk.yellow('q')} to exit` : - `Press enter to continue or ${chalk.yellow('q')} to exit` + const char = await prompt(message, {type: 'single', required: false}) + if (tty) process.stderr.write('\n') + if (char === 'q') Errors.error('quit') + if (char === '\u0003') Errors.error('ctrl-c') + return char } - - const char = await prompt(message, {type: 'single', required: false}) - if (tty) process.stderr.write('\n') - if (char === 'q') Errors.error('quit') - if (char === '\u0003') Errors.error('ctrl-c') - return char } diff --git a/src/cli-ux/styled/json.ts b/src/cli-ux/styled/json.ts index eec1748cd..5ad9e0ecb 100644 --- a/src/cli-ux/styled/json.ts +++ b/src/cli-ux/styled/json.ts @@ -2,16 +2,16 @@ import * as chalk from 'chalk' -import {cli} from '..' +import {CliUx} from '..' export default function styledJSON(obj: any) { const json = JSON.stringify(obj, null, 2) if (!chalk.level) { - cli.info(json) + CliUx.cli.info(json) return } const cardinal = require('cardinal') const theme = require('cardinal/themes/jq') - cli.info(cardinal.highlight(json, {json: true, theme})) + CliUx.cli.info(cardinal.highlight(json, {json: true, theme})) } diff --git a/src/command.ts b/src/command.ts index 10c17bcd3..79ae63566 100644 --- a/src/command.ts +++ b/src/command.ts @@ -1,7 +1,7 @@ import {fileURLToPath} from 'url' import {format, inspect} from 'util' -import {cli} from './cli-ux' +import {CliUx} from './' import {Config} from './config' import * as Interfaces from './interfaces' import * as Errors from './errors' @@ -163,7 +163,7 @@ export default abstract class Command { } if (result && this.jsonEnabled()) { - cli.styledJSON(this.toSuccessJson(result)) + CliUx.cli.styledJSON(this.toSuccessJson(result)) } return result @@ -227,12 +227,12 @@ export default abstract class Command { protected async catch(err: Record): Promise { process.exitCode = process.exitCode ?? err.exitCode ?? 1 if (this.jsonEnabled()) { - cli.styledJSON(this.toErrorJson(err)) + CliUx.cli.styledJSON(this.toErrorJson(err)) } else { if (!err.message) throw err try { const chalk = require('chalk') - cli.action.stop(chalk.bold.red('!')) + CliUx.cli.action.stop(chalk.bold.red('!')) } catch {} throw err diff --git a/test/cli-ux/fancy.ts b/test/cli-ux/fancy.ts index ea217fe5c..38e705558 100644 --- a/test/cli-ux/fancy.ts +++ b/test/cli-ux/fancy.ts @@ -2,7 +2,7 @@ import {expect, fancy as base, FancyTypes} from 'fancy-test' import * as fs from 'fs-extra' import * as path from 'path' -import {cli} from '../../src/cli-ux' +import {CliUx} from '../../src/cli-ux' export { expect, @@ -20,5 +20,5 @@ export const fancy = base chalk.level = 0 }) .finally(async () => { - await cli.done() + await CliUx.cli.done() }) diff --git a/test/cli-ux/index.test.ts b/test/cli-ux/index.test.ts index 81f6b0d52..6d49da3cd 100644 --- a/test/cli-ux/index.test.ts +++ b/test/cli-ux/index.test.ts @@ -1,4 +1,4 @@ -import {ux} from '../../src/cli-ux' +import {CliUx} from '../../src/cli-ux' import {expect, fancy} from './fancy' const hyperlinker = require('hyperlinker') @@ -7,7 +7,7 @@ describe('url', () => { fancy .env({FORCE_HYPERLINK: '1'}, {clear: true}) .stdout() - .do(() => ux.url('sometext', 'https://google.com')) + .do(() => CliUx.ux.url('sometext', 'https://google.com')) .it('renders hyperlink', async ({stdout}) => { expect(stdout).to.equal('sometext\n') }) diff --git a/test/cli-ux/prompt.test.ts b/test/cli-ux/prompt.test.ts index 2d1ff3e2a..d79c5dec4 100644 --- a/test/cli-ux/prompt.test.ts +++ b/test/cli-ux/prompt.test.ts @@ -2,7 +2,7 @@ import * as chai from 'chai' const expect = chai.expect -import {cli} from '../../src/cli-ux' +import {CliUx} from '../../src/cli-ux' import {fancy} from './fancy' @@ -11,11 +11,11 @@ describe('prompt', () => { .stdout() .stderr() .end('requires input', async () => { - const promptPromise = cli.prompt('Require input?') + const promptPromise = CliUx.cli.prompt('Require input?') process.stdin.emit('data', '') process.stdin.emit('data', 'answer') const answer = await promptPromise - await cli.done() + await CliUx.cli.done() expect(answer).to.equal('answer') }) @@ -24,9 +24,9 @@ describe('prompt', () => { .stderr() .stdin('y') .end('confirm', async () => { - const promptPromise = cli.confirm('yes/no?') + const promptPromise = CliUx.cli.confirm('yes/no?') const answer = await promptPromise - await cli.done() + await CliUx.cli.done() expect(answer).to.equal(true) }) @@ -35,9 +35,9 @@ describe('prompt', () => { .stderr() .stdin('n') .end('confirm', async () => { - const promptPromise = cli.confirm('yes/no?') + const promptPromise = CliUx.cli.confirm('yes/no?') const answer = await promptPromise - await cli.done() + await CliUx.cli.done() expect(answer).to.equal(false) }) @@ -46,9 +46,9 @@ describe('prompt', () => { .stderr() .stdin('x') .end('gets anykey', async () => { - const promptPromise = cli.anykey() + const promptPromise = CliUx.cli.anykey() const answer = await promptPromise - await cli.done() + await CliUx.cli.done() expect(answer).to.equal('x') }) @@ -56,12 +56,12 @@ describe('prompt', () => { .stdout() .stderr() .end('does not require input', async () => { - const promptPromise = cli.prompt('Require input?', { + const promptPromise = CliUx.cli.prompt('Require input?', { required: false, }) process.stdin.emit('data', '') const answer = await promptPromise - await cli.done() + await CliUx.cli.done() expect(answer).to.equal('') }) @@ -69,7 +69,7 @@ describe('prompt', () => { .stdout() .stderr() .it('timeouts with no input', async () => { - await expect(cli.prompt('Require input?', {timeout: 1})) + await expect(CliUx.cli.prompt('Require input?', {timeout: 1})) .to.eventually.be.rejectedWith('Prompt timeout') }) }) diff --git a/test/cli-ux/styled/object.test.ts b/test/cli-ux/styled/object.test.ts index 21819d527..0b6e672df 100644 --- a/test/cli-ux/styled/object.test.ts +++ b/test/cli-ux/styled/object.test.ts @@ -1,12 +1,12 @@ import {expect, fancy} from 'fancy-test' -import {cli} from '../../../src/cli-ux' +import {CliUx} from '../../../src/cli-ux' describe('styled/object', () => { fancy .stdout() .end('shows a table', output => { - cli.styledObject([ + CliUx.cli.styledObject([ {foo: 1, bar: 1}, {foo: 2, bar: 2}, {foo: 3, bar: 3}, diff --git a/test/cli-ux/styled/progress.test.ts b/test/cli-ux/styled/progress.test.ts index 76efc58f8..507effd9a 100644 --- a/test/cli-ux/styled/progress.test.ts +++ b/test/cli-ux/styled/progress.test.ts @@ -1,13 +1,13 @@ import {expect, fancy} from 'fancy-test' // import {BarType, Progress} from '../../src/progress' -import {cli} from '../../../src/cli-ux' +import {CliUx} from '../../../src/cli-ux' describe('progress', () => { // single bar fancy .end('single bar has default settings', _ => { - const b1 = cli.progress({format: 'Example 1: Progress {bar} | {percentage}%'}) + const b1 = CliUx.cli.progress({format: 'Example 1: Progress {bar} | {percentage}%'}) expect(b1.options.format).to.contain('Example 1: Progress') expect(b1.bars).to.not.have }) @@ -15,7 +15,7 @@ describe('progress', () => { // testing no settings passed, default settings created fancy .end('single bar, no bars array', _ => { - const b1 = cli.progress({}) + const b1 = CliUx.cli.progress({}) expect(b1.options.format).to.contain('progress') expect(b1.bars).to.not.have expect(b1.options.noTTYOutput).to.not.be.null @@ -23,7 +23,7 @@ describe('progress', () => { // testing getProgressBar returns correct type fancy .end('typeof progress bar is object', _ => { - const b1 = cli.progress({format: 'Example 1: Progress {bar} | {percentage}%'}) + const b1 = CliUx.cli.progress({format: 'Example 1: Progress {bar} | {percentage}%'}) expect(typeof (b1)).to.equal('object') }) }) diff --git a/test/cli-ux/styled/table.test.ts b/test/cli-ux/styled/table.test.ts index 3f9636211..0898eae18 100644 --- a/test/cli-ux/styled/table.test.ts +++ b/test/cli-ux/styled/table.test.ts @@ -1,6 +1,6 @@ import {expect, fancy} from 'fancy-test' -import {cli} from '../../../src/cli-ux' +import {CliUx} from '../../../src/cli-ux' /* eslint-disable camelcase */ const apps = [ @@ -71,13 +71,13 @@ const extendedHeader = `ID Name${ws.padEnd(14)}Web url${ws.padEnd(34)}Stack${ws describe('styled/table', () => { fancy .end('export flags and display()', () => { - expect(typeof (cli.table.flags())).to.eq('object') - expect(typeof (cli.table)).to.eq('function') + expect(typeof (CliUx.cli.table.flags())).to.eq('object') + expect(typeof (CliUx.cli.table)).to.eq('function') }) fancy .end('has optional flags', _ => { - const flags = cli.table.flags() + const flags = CliUx.cli.table.flags() expect(flags.columns).to.exist expect(flags.sort).to.exist expect(flags.filter).to.exist @@ -91,7 +91,7 @@ describe('styled/table', () => { fancy .stdout() .end('displays table', output => { - cli.table(apps, columns) + CliUx.cli.table(apps, columns) expect(output.stdout).to.equal(` ID Name${ws.padEnd(14)} ─── ─────────────────${ws} 123 supertable-test-1${ws} @@ -102,14 +102,14 @@ describe('styled/table', () => { fancy .stdout() .end('use header value for id', output => { - cli.table(apps, columns) + CliUx.cli.table(apps, columns) expect(output.stdout.slice(1, 3)).to.equal('ID') }) fancy .stdout() .end('shows extended columns/uses get() for value', output => { - cli.table(apps, columns, {extended: true}) + CliUx.cli.table(apps, columns, {extended: true}) expect(output.stdout).to.equal(`${ws}${extendedHeader} ─── ───────────────── ──────────────────────────────────────── ─────────${ws} 123 supertable-test-1 https://supertable-test-1.herokuapp.com/ heroku-16${ws} @@ -121,14 +121,14 @@ describe('styled/table', () => { fancy .stdout() .end('shows extended columns', output => { - cli.table(apps, columns, {extended: true}) + CliUx.cli.table(apps, columns, {extended: true}) expect(output.stdout).to.contain(extendedHeader) }) fancy .stdout() .end('shows title with divider', output => { - cli.table(apps, columns, {title: 'testing'}) + CliUx.cli.table(apps, columns, {title: 'testing'}) expect(output.stdout).to.equal(`testing ======================= | ID Name${ws.padEnd(14)} @@ -140,7 +140,7 @@ describe('styled/table', () => { fancy .stdout() .end('skips header', output => { - cli.table(apps, columns, {'no-header': true}) + CliUx.cli.table(apps, columns, {'no-header': true}) expect(output.stdout).to.equal(` 123 supertable-test-1${ws} 321 supertable-test-2${ws}\n`) }) @@ -148,7 +148,7 @@ describe('styled/table', () => { fancy .stdout() .end('only displays given columns', output => { - cli.table(apps, columns, {columns: 'id'}) + CliUx.cli.table(apps, columns, {columns: 'id'}) expect(output.stdout).to.equal(` ID${ws}${ws} ───${ws} 123${ws} @@ -158,7 +158,7 @@ describe('styled/table', () => { fancy .stdout() .end('outputs in csv', output => { - cli.table(apps, columns, {output: 'csv'}) + CliUx.cli.table(apps, columns, {output: 'csv'}) expect(output.stdout).to.equal(`ID,Name 123,supertable-test-1 321,supertable-test-2\n`) @@ -167,7 +167,7 @@ describe('styled/table', () => { fancy .stdout() .end('outputs in csv with escaped values', output => { - cli.table([ + CliUx.cli.table([ { id: '123\n2', name: 'supertable-test-1', @@ -195,7 +195,7 @@ describe('styled/table', () => { fancy .stdout() .end('outputs in csv without headers', output => { - cli.table(apps, columns, {output: 'csv', 'no-header': true}) + CliUx.cli.table(apps, columns, {output: 'csv', 'no-header': true}) expect(output.stdout).to.equal(`123,supertable-test-1 321,supertable-test-2\n`) }) @@ -203,7 +203,7 @@ describe('styled/table', () => { fancy .stdout() .end('outputs in csv with alias flag', output => { - cli.table(apps, columns, {csv: true}) + CliUx.cli.table(apps, columns, {csv: true}) expect(output.stdout).to.equal(`ID,Name 123,supertable-test-1 321,supertable-test-2\n`) @@ -212,7 +212,7 @@ describe('styled/table', () => { fancy .stdout() .end('outputs in json', output => { - cli.table(apps, columns, {output: 'json'}) + CliUx.cli.table(apps, columns, {output: 'json'}) expect(output.stdout).to.equal(`[ { "id": "123", @@ -229,7 +229,7 @@ describe('styled/table', () => { fancy .stdout() .end('outputs in yaml', output => { - cli.table(apps, columns, {output: 'yaml'}) + CliUx.cli.table(apps, columns, {output: 'yaml'}) expect(output.stdout).to.equal(`- id: '123' name: supertable-test-1 - id: '321' @@ -241,7 +241,7 @@ describe('styled/table', () => { fancy .stdout() .end('sorts by property', output => { - cli.table(apps, columns, {sort: '-name'}) + CliUx.cli.table(apps, columns, {sort: '-name'}) expect(output.stdout).to.equal(` ID Name${ws.padEnd(14)} ─── ─────────────────${ws} 321 supertable-test-2${ws} @@ -251,7 +251,7 @@ describe('styled/table', () => { fancy .stdout() .end('filters by property & value (partial string match)', output => { - cli.table(apps, columns, {filter: 'id=123'}) + CliUx.cli.table(apps, columns, {filter: 'id=123'}) expect(output.stdout).to.equal(` ID Name${ws.padEnd(14)} ─── ─────────────────${ws} 123 supertable-test-1${ws}\n`) @@ -261,7 +261,7 @@ describe('styled/table', () => { .stdout() .end('does not truncate', output => { const three = {...apps[0], id: '0'.repeat(80), name: 'supertable-test-3'} - cli.table([...apps, three], columns, {filter: 'id=0', 'no-truncate': true}) + CliUx.cli.table([...apps, three], columns, {filter: 'id=0', 'no-truncate': true}) expect(output.stdout).to.equal(` ID${ws.padEnd(78)} Name${ws.padEnd(14)} ${''.padEnd(three.id.length, '─')} ─────────────────${ws} ${three.id} supertable-test-3${ws}\n`) @@ -271,14 +271,14 @@ describe('styled/table', () => { describe('#flags', () => { fancy .end('includes only flags', _ => { - const flags = cli.table.flags({only: 'columns'}) + const flags = CliUx.cli.table.flags({only: 'columns'}) expect(flags.columns).to.be.a('object') expect((flags as any).sort).to.be.undefined }) fancy .end('excludes except flags', _ => { - const flags = cli.table.flags({except: 'columns'}) + const flags = CliUx.cli.table.flags({except: 'columns'}) expect((flags as any).columns).to.be.undefined expect(flags.sort).to.be.a('object') }) @@ -288,7 +288,7 @@ describe('styled/table', () => { fancy .stdout() .end('ignores header case', output => { - cli.table(apps, columns, {columns: 'iD,Name', filter: 'nAMe=supertable-test', sort: '-ID'}) + CliUx.cli.table(apps, columns, {columns: 'iD,Name', filter: 'nAMe=supertable-test', sort: '-ID'}) expect(output.stdout).to.equal(` ID Name${ws.padEnd(14)} ─── ─────────────────${ws} 321 supertable-test-2${ws} @@ -309,7 +309,7 @@ describe('styled/table', () => { } /* eslint-enable camelcase */ - cli.table([...apps, app3 as any], columns, {sort: '-ID'}) + CliUx.cli.table([...apps, app3 as any], columns, {sort: '-ID'}) expect(output.stdout).to.equal(` ID Name${ws.padEnd(14)} ─── ─────────────────${ws} 456 supertable-test${ws.padEnd(3)} diff --git a/test/cli-ux/styled/tree.test.ts b/test/cli-ux/styled/tree.test.ts index cb602495c..7a4026442 100644 --- a/test/cli-ux/styled/tree.test.ts +++ b/test/cli-ux/styled/tree.test.ts @@ -1,16 +1,16 @@ import {expect, fancy} from 'fancy-test' -import {cli} from '../../../src/cli-ux' +import {CliUx} from '../../../src/cli-ux' describe('styled/tree', () => { fancy .stdout() .end('shows the tree', output => { - const tree = cli.tree() + const tree = CliUx.cli.tree() tree.insert('foo') tree.insert('bar') - const subtree = cli.tree() + const subtree = CliUx.cli.tree() subtree.insert('qux') tree.nodes.bar.insert('baz', subtree) From 8d2f1ecf542d522ce403eceac5259166969259c5 Mon Sep 17 00:00:00 2001 From: Peter Hale Date: Tue, 18 Jan 2022 15:16:25 -0700 Subject: [PATCH 10/28] chore: remove unneeded import --- src/cli-ux/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/cli-ux/index.ts b/src/cli-ux/index.ts index d2d855062..7452f2658 100644 --- a/src/cli-ux/index.ts +++ b/src/cli-ux/index.ts @@ -4,7 +4,6 @@ import * as util from 'util' import * as uxConfig from './config' import deps from './deps' import * as Table from './styled/table' -import {CliUx as BaseCliUx} from './action/base' const hyperlinker = require('hyperlinker') From e7eb16d7d3ae54f280ce48516f7d916f5228be5e Mon Sep 17 00:00:00 2001 From: Peter Hale Date: Wed, 19 Jan 2022 08:48:53 -0700 Subject: [PATCH 11/28] Revert "chore: remove unneeded import" This reverts commit 8d2f1ecf542d522ce403eceac5259166969259c5. --- src/cli-ux/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cli-ux/index.ts b/src/cli-ux/index.ts index 7452f2658..d2d855062 100644 --- a/src/cli-ux/index.ts +++ b/src/cli-ux/index.ts @@ -4,6 +4,7 @@ import * as util from 'util' import * as uxConfig from './config' import deps from './deps' import * as Table from './styled/table' +import {CliUx as BaseCliUx} from './action/base' const hyperlinker = require('hyperlinker') From dab5aaf3675b88373078de1d78762e0683be4aee Mon Sep 17 00:00:00 2001 From: Peter Hale Date: Wed, 19 Jan 2022 08:49:04 -0700 Subject: [PATCH 12/28] Revert "chore: wrap cli-ux exported members in namespace" This reverts commit 905e5d5a2d8f4542536a9ea561181b5bbf5322c1. --- src/cli-ux/action/base.ts | 304 ++++++++++++++-------------- src/cli-ux/action/simple.ts | 6 +- src/cli-ux/action/spinner.ts | 7 +- src/cli-ux/config.ts | 57 +++--- src/cli-ux/deps.ts | 1 - src/cli-ux/exit.ts | 26 ++- src/cli-ux/index.ts | 209 +++++++++---------- src/cli-ux/prompt.ts | 113 +++++------ src/cli-ux/styled/json.ts | 6 +- src/command.ts | 8 +- test/cli-ux/fancy.ts | 4 +- test/cli-ux/index.test.ts | 4 +- test/cli-ux/prompt.test.ts | 24 +-- test/cli-ux/styled/object.test.ts | 4 +- test/cli-ux/styled/progress.test.ts | 8 +- test/cli-ux/styled/table.test.ts | 48 ++--- test/cli-ux/styled/tree.test.ts | 6 +- 17 files changed, 414 insertions(+), 421 deletions(-) diff --git a/src/cli-ux/action/base.ts b/src/cli-ux/action/base.ts index a93f7161b..cfb51b50b 100644 --- a/src/cli-ux/action/base.ts +++ b/src/cli-ux/action/base.ts @@ -1,203 +1,201 @@ import castArray from 'lodash/castArray' import {inspect} from 'util' -export namespace CliUx { - export interface ITask { - action: string; - status: string | undefined; - active: boolean; - } - export type ActionType = 'spinner' | 'simple' | 'debug' - export interface Options { - stdout?: boolean; - } +export interface ITask { + action: string; + status: string | undefined; + active: boolean; } -export namespace CliUx { - export class ActionBase { - type!: ActionType +export type ActionType = 'spinner' | 'simple' | 'debug' - std: 'stdout' | 'stderr' = 'stderr' +export interface Options { + stdout?: boolean; +} - protected stdmocks?: ['stdout' | 'stderr', string[]][] +export class ActionBase { + type!: ActionType - private stdmockOrigs = { - stdout: process.stdout.write, - stderr: process.stderr.write, - } + std: 'stdout' | 'stderr' = 'stderr' - public start(action: string, status?: string, opts: Options = {}) { - this.std = opts.stdout ? 'stdout' : 'stderr' - const task = {action, status, active: Boolean(this.task && this.task.active)} - this.task = task + protected stdmocks?: ['stdout' | 'stderr', string[]][] - this._start() - task.active = true - this._stdout(true) - } + private stdmockOrigs = { + stdout: process.stdout.write, + stderr: process.stderr.write, + } - public stop(msg = 'done') { - const task = this.task - if (!task) { - return - } + public start(action: string, status?: string, opts: Options = {}) { + this.std = opts.stdout ? 'stdout' : 'stderr' + const task = {action, status, active: Boolean(this.task && this.task.active)} + this.task = task - this._stop(msg) - task.active = false - this.task = undefined - this._stdout(false) - } + this._start() + task.active = true + this._stdout(true) + } - private get globals(): { action: { task?: ITask }; output: string | undefined } { - (global as any)['cli-ux'] = (global as any)['cli-ux'] || {} - const globals = (global as any)['cli-ux'] - globals.action = globals.action || {} - return globals + public stop(msg = 'done') { + const task = this.task + if (!task) { + return } - public get task(): ITask | undefined { - return this.globals.action.task - } + this._stop(msg) + task.active = false + this.task = undefined + this._stdout(false) + } - public set task(task: ITask | undefined) { - this.globals.action.task = task - } + private get globals(): { action: { task?: ITask }; output: string | undefined } { + (global as any)['cli-ux'] = (global as any)['cli-ux'] || {} + const globals = (global as any)['cli-ux'] + globals.action = globals.action || {} + return globals + } - protected get output(): string | undefined { - return this.globals.output - } + public get task(): ITask | undefined { + return this.globals.action.task + } - protected set output(output: string | undefined) { - this.globals.output = output - } + public set task(task: ITask | undefined) { + this.globals.action.task = task + } - get running(): boolean { - return Boolean(this.task) - } + protected get output(): string | undefined { + return this.globals.output + } - get status(): string | undefined { - return this.task ? this.task.status : undefined - } + protected set output(output: string | undefined) { + this.globals.output = output + } - set status(status: string | undefined) { - const task = this.task - if (!task) { - return - } + get running(): boolean { + return Boolean(this.task) + } - if (task.status === status) { - return - } + get status(): string | undefined { + return this.task ? this.task.status : undefined + } - this._updateStatus(status, task.status) - task.status = status + set status(status: string | undefined) { + const task = this.task + if (!task) { + return } - public async pauseAsync(fn: () => Promise, icon?: string) { - const task = this.task - const active = task && task.active - if (task && active) { - this._pause(icon) - this._stdout(false) - task.active = false - } + if (task.status === status) { + return + } - const ret = await fn() - if (task && active) { - this._resume() - } + this._updateStatus(status, task.status) + task.status = status + } - return ret + public async pauseAsync(fn: () => Promise, icon?: string) { + const task = this.task + const active = task && task.active + if (task && active) { + this._pause(icon) + this._stdout(false) + task.active = false } - public pause(fn: () => any, icon?: string) { - const task = this.task - const active = task && task.active - if (task && active) { - this._pause(icon) - this._stdout(false) - task.active = false - } + const ret = await fn() + if (task && active) { + this._resume() + } - const ret = fn() - if (task && active) { - this._resume() - } + return ret + } - return ret + public pause(fn: () => any, icon?: string) { + const task = this.task + const active = task && task.active + if (task && active) { + this._pause(icon) + this._stdout(false) + task.active = false } - protected _start() { - throw new Error('not implemented') + const ret = fn() + if (task && active) { + this._resume() } - protected _stop(_: string) { - throw new Error('not implemented') - } + return ret + } - protected _resume() { - if (this.task) this.start(this.task.action, this.task.status) - } + protected _start() { + throw new Error('not implemented') + } - protected _pause(_?: string) { - throw new Error('not implemented') - } + protected _stop(_: string) { + throw new Error('not implemented') + } - protected _updateStatus(_: string | undefined, __?: string) {} - - // mock out stdout/stderr so it doesn't screw up the rendering - protected _stdout(toggle: boolean) { - try { - const outputs: ['stdout', 'stderr'] = ['stdout', 'stderr'] - if (toggle) { - if (this.stdmocks) return - this.stdmockOrigs = { - stdout: process.stdout.write, - stderr: process.stderr.write, - } + protected _resume() { + if (this.task) this.start(this.task.action, this.task.status) + } + + protected _pause(_?: string) { + throw new Error('not implemented') + } + + protected _updateStatus(_: string | undefined, __?: string) {} + + // mock out stdout/stderr so it doesn't screw up the rendering + protected _stdout(toggle: boolean) { + try { + const outputs: ['stdout', 'stderr'] = ['stdout', 'stderr'] + if (toggle) { + if (this.stdmocks) return + this.stdmockOrigs = { + stdout: process.stdout.write, + stderr: process.stderr.write, + } - this.stdmocks = [] - for (const std of outputs) { - (process[std] as any).write = (...args: any[]) => { - this.stdmocks!.push([std, args] as ['stdout' | 'stderr', string[]]) - } + this.stdmocks = [] + for (const std of outputs) { + (process[std] as any).write = (...args: any[]) => { + this.stdmocks!.push([std, args] as ['stdout' | 'stderr', string[]]) } - } else { - if (!this.stdmocks) return - // this._write('stderr', '\nresetstdmock\n\n\n') - delete this.stdmocks - for (const std of outputs) process[std].write = this.stdmockOrigs[std] as any } - } catch (error) { - this._write('stderr', inspect(error)) + } else { + if (!this.stdmocks) return + // this._write('stderr', '\nresetstdmock\n\n\n') + delete this.stdmocks + for (const std of outputs) process[std].write = this.stdmockOrigs[std] as any } + } catch (error) { + this._write('stderr', inspect(error)) } + } - // flush mocked stdout/stderr - protected _flushStdout() { - try { - let output = '' - let std: 'stdout' | 'stderr' | undefined - while (this.stdmocks && this.stdmocks.length > 0) { - const cur = this.stdmocks.shift() as ['stdout' | 'stderr', string[]] - std = cur[0] - this._write(std, cur[1]) - output += (cur[1][0] as any).toString('utf8') - } - // add newline if there isn't one already - // otherwise we'll just overwrite it when we render + // flush mocked stdout/stderr + protected _flushStdout() { + try { + let output = '' + let std: 'stdout' | 'stderr' | undefined + while (this.stdmocks && this.stdmocks.length > 0) { + const cur = this.stdmocks.shift() as ['stdout' | 'stderr', string[]] + std = cur[0] + this._write(std, cur[1]) + output += (cur[1][0] as any).toString('utf8') + } + // add newline if there isn't one already + // otherwise we'll just overwrite it when we render - if (output && std && output[output.length - 1] !== '\n') { - this._write(std, '\n') - } - } catch (error) { - this._write('stderr', inspect(error)) + if (output && std && output[output.length - 1] !== '\n') { + this._write(std, '\n') } + } catch (error) { + this._write('stderr', inspect(error)) } + } - // write to the real stdout/stderr - protected _write(std: 'stdout' | 'stderr', s: string | string[]) { - this.stdmockOrigs[std].apply(process[std], castArray(s) as [string]) - } + // write to the real stdout/stderr + protected _write(std: 'stdout' | 'stderr', s: string | string[]) { + this.stdmockOrigs[std].apply(process[std], castArray(s) as [string]) } } diff --git a/src/cli-ux/action/simple.ts b/src/cli-ux/action/simple.ts index 223eaeeb8..48722ae32 100644 --- a/src/cli-ux/action/simple.ts +++ b/src/cli-ux/action/simple.ts @@ -1,7 +1,7 @@ -import {CliUx} from './base' +import {ActionBase, ActionType} from './base' -export default class SimpleAction extends CliUx.ActionBase { - public type: CliUx.ActionType = 'simple' +export default class SimpleAction extends ActionBase { + public type: ActionType = 'simple' protected _start() { const task = this.task diff --git a/src/cli-ux/action/spinner.ts b/src/cli-ux/action/spinner.ts index ba08ce475..936d602c2 100644 --- a/src/cli-ux/action/spinner.ts +++ b/src/cli-ux/action/spinner.ts @@ -5,17 +5,18 @@ import * as supportsColor from 'supports-color' import deps from '../deps' -import {CliUx} from './base' +import {ActionBase, ActionType} from './base' /* eslint-disable-next-line node/no-missing-require */ const spinners = require('./spinners') + function color(s: string): string { if (!supportsColor) return s const has256 = supportsColor.stdout ? supportsColor.stdout.has256 : (process.env.TERM || '').includes('256') return has256 ? `\u001B[38;5;104m${s}${deps.ansiStyles.reset.open}` : chalk.magenta(s) } -export default class SpinnerAction extends CliUx.ActionBase { - public type: CliUx.ActionType = 'spinner' +export default class SpinnerAction extends ActionBase { + public type: ActionType = 'spinner' spinner?: NodeJS.Timeout diff --git a/src/cli-ux/config.ts b/src/cli-ux/config.ts index a17ae1554..e0a110e1c 100644 --- a/src/cli-ux/config.ts +++ b/src/cli-ux/config.ts @@ -1,18 +1,17 @@ import * as semver from 'semver' -import {CliUx as AB} from './action/base' +import {ActionBase} from './action/base' const version = semver.parse(require('../../package.json').version)! export type Levels = 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'trace' -export namespace CliUx { - export interface ConfigMessage { - type: 'config'; - prop: string; - value: any; - } +export interface ConfigMessage { + type: 'config'; + prop: string; + value: any; } + const g: any = global const globals = g['cli-ux'] || (g['cli-ux'] = {}) @@ -28,41 +27,39 @@ const Action = actionType === 'spinner' ? require('./action/spinner').default : const PrideAction = actionType === 'spinner' ? require('./action/pride-spinner').default : require('./action/simple').default /* eslint-enable node/no-missing-require */ -export namespace CliUx { - export class Config { - outputLevel: Levels = 'info' +export class UxConfig { + outputLevel: Levels = 'info' - action: AB.ActionBase = new Action() + action: ActionBase = new Action() - prideAction: AB.ActionBase = new PrideAction() + prideAction: ActionBase = new PrideAction() - errorsHandled = false + errorsHandled = false - showStackTrace = true + showStackTrace = true - get debug(): boolean { - return globals.debug || process.env.DEBUG === '*' - } + get debug(): boolean { + return globals.debug || process.env.DEBUG === '*' + } - set debug(v: boolean) { - globals.debug = v - } + set debug(v: boolean) { + globals.debug = v + } - get context(): any { - return globals.context || {} - } + get context(): any { + return globals.context || {} + } - set context(v: any) { - globals.context = v - } + set context(v: any) { + globals.context = v } } + function fetch() { if (globals[version.major]) return globals[version.major] - globals[version.major] = new CliUx.Config() + globals[version.major] = new UxConfig() return globals[version.major] } -export namespace CliUx { - export const config: Config = fetch() -} +export const config: UxConfig = fetch() +export default config diff --git a/src/cli-ux/deps.ts b/src/cli-ux/deps.ts index 02d0ff863..329486e84 100644 --- a/src/cli-ux/deps.ts +++ b/src/cli-ux/deps.ts @@ -1,5 +1,4 @@ /* eslint-disable node/no-missing-require */ - export default { get stripAnsi(): (string: string) => string { return require('strip-ansi') diff --git a/src/cli-ux/exit.ts b/src/cli-ux/exit.ts index 6174d0d4a..4f036df30 100644 --- a/src/cli-ux/exit.ts +++ b/src/cli-ux/exit.ts @@ -1,19 +1,17 @@ -export namespace CliUx { - export class ExitError extends Error { - public 'cli-ux': { - exit: number; - } +export class ExitError extends Error { + public 'cli-ux': { + exit: number; + } - public code: 'EEXIT' + public code: 'EEXIT' - public error?: Error + public error?: Error - constructor(status: number, error?: Error) { - const code = 'EEXIT' - super(error ? error.message : `${code}: ${status}`) - this.error = error - this['cli-ux'] = {exit: status} - this.code = code - } + constructor(status: number, error?: Error) { + const code = 'EEXIT' + super(error ? error.message : `${code}: ${status}`) + this.error = error + this['cli-ux'] = {exit: status} + this.code = code } } diff --git a/src/cli-ux/index.ts b/src/cli-ux/index.ts index d2d855062..68f56e395 100644 --- a/src/cli-ux/index.ts +++ b/src/cli-ux/index.ts @@ -1,10 +1,12 @@ import * as Errors from '../errors' import * as util from 'util' -import * as uxConfig from './config' +import {ActionBase} from './action/base' +import {config, UxConfig} from './config' import deps from './deps' +import {ExitError} from './exit' +import {IPromptOptions} from './prompt' import * as Table from './styled/table' -import {CliUx as BaseCliUx} from './action/base' const hyperlinker = require('hyperlinker') @@ -16,7 +18,7 @@ function timeout(p: Promise, ms: number) { }) } - return Promise.race([p, wait(ms, true).then(() => CliUx.ux.error('timed out'))]) + return Promise.race([p, wait(ms, true).then(() => ux.error('timed out'))]) } async function flush() { @@ -27,117 +29,118 @@ async function flush() { return p } -export namespace CliUx { - export const ux = { - config: uxConfig.CliUx.config, - warn: Errors.warn, - error: Errors.error, - exit: Errors.exit, - - get prompt() { - return deps.prompt.CliUx.prompt - }, - /** +export const ux = { + config, + + warn: Errors.warn, + error: Errors.error, + exit: Errors.exit, + + get prompt() { + return deps.prompt.prompt + }, + /** * "press anykey to continue" */ - get anykey() { - return deps.prompt.CliUx.anykey - }, - get confirm() { - return deps.prompt.CliUx.confirm - }, - get action() { - return uxConfig.CliUx.config.action - }, - get prideAction() { - return uxConfig.CliUx.config.prideAction - }, - styledObject(obj: any, keys?: string[]) { - ux.info(deps.styledObject(obj, keys)) - }, - get styledHeader() { - return deps.styledHeader - }, - get styledJSON() { - return deps.styledJSON - }, - get table() { - return deps.table - }, - get tree() { - return deps.tree - }, - get open() { - return deps.open - }, - get wait() { - return deps.wait - }, - get progress() { - return deps.progress - }, - - async done() { - uxConfig.CliUx.config.action.stop() - // await flushStdout() - }, - - trace(format: string, ...args: string[]) { - if (this.config.outputLevel === 'trace') { - process.stdout.write(util.format(format, ...args) + '\n') - } - }, - - debug(format: string, ...args: string[]) { - if (['trace', 'debug'].includes(this.config.outputLevel)) { - process.stdout.write(util.format(format, ...args) + '\n') - } - }, - - info(format: string, ...args: string[]) { + get anykey() { + return deps.prompt.anykey + }, + get confirm() { + return deps.prompt.confirm + }, + get action() { + return config.action + }, + get prideAction() { + return config.prideAction + }, + styledObject(obj: any, keys?: string[]) { + ux.info(deps.styledObject(obj, keys)) + }, + get styledHeader() { + return deps.styledHeader + }, + get styledJSON() { + return deps.styledJSON + }, + get table() { + return deps.table + }, + get tree() { + return deps.tree + }, + get open() { + return deps.open + }, + get wait() { + return deps.wait + }, + get progress() { + return deps.progress + }, + + async done() { + config.action.stop() + // await flushStdout() + }, + + trace(format: string, ...args: string[]) { + if (this.config.outputLevel === 'trace') { process.stdout.write(util.format(format, ...args) + '\n') - }, - - log(format?: string, ...args: string[]) { - this.info(format || '', ...args) - }, - - url(text: string, uri: string, params = {}) { - const supports = require('supports-hyperlinks') - if (supports.stdout) { - this.log(hyperlinker(text, uri, params)) - } else { - this.log(uri) - } - }, - - annotation(text: string, annotation: string) { - const supports = require('supports-hyperlinks') - if (supports.stdout) { - // \u001b]8;;https://google.com\u0007sometext\u001b]8;;\u0007 - this.log(`\u001B]1337;AddAnnotation=${text.length}|${annotation}\u0007${text}`) - } else { - this.log(text) - } - }, - - async flush() { - await timeout(flush(), 10_000) - }, - } + } + }, + + debug(format: string, ...args: string[]) { + if (['trace', 'debug'].includes(this.config.outputLevel)) { + process.stdout.write(util.format(format, ...args) + '\n') + } + }, + + info(format: string, ...args: string[]) { + process.stdout.write(util.format(format, ...args) + '\n') + }, + + log(format?: string, ...args: string[]) { + this.info(format || '', ...args) + }, + + url(text: string, uri: string, params = {}) { + const supports = require('supports-hyperlinks') + if (supports.stdout) { + this.log(hyperlinker(text, uri, params)) + } else { + this.log(uri) + } + }, + + annotation(text: string, annotation: string) { + const supports = require('supports-hyperlinks') + if (supports.stdout) { + // \u001b]8;;https://google.com\u0007sometext\u001b]8;;\u0007 + this.log(`\u001B]1337;AddAnnotation=${text.length}|${annotation}\u0007${text}`) + } else { + this.log(text) + } + }, + + async flush() { + await timeout(flush(), 10_000) + }, } +export const cli = ux export { + config, + ActionBase, + UxConfig, + ExitError, + IPromptOptions, Table, } -export namespace CliUx { - export const cli = ux -} - const cliuxProcessExitHandler = async () => { try { - await CliUx.ux.done() + await ux.done() } catch (error) { // tslint:disable no-console console.error(error) diff --git a/src/cli-ux/prompt.ts b/src/cli-ux/prompt.ts index d8ab06c73..ab4958622 100644 --- a/src/cli-ux/prompt.ts +++ b/src/cli-ux/prompt.ts @@ -1,19 +1,18 @@ import * as Errors from '../../src/errors' import * as chalk from 'chalk' -import * as config from './config' + +import config from './config' import deps from './deps' -export namespace CliUx { - export interface IPromptOptions { - prompt?: string; - type?: 'normal' | 'mask' | 'hide' | 'single'; - timeout?: number; - /** - * Requires user input if true, otherwise allows empty input - */ - required?: boolean; - default?: string; - } +export interface IPromptOptions { + prompt?: string; + type?: 'normal' | 'mask' | 'hide' | 'single'; + timeout?: number; + /** + * Requires user input if true, otherwise allows empty input + */ + required?: boolean; + default?: string; } interface IPromptConfig { @@ -81,7 +80,7 @@ function replacePrompt(prompt: string) { deps.ansiEscapes.cursorDown(1) + deps.ansiEscapes.cursorLeft + deps.ansiEscapes.cursorShow) } -function _prompt(name: string, inputOptions: Partial = {}): Promise { +function _prompt(name: string, inputOptions: Partial = {}): Promise { const prompt = getPrompt(name, inputOptions.type, inputOptions.default) const options: IPromptConfig = { isTTY: Boolean(process.env.TERM !== 'dumb' && process.stdin.isTTY), @@ -117,54 +116,52 @@ function _prompt(name: string, inputOptions: Partial = {}) } } -export namespace CliUx { - /** - * prompt for input - * @param name - prompt text - * @param options - @see IPromptOptions - * @returns void - */ - export function prompt(name: string, options: CliUx.IPromptOptions = {}) { - return config.CliUx.config.action.pauseAsync(() => { - return _prompt(name, options) - }, chalk.cyan('?')) - } - - /** - * confirmation prompt (yes/no) - * @param message - confirmation text - * @returns Promise - */ - export function confirm(message: string): Promise { - return config.CliUx.config.action.pauseAsync(async () => { - const confirm = async (): Promise => { - const response = (await _prompt(message)).toLowerCase() - if (['n', 'no'].includes(response)) return false - if (['y', 'yes'].includes(response)) return true - return confirm() - } +/** + * prompt for input + * @param name - prompt text + * @param options - @see IPromptOptions + * @returns void + */ +export function prompt(name: string, options: IPromptOptions = {}) { + return config.action.pauseAsync(() => { + return _prompt(name, options) + }, chalk.cyan('?')) +} +/** + * confirmation prompt (yes/no) + * @param message - confirmation text + * @returns Promise + */ +export function confirm(message: string): Promise { + return config.action.pauseAsync(async () => { + const confirm = async (): Promise => { + const response = (await _prompt(message)).toLowerCase() + if (['n', 'no'].includes(response)) return false + if (['y', 'yes'].includes(response)) return true return confirm() - }, chalk.cyan('?')) - } - - /** - * "press anykey to continue" - * @param message - optional message to display to user - * @returns Promise - */ - export async function anykey(message?: string): Promise { - const tty = Boolean(process.stdin.setRawMode) - if (!message) { - message = tty ? - `Press any key to continue or ${chalk.yellow('q')} to exit` : - `Press enter to continue or ${chalk.yellow('q')} to exit` } - const char = await prompt(message, {type: 'single', required: false}) - if (tty) process.stderr.write('\n') - if (char === 'q') Errors.error('quit') - if (char === '\u0003') Errors.error('ctrl-c') - return char + return confirm() + }, chalk.cyan('?')) +} + +/** + * "press anykey to continue" + * @param message - optional message to display to user + * @returns Promise + */ +export async function anykey(message?: string): Promise { + const tty = Boolean(process.stdin.setRawMode) + if (!message) { + message = tty ? + `Press any key to continue or ${chalk.yellow('q')} to exit` : + `Press enter to continue or ${chalk.yellow('q')} to exit` } + + const char = await prompt(message, {type: 'single', required: false}) + if (tty) process.stderr.write('\n') + if (char === 'q') Errors.error('quit') + if (char === '\u0003') Errors.error('ctrl-c') + return char } diff --git a/src/cli-ux/styled/json.ts b/src/cli-ux/styled/json.ts index 5ad9e0ecb..eec1748cd 100644 --- a/src/cli-ux/styled/json.ts +++ b/src/cli-ux/styled/json.ts @@ -2,16 +2,16 @@ import * as chalk from 'chalk' -import {CliUx} from '..' +import {cli} from '..' export default function styledJSON(obj: any) { const json = JSON.stringify(obj, null, 2) if (!chalk.level) { - CliUx.cli.info(json) + cli.info(json) return } const cardinal = require('cardinal') const theme = require('cardinal/themes/jq') - CliUx.cli.info(cardinal.highlight(json, {json: true, theme})) + cli.info(cardinal.highlight(json, {json: true, theme})) } diff --git a/src/command.ts b/src/command.ts index 79ae63566..10c17bcd3 100644 --- a/src/command.ts +++ b/src/command.ts @@ -1,7 +1,7 @@ import {fileURLToPath} from 'url' import {format, inspect} from 'util' -import {CliUx} from './' +import {cli} from './cli-ux' import {Config} from './config' import * as Interfaces from './interfaces' import * as Errors from './errors' @@ -163,7 +163,7 @@ export default abstract class Command { } if (result && this.jsonEnabled()) { - CliUx.cli.styledJSON(this.toSuccessJson(result)) + cli.styledJSON(this.toSuccessJson(result)) } return result @@ -227,12 +227,12 @@ export default abstract class Command { protected async catch(err: Record): Promise { process.exitCode = process.exitCode ?? err.exitCode ?? 1 if (this.jsonEnabled()) { - CliUx.cli.styledJSON(this.toErrorJson(err)) + cli.styledJSON(this.toErrorJson(err)) } else { if (!err.message) throw err try { const chalk = require('chalk') - CliUx.cli.action.stop(chalk.bold.red('!')) + cli.action.stop(chalk.bold.red('!')) } catch {} throw err diff --git a/test/cli-ux/fancy.ts b/test/cli-ux/fancy.ts index 38e705558..ea217fe5c 100644 --- a/test/cli-ux/fancy.ts +++ b/test/cli-ux/fancy.ts @@ -2,7 +2,7 @@ import {expect, fancy as base, FancyTypes} from 'fancy-test' import * as fs from 'fs-extra' import * as path from 'path' -import {CliUx} from '../../src/cli-ux' +import {cli} from '../../src/cli-ux' export { expect, @@ -20,5 +20,5 @@ export const fancy = base chalk.level = 0 }) .finally(async () => { - await CliUx.cli.done() + await cli.done() }) diff --git a/test/cli-ux/index.test.ts b/test/cli-ux/index.test.ts index 6d49da3cd..81f6b0d52 100644 --- a/test/cli-ux/index.test.ts +++ b/test/cli-ux/index.test.ts @@ -1,4 +1,4 @@ -import {CliUx} from '../../src/cli-ux' +import {ux} from '../../src/cli-ux' import {expect, fancy} from './fancy' const hyperlinker = require('hyperlinker') @@ -7,7 +7,7 @@ describe('url', () => { fancy .env({FORCE_HYPERLINK: '1'}, {clear: true}) .stdout() - .do(() => CliUx.ux.url('sometext', 'https://google.com')) + .do(() => ux.url('sometext', 'https://google.com')) .it('renders hyperlink', async ({stdout}) => { expect(stdout).to.equal('sometext\n') }) diff --git a/test/cli-ux/prompt.test.ts b/test/cli-ux/prompt.test.ts index d79c5dec4..2d1ff3e2a 100644 --- a/test/cli-ux/prompt.test.ts +++ b/test/cli-ux/prompt.test.ts @@ -2,7 +2,7 @@ import * as chai from 'chai' const expect = chai.expect -import {CliUx} from '../../src/cli-ux' +import {cli} from '../../src/cli-ux' import {fancy} from './fancy' @@ -11,11 +11,11 @@ describe('prompt', () => { .stdout() .stderr() .end('requires input', async () => { - const promptPromise = CliUx.cli.prompt('Require input?') + const promptPromise = cli.prompt('Require input?') process.stdin.emit('data', '') process.stdin.emit('data', 'answer') const answer = await promptPromise - await CliUx.cli.done() + await cli.done() expect(answer).to.equal('answer') }) @@ -24,9 +24,9 @@ describe('prompt', () => { .stderr() .stdin('y') .end('confirm', async () => { - const promptPromise = CliUx.cli.confirm('yes/no?') + const promptPromise = cli.confirm('yes/no?') const answer = await promptPromise - await CliUx.cli.done() + await cli.done() expect(answer).to.equal(true) }) @@ -35,9 +35,9 @@ describe('prompt', () => { .stderr() .stdin('n') .end('confirm', async () => { - const promptPromise = CliUx.cli.confirm('yes/no?') + const promptPromise = cli.confirm('yes/no?') const answer = await promptPromise - await CliUx.cli.done() + await cli.done() expect(answer).to.equal(false) }) @@ -46,9 +46,9 @@ describe('prompt', () => { .stderr() .stdin('x') .end('gets anykey', async () => { - const promptPromise = CliUx.cli.anykey() + const promptPromise = cli.anykey() const answer = await promptPromise - await CliUx.cli.done() + await cli.done() expect(answer).to.equal('x') }) @@ -56,12 +56,12 @@ describe('prompt', () => { .stdout() .stderr() .end('does not require input', async () => { - const promptPromise = CliUx.cli.prompt('Require input?', { + const promptPromise = cli.prompt('Require input?', { required: false, }) process.stdin.emit('data', '') const answer = await promptPromise - await CliUx.cli.done() + await cli.done() expect(answer).to.equal('') }) @@ -69,7 +69,7 @@ describe('prompt', () => { .stdout() .stderr() .it('timeouts with no input', async () => { - await expect(CliUx.cli.prompt('Require input?', {timeout: 1})) + await expect(cli.prompt('Require input?', {timeout: 1})) .to.eventually.be.rejectedWith('Prompt timeout') }) }) diff --git a/test/cli-ux/styled/object.test.ts b/test/cli-ux/styled/object.test.ts index 0b6e672df..21819d527 100644 --- a/test/cli-ux/styled/object.test.ts +++ b/test/cli-ux/styled/object.test.ts @@ -1,12 +1,12 @@ import {expect, fancy} from 'fancy-test' -import {CliUx} from '../../../src/cli-ux' +import {cli} from '../../../src/cli-ux' describe('styled/object', () => { fancy .stdout() .end('shows a table', output => { - CliUx.cli.styledObject([ + cli.styledObject([ {foo: 1, bar: 1}, {foo: 2, bar: 2}, {foo: 3, bar: 3}, diff --git a/test/cli-ux/styled/progress.test.ts b/test/cli-ux/styled/progress.test.ts index 507effd9a..76efc58f8 100644 --- a/test/cli-ux/styled/progress.test.ts +++ b/test/cli-ux/styled/progress.test.ts @@ -1,13 +1,13 @@ import {expect, fancy} from 'fancy-test' // import {BarType, Progress} from '../../src/progress' -import {CliUx} from '../../../src/cli-ux' +import {cli} from '../../../src/cli-ux' describe('progress', () => { // single bar fancy .end('single bar has default settings', _ => { - const b1 = CliUx.cli.progress({format: 'Example 1: Progress {bar} | {percentage}%'}) + const b1 = cli.progress({format: 'Example 1: Progress {bar} | {percentage}%'}) expect(b1.options.format).to.contain('Example 1: Progress') expect(b1.bars).to.not.have }) @@ -15,7 +15,7 @@ describe('progress', () => { // testing no settings passed, default settings created fancy .end('single bar, no bars array', _ => { - const b1 = CliUx.cli.progress({}) + const b1 = cli.progress({}) expect(b1.options.format).to.contain('progress') expect(b1.bars).to.not.have expect(b1.options.noTTYOutput).to.not.be.null @@ -23,7 +23,7 @@ describe('progress', () => { // testing getProgressBar returns correct type fancy .end('typeof progress bar is object', _ => { - const b1 = CliUx.cli.progress({format: 'Example 1: Progress {bar} | {percentage}%'}) + const b1 = cli.progress({format: 'Example 1: Progress {bar} | {percentage}%'}) expect(typeof (b1)).to.equal('object') }) }) diff --git a/test/cli-ux/styled/table.test.ts b/test/cli-ux/styled/table.test.ts index 0898eae18..3f9636211 100644 --- a/test/cli-ux/styled/table.test.ts +++ b/test/cli-ux/styled/table.test.ts @@ -1,6 +1,6 @@ import {expect, fancy} from 'fancy-test' -import {CliUx} from '../../../src/cli-ux' +import {cli} from '../../../src/cli-ux' /* eslint-disable camelcase */ const apps = [ @@ -71,13 +71,13 @@ const extendedHeader = `ID Name${ws.padEnd(14)}Web url${ws.padEnd(34)}Stack${ws describe('styled/table', () => { fancy .end('export flags and display()', () => { - expect(typeof (CliUx.cli.table.flags())).to.eq('object') - expect(typeof (CliUx.cli.table)).to.eq('function') + expect(typeof (cli.table.flags())).to.eq('object') + expect(typeof (cli.table)).to.eq('function') }) fancy .end('has optional flags', _ => { - const flags = CliUx.cli.table.flags() + const flags = cli.table.flags() expect(flags.columns).to.exist expect(flags.sort).to.exist expect(flags.filter).to.exist @@ -91,7 +91,7 @@ describe('styled/table', () => { fancy .stdout() .end('displays table', output => { - CliUx.cli.table(apps, columns) + cli.table(apps, columns) expect(output.stdout).to.equal(` ID Name${ws.padEnd(14)} ─── ─────────────────${ws} 123 supertable-test-1${ws} @@ -102,14 +102,14 @@ describe('styled/table', () => { fancy .stdout() .end('use header value for id', output => { - CliUx.cli.table(apps, columns) + cli.table(apps, columns) expect(output.stdout.slice(1, 3)).to.equal('ID') }) fancy .stdout() .end('shows extended columns/uses get() for value', output => { - CliUx.cli.table(apps, columns, {extended: true}) + cli.table(apps, columns, {extended: true}) expect(output.stdout).to.equal(`${ws}${extendedHeader} ─── ───────────────── ──────────────────────────────────────── ─────────${ws} 123 supertable-test-1 https://supertable-test-1.herokuapp.com/ heroku-16${ws} @@ -121,14 +121,14 @@ describe('styled/table', () => { fancy .stdout() .end('shows extended columns', output => { - CliUx.cli.table(apps, columns, {extended: true}) + cli.table(apps, columns, {extended: true}) expect(output.stdout).to.contain(extendedHeader) }) fancy .stdout() .end('shows title with divider', output => { - CliUx.cli.table(apps, columns, {title: 'testing'}) + cli.table(apps, columns, {title: 'testing'}) expect(output.stdout).to.equal(`testing ======================= | ID Name${ws.padEnd(14)} @@ -140,7 +140,7 @@ describe('styled/table', () => { fancy .stdout() .end('skips header', output => { - CliUx.cli.table(apps, columns, {'no-header': true}) + cli.table(apps, columns, {'no-header': true}) expect(output.stdout).to.equal(` 123 supertable-test-1${ws} 321 supertable-test-2${ws}\n`) }) @@ -148,7 +148,7 @@ describe('styled/table', () => { fancy .stdout() .end('only displays given columns', output => { - CliUx.cli.table(apps, columns, {columns: 'id'}) + cli.table(apps, columns, {columns: 'id'}) expect(output.stdout).to.equal(` ID${ws}${ws} ───${ws} 123${ws} @@ -158,7 +158,7 @@ describe('styled/table', () => { fancy .stdout() .end('outputs in csv', output => { - CliUx.cli.table(apps, columns, {output: 'csv'}) + cli.table(apps, columns, {output: 'csv'}) expect(output.stdout).to.equal(`ID,Name 123,supertable-test-1 321,supertable-test-2\n`) @@ -167,7 +167,7 @@ describe('styled/table', () => { fancy .stdout() .end('outputs in csv with escaped values', output => { - CliUx.cli.table([ + cli.table([ { id: '123\n2', name: 'supertable-test-1', @@ -195,7 +195,7 @@ describe('styled/table', () => { fancy .stdout() .end('outputs in csv without headers', output => { - CliUx.cli.table(apps, columns, {output: 'csv', 'no-header': true}) + cli.table(apps, columns, {output: 'csv', 'no-header': true}) expect(output.stdout).to.equal(`123,supertable-test-1 321,supertable-test-2\n`) }) @@ -203,7 +203,7 @@ describe('styled/table', () => { fancy .stdout() .end('outputs in csv with alias flag', output => { - CliUx.cli.table(apps, columns, {csv: true}) + cli.table(apps, columns, {csv: true}) expect(output.stdout).to.equal(`ID,Name 123,supertable-test-1 321,supertable-test-2\n`) @@ -212,7 +212,7 @@ describe('styled/table', () => { fancy .stdout() .end('outputs in json', output => { - CliUx.cli.table(apps, columns, {output: 'json'}) + cli.table(apps, columns, {output: 'json'}) expect(output.stdout).to.equal(`[ { "id": "123", @@ -229,7 +229,7 @@ describe('styled/table', () => { fancy .stdout() .end('outputs in yaml', output => { - CliUx.cli.table(apps, columns, {output: 'yaml'}) + cli.table(apps, columns, {output: 'yaml'}) expect(output.stdout).to.equal(`- id: '123' name: supertable-test-1 - id: '321' @@ -241,7 +241,7 @@ describe('styled/table', () => { fancy .stdout() .end('sorts by property', output => { - CliUx.cli.table(apps, columns, {sort: '-name'}) + cli.table(apps, columns, {sort: '-name'}) expect(output.stdout).to.equal(` ID Name${ws.padEnd(14)} ─── ─────────────────${ws} 321 supertable-test-2${ws} @@ -251,7 +251,7 @@ describe('styled/table', () => { fancy .stdout() .end('filters by property & value (partial string match)', output => { - CliUx.cli.table(apps, columns, {filter: 'id=123'}) + cli.table(apps, columns, {filter: 'id=123'}) expect(output.stdout).to.equal(` ID Name${ws.padEnd(14)} ─── ─────────────────${ws} 123 supertable-test-1${ws}\n`) @@ -261,7 +261,7 @@ describe('styled/table', () => { .stdout() .end('does not truncate', output => { const three = {...apps[0], id: '0'.repeat(80), name: 'supertable-test-3'} - CliUx.cli.table([...apps, three], columns, {filter: 'id=0', 'no-truncate': true}) + cli.table([...apps, three], columns, {filter: 'id=0', 'no-truncate': true}) expect(output.stdout).to.equal(` ID${ws.padEnd(78)} Name${ws.padEnd(14)} ${''.padEnd(three.id.length, '─')} ─────────────────${ws} ${three.id} supertable-test-3${ws}\n`) @@ -271,14 +271,14 @@ describe('styled/table', () => { describe('#flags', () => { fancy .end('includes only flags', _ => { - const flags = CliUx.cli.table.flags({only: 'columns'}) + const flags = cli.table.flags({only: 'columns'}) expect(flags.columns).to.be.a('object') expect((flags as any).sort).to.be.undefined }) fancy .end('excludes except flags', _ => { - const flags = CliUx.cli.table.flags({except: 'columns'}) + const flags = cli.table.flags({except: 'columns'}) expect((flags as any).columns).to.be.undefined expect(flags.sort).to.be.a('object') }) @@ -288,7 +288,7 @@ describe('styled/table', () => { fancy .stdout() .end('ignores header case', output => { - CliUx.cli.table(apps, columns, {columns: 'iD,Name', filter: 'nAMe=supertable-test', sort: '-ID'}) + cli.table(apps, columns, {columns: 'iD,Name', filter: 'nAMe=supertable-test', sort: '-ID'}) expect(output.stdout).to.equal(` ID Name${ws.padEnd(14)} ─── ─────────────────${ws} 321 supertable-test-2${ws} @@ -309,7 +309,7 @@ describe('styled/table', () => { } /* eslint-enable camelcase */ - CliUx.cli.table([...apps, app3 as any], columns, {sort: '-ID'}) + cli.table([...apps, app3 as any], columns, {sort: '-ID'}) expect(output.stdout).to.equal(` ID Name${ws.padEnd(14)} ─── ─────────────────${ws} 456 supertable-test${ws.padEnd(3)} diff --git a/test/cli-ux/styled/tree.test.ts b/test/cli-ux/styled/tree.test.ts index 7a4026442..cb602495c 100644 --- a/test/cli-ux/styled/tree.test.ts +++ b/test/cli-ux/styled/tree.test.ts @@ -1,16 +1,16 @@ import {expect, fancy} from 'fancy-test' -import {CliUx} from '../../../src/cli-ux' +import {cli} from '../../../src/cli-ux' describe('styled/tree', () => { fancy .stdout() .end('shows the tree', output => { - const tree = CliUx.cli.tree() + const tree = cli.tree() tree.insert('foo') tree.insert('bar') - const subtree = CliUx.cli.tree() + const subtree = cli.tree() subtree.insert('qux') tree.nodes.bar.insert('baz', subtree) From 8ec730318fc16e0b61f6be2baab3b93087121ba1 Mon Sep 17 00:00:00 2001 From: Peter Hale Date: Wed, 19 Jan 2022 11:09:56 -0700 Subject: [PATCH 13/28] chore: rework cliux namespace export --- src/cli-ux/config.ts | 6 ++-- src/cli-ux/index.ts | 5 ++- src/cli-ux/styled/json.ts | 6 ++-- src/command.ts | 8 ++--- src/index.ts | 13 +++++++- test/cli-ux/fancy.ts | 4 +-- test/cli-ux/prompt.test.ts | 24 +++++++-------- test/cli-ux/styled/object.test.ts | 4 +-- test/cli-ux/styled/progress.test.ts | 10 +++--- test/cli-ux/styled/table.test.ts | 48 ++++++++++++++--------------- test/cli-ux/styled/tree.test.ts | 6 ++-- 11 files changed, 71 insertions(+), 63 deletions(-) diff --git a/src/cli-ux/config.ts b/src/cli-ux/config.ts index e0a110e1c..e3cb40f13 100644 --- a/src/cli-ux/config.ts +++ b/src/cli-ux/config.ts @@ -27,7 +27,7 @@ const Action = actionType === 'spinner' ? require('./action/spinner').default : const PrideAction = actionType === 'spinner' ? require('./action/pride-spinner').default : require('./action/simple').default /* eslint-enable node/no-missing-require */ -export class UxConfig { +export class Config { outputLevel: Levels = 'info' action: ActionBase = new Action() @@ -57,9 +57,9 @@ export class UxConfig { function fetch() { if (globals[version.major]) return globals[version.major] - globals[version.major] = new UxConfig() + globals[version.major] = new Config() return globals[version.major] } -export const config: UxConfig = fetch() +export const config: Config = fetch() export default config diff --git a/src/cli-ux/index.ts b/src/cli-ux/index.ts index 68f56e395..da5a9e59f 100644 --- a/src/cli-ux/index.ts +++ b/src/cli-ux/index.ts @@ -2,7 +2,7 @@ import * as Errors from '../errors' import * as util from 'util' import {ActionBase} from './action/base' -import {config, UxConfig} from './config' +import {config, Config} from './config' import deps from './deps' import {ExitError} from './exit' import {IPromptOptions} from './prompt' @@ -127,12 +127,11 @@ export const ux = { await timeout(flush(), 10_000) }, } -export const cli = ux export { config, ActionBase, - UxConfig, + Config, ExitError, IPromptOptions, Table, diff --git a/src/cli-ux/styled/json.ts b/src/cli-ux/styled/json.ts index eec1748cd..bc8109ab8 100644 --- a/src/cli-ux/styled/json.ts +++ b/src/cli-ux/styled/json.ts @@ -2,16 +2,16 @@ import * as chalk from 'chalk' -import {cli} from '..' +import {CliUx} from '../../index' export default function styledJSON(obj: any) { const json = JSON.stringify(obj, null, 2) if (!chalk.level) { - cli.info(json) + CliUx.ux.info(json) return } const cardinal = require('cardinal') const theme = require('cardinal/themes/jq') - cli.info(cardinal.highlight(json, {json: true, theme})) + CliUx.ux.info(cardinal.highlight(json, {json: true, theme})) } diff --git a/src/command.ts b/src/command.ts index 10c17bcd3..d1cfee868 100644 --- a/src/command.ts +++ b/src/command.ts @@ -1,7 +1,7 @@ import {fileURLToPath} from 'url' import {format, inspect} from 'util' -import {cli} from './cli-ux' +import {CliUx} from './index' import {Config} from './config' import * as Interfaces from './interfaces' import * as Errors from './errors' @@ -163,7 +163,7 @@ export default abstract class Command { } if (result && this.jsonEnabled()) { - cli.styledJSON(this.toSuccessJson(result)) + CliUx.ux.styledJSON(this.toSuccessJson(result)) } return result @@ -227,12 +227,12 @@ export default abstract class Command { protected async catch(err: Record): Promise { process.exitCode = process.exitCode ?? err.exitCode ?? 1 if (this.jsonEnabled()) { - cli.styledJSON(this.toErrorJson(err)) + CliUx.ux.styledJSON(this.toErrorJson(err)) } else { if (!err.message) throw err try { const chalk = require('chalk') - cli.action.stop(chalk.bold.red('!')) + CliUx.ux.action.stop(chalk.bold.red('!')) } catch {} throw err diff --git a/src/index.ts b/src/index.ts index 016363056..afefb1f6a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,6 +13,8 @@ import * as Parser from './parser' import {Hook} from './interfaces/hooks' import {settings, Settings} from './settings' import {HelpSection, HelpSectionRenderer, HelpSectionKeyValueTable} from './help/formatter' +import * as cliUx from './cli-ux' +import {IPromptOptions as _IPromptOptions} from './cli-ux' const flush = require('../flush') @@ -42,7 +44,16 @@ export { flush, } -export * from './cli-ux' +export namespace CliUx { + export const ux = cliUx.ux + export const Config = cliUx.Config + export const config = cliUx.config + export const ExitError = cliUx.ExitError + export const ActionBase = cliUx.ActionBase + export const Table = cliUx.Table + // eslint-disable-next-line @typescript-eslint/no-empty-interface + export interface IPromptOptions extends _IPromptOptions {} +} function checkCWD() { try { diff --git a/test/cli-ux/fancy.ts b/test/cli-ux/fancy.ts index ea217fe5c..7718368c2 100644 --- a/test/cli-ux/fancy.ts +++ b/test/cli-ux/fancy.ts @@ -2,7 +2,7 @@ import {expect, fancy as base, FancyTypes} from 'fancy-test' import * as fs from 'fs-extra' import * as path from 'path' -import {cli} from '../../src/cli-ux' +import {CliUx} from '../../src' export { expect, @@ -20,5 +20,5 @@ export const fancy = base chalk.level = 0 }) .finally(async () => { - await cli.done() + await CliUx.ux.done() }) diff --git a/test/cli-ux/prompt.test.ts b/test/cli-ux/prompt.test.ts index 2d1ff3e2a..72498d9ad 100644 --- a/test/cli-ux/prompt.test.ts +++ b/test/cli-ux/prompt.test.ts @@ -2,7 +2,7 @@ import * as chai from 'chai' const expect = chai.expect -import {cli} from '../../src/cli-ux' +import {CliUx} from '../../src' import {fancy} from './fancy' @@ -11,11 +11,11 @@ describe('prompt', () => { .stdout() .stderr() .end('requires input', async () => { - const promptPromise = cli.prompt('Require input?') + const promptPromise = CliUx.ux.prompt('Require input?') process.stdin.emit('data', '') process.stdin.emit('data', 'answer') const answer = await promptPromise - await cli.done() + await CliUx.ux.done() expect(answer).to.equal('answer') }) @@ -24,9 +24,9 @@ describe('prompt', () => { .stderr() .stdin('y') .end('confirm', async () => { - const promptPromise = cli.confirm('yes/no?') + const promptPromise = CliUx.ux.confirm('yes/no?') const answer = await promptPromise - await cli.done() + await CliUx.ux.done() expect(answer).to.equal(true) }) @@ -35,9 +35,9 @@ describe('prompt', () => { .stderr() .stdin('n') .end('confirm', async () => { - const promptPromise = cli.confirm('yes/no?') + const promptPromise = CliUx.ux.confirm('yes/no?') const answer = await promptPromise - await cli.done() + await CliUx.ux.done() expect(answer).to.equal(false) }) @@ -46,9 +46,9 @@ describe('prompt', () => { .stderr() .stdin('x') .end('gets anykey', async () => { - const promptPromise = cli.anykey() + const promptPromise = CliUx.ux.anykey() const answer = await promptPromise - await cli.done() + await CliUx.ux.done() expect(answer).to.equal('x') }) @@ -56,12 +56,12 @@ describe('prompt', () => { .stdout() .stderr() .end('does not require input', async () => { - const promptPromise = cli.prompt('Require input?', { + const promptPromise = CliUx.ux.prompt('Require input?', { required: false, }) process.stdin.emit('data', '') const answer = await promptPromise - await cli.done() + await CliUx.ux.done() expect(answer).to.equal('') }) @@ -69,7 +69,7 @@ describe('prompt', () => { .stdout() .stderr() .it('timeouts with no input', async () => { - await expect(cli.prompt('Require input?', {timeout: 1})) + await expect(CliUx.ux.prompt('Require input?', {timeout: 1})) .to.eventually.be.rejectedWith('Prompt timeout') }) }) diff --git a/test/cli-ux/styled/object.test.ts b/test/cli-ux/styled/object.test.ts index 21819d527..27db22a81 100644 --- a/test/cli-ux/styled/object.test.ts +++ b/test/cli-ux/styled/object.test.ts @@ -1,12 +1,12 @@ import {expect, fancy} from 'fancy-test' -import {cli} from '../../../src/cli-ux' +import {CliUx} from '../../../src' describe('styled/object', () => { fancy .stdout() .end('shows a table', output => { - cli.styledObject([ + CliUx.ux.styledObject([ {foo: 1, bar: 1}, {foo: 2, bar: 2}, {foo: 3, bar: 3}, diff --git a/test/cli-ux/styled/progress.test.ts b/test/cli-ux/styled/progress.test.ts index 76efc58f8..c2f09bc17 100644 --- a/test/cli-ux/styled/progress.test.ts +++ b/test/cli-ux/styled/progress.test.ts @@ -1,13 +1,11 @@ import {expect, fancy} from 'fancy-test' - -// import {BarType, Progress} from '../../src/progress' -import {cli} from '../../../src/cli-ux' +import {CliUx} from '../../../src' describe('progress', () => { // single bar fancy .end('single bar has default settings', _ => { - const b1 = cli.progress({format: 'Example 1: Progress {bar} | {percentage}%'}) + const b1 = CliUx.ux.progress({format: 'Example 1: Progress {bar} | {percentage}%'}) expect(b1.options.format).to.contain('Example 1: Progress') expect(b1.bars).to.not.have }) @@ -15,7 +13,7 @@ describe('progress', () => { // testing no settings passed, default settings created fancy .end('single bar, no bars array', _ => { - const b1 = cli.progress({}) + const b1 = CliUx.ux.progress({}) expect(b1.options.format).to.contain('progress') expect(b1.bars).to.not.have expect(b1.options.noTTYOutput).to.not.be.null @@ -23,7 +21,7 @@ describe('progress', () => { // testing getProgressBar returns correct type fancy .end('typeof progress bar is object', _ => { - const b1 = cli.progress({format: 'Example 1: Progress {bar} | {percentage}%'}) + const b1 = CliUx.ux.progress({format: 'Example 1: Progress {bar} | {percentage}%'}) expect(typeof (b1)).to.equal('object') }) }) diff --git a/test/cli-ux/styled/table.test.ts b/test/cli-ux/styled/table.test.ts index 3f9636211..ef4c0ac37 100644 --- a/test/cli-ux/styled/table.test.ts +++ b/test/cli-ux/styled/table.test.ts @@ -1,6 +1,6 @@ import {expect, fancy} from 'fancy-test' -import {cli} from '../../../src/cli-ux' +import {CliUx} from '../../../src' /* eslint-disable camelcase */ const apps = [ @@ -71,13 +71,13 @@ const extendedHeader = `ID Name${ws.padEnd(14)}Web url${ws.padEnd(34)}Stack${ws describe('styled/table', () => { fancy .end('export flags and display()', () => { - expect(typeof (cli.table.flags())).to.eq('object') - expect(typeof (cli.table)).to.eq('function') + expect(typeof (CliUx.ux.table.flags())).to.eq('object') + expect(typeof (CliUx.ux.table)).to.eq('function') }) fancy .end('has optional flags', _ => { - const flags = cli.table.flags() + const flags = CliUx.ux.table.flags() expect(flags.columns).to.exist expect(flags.sort).to.exist expect(flags.filter).to.exist @@ -91,7 +91,7 @@ describe('styled/table', () => { fancy .stdout() .end('displays table', output => { - cli.table(apps, columns) + CliUx.ux.table(apps, columns) expect(output.stdout).to.equal(` ID Name${ws.padEnd(14)} ─── ─────────────────${ws} 123 supertable-test-1${ws} @@ -102,14 +102,14 @@ describe('styled/table', () => { fancy .stdout() .end('use header value for id', output => { - cli.table(apps, columns) + CliUx.ux.table(apps, columns) expect(output.stdout.slice(1, 3)).to.equal('ID') }) fancy .stdout() .end('shows extended columns/uses get() for value', output => { - cli.table(apps, columns, {extended: true}) + CliUx.ux.table(apps, columns, {extended: true}) expect(output.stdout).to.equal(`${ws}${extendedHeader} ─── ───────────────── ──────────────────────────────────────── ─────────${ws} 123 supertable-test-1 https://supertable-test-1.herokuapp.com/ heroku-16${ws} @@ -121,14 +121,14 @@ describe('styled/table', () => { fancy .stdout() .end('shows extended columns', output => { - cli.table(apps, columns, {extended: true}) + CliUx.ux.table(apps, columns, {extended: true}) expect(output.stdout).to.contain(extendedHeader) }) fancy .stdout() .end('shows title with divider', output => { - cli.table(apps, columns, {title: 'testing'}) + CliUx.ux.table(apps, columns, {title: 'testing'}) expect(output.stdout).to.equal(`testing ======================= | ID Name${ws.padEnd(14)} @@ -140,7 +140,7 @@ describe('styled/table', () => { fancy .stdout() .end('skips header', output => { - cli.table(apps, columns, {'no-header': true}) + CliUx.ux.table(apps, columns, {'no-header': true}) expect(output.stdout).to.equal(` 123 supertable-test-1${ws} 321 supertable-test-2${ws}\n`) }) @@ -148,7 +148,7 @@ describe('styled/table', () => { fancy .stdout() .end('only displays given columns', output => { - cli.table(apps, columns, {columns: 'id'}) + CliUx.ux.table(apps, columns, {columns: 'id'}) expect(output.stdout).to.equal(` ID${ws}${ws} ───${ws} 123${ws} @@ -158,7 +158,7 @@ describe('styled/table', () => { fancy .stdout() .end('outputs in csv', output => { - cli.table(apps, columns, {output: 'csv'}) + CliUx.ux.table(apps, columns, {output: 'csv'}) expect(output.stdout).to.equal(`ID,Name 123,supertable-test-1 321,supertable-test-2\n`) @@ -167,7 +167,7 @@ describe('styled/table', () => { fancy .stdout() .end('outputs in csv with escaped values', output => { - cli.table([ + CliUx.ux.table([ { id: '123\n2', name: 'supertable-test-1', @@ -195,7 +195,7 @@ describe('styled/table', () => { fancy .stdout() .end('outputs in csv without headers', output => { - cli.table(apps, columns, {output: 'csv', 'no-header': true}) + CliUx.ux.table(apps, columns, {output: 'csv', 'no-header': true}) expect(output.stdout).to.equal(`123,supertable-test-1 321,supertable-test-2\n`) }) @@ -203,7 +203,7 @@ describe('styled/table', () => { fancy .stdout() .end('outputs in csv with alias flag', output => { - cli.table(apps, columns, {csv: true}) + CliUx.ux.table(apps, columns, {csv: true}) expect(output.stdout).to.equal(`ID,Name 123,supertable-test-1 321,supertable-test-2\n`) @@ -212,7 +212,7 @@ describe('styled/table', () => { fancy .stdout() .end('outputs in json', output => { - cli.table(apps, columns, {output: 'json'}) + CliUx.ux.table(apps, columns, {output: 'json'}) expect(output.stdout).to.equal(`[ { "id": "123", @@ -229,7 +229,7 @@ describe('styled/table', () => { fancy .stdout() .end('outputs in yaml', output => { - cli.table(apps, columns, {output: 'yaml'}) + CliUx.ux.table(apps, columns, {output: 'yaml'}) expect(output.stdout).to.equal(`- id: '123' name: supertable-test-1 - id: '321' @@ -241,7 +241,7 @@ describe('styled/table', () => { fancy .stdout() .end('sorts by property', output => { - cli.table(apps, columns, {sort: '-name'}) + CliUx.ux.table(apps, columns, {sort: '-name'}) expect(output.stdout).to.equal(` ID Name${ws.padEnd(14)} ─── ─────────────────${ws} 321 supertable-test-2${ws} @@ -251,7 +251,7 @@ describe('styled/table', () => { fancy .stdout() .end('filters by property & value (partial string match)', output => { - cli.table(apps, columns, {filter: 'id=123'}) + CliUx.ux.table(apps, columns, {filter: 'id=123'}) expect(output.stdout).to.equal(` ID Name${ws.padEnd(14)} ─── ─────────────────${ws} 123 supertable-test-1${ws}\n`) @@ -261,7 +261,7 @@ describe('styled/table', () => { .stdout() .end('does not truncate', output => { const three = {...apps[0], id: '0'.repeat(80), name: 'supertable-test-3'} - cli.table([...apps, three], columns, {filter: 'id=0', 'no-truncate': true}) + CliUx.ux.table([...apps, three], columns, {filter: 'id=0', 'no-truncate': true}) expect(output.stdout).to.equal(` ID${ws.padEnd(78)} Name${ws.padEnd(14)} ${''.padEnd(three.id.length, '─')} ─────────────────${ws} ${three.id} supertable-test-3${ws}\n`) @@ -271,14 +271,14 @@ describe('styled/table', () => { describe('#flags', () => { fancy .end('includes only flags', _ => { - const flags = cli.table.flags({only: 'columns'}) + const flags = CliUx.ux.table.flags({only: 'columns'}) expect(flags.columns).to.be.a('object') expect((flags as any).sort).to.be.undefined }) fancy .end('excludes except flags', _ => { - const flags = cli.table.flags({except: 'columns'}) + const flags = CliUx.ux.table.flags({except: 'columns'}) expect((flags as any).columns).to.be.undefined expect(flags.sort).to.be.a('object') }) @@ -288,7 +288,7 @@ describe('styled/table', () => { fancy .stdout() .end('ignores header case', output => { - cli.table(apps, columns, {columns: 'iD,Name', filter: 'nAMe=supertable-test', sort: '-ID'}) + CliUx.ux.table(apps, columns, {columns: 'iD,Name', filter: 'nAMe=supertable-test', sort: '-ID'}) expect(output.stdout).to.equal(` ID Name${ws.padEnd(14)} ─── ─────────────────${ws} 321 supertable-test-2${ws} @@ -309,7 +309,7 @@ describe('styled/table', () => { } /* eslint-enable camelcase */ - cli.table([...apps, app3 as any], columns, {sort: '-ID'}) + CliUx.ux.table([...apps, app3 as any], columns, {sort: '-ID'}) expect(output.stdout).to.equal(` ID Name${ws.padEnd(14)} ─── ─────────────────${ws} 456 supertable-test${ws.padEnd(3)} diff --git a/test/cli-ux/styled/tree.test.ts b/test/cli-ux/styled/tree.test.ts index cb602495c..c42cd03fb 100644 --- a/test/cli-ux/styled/tree.test.ts +++ b/test/cli-ux/styled/tree.test.ts @@ -1,16 +1,16 @@ import {expect, fancy} from 'fancy-test' -import {cli} from '../../../src/cli-ux' +import {CliUx} from '../../../src' describe('styled/tree', () => { fancy .stdout() .end('shows the tree', output => { - const tree = cli.tree() + const tree = CliUx.ux.tree() tree.insert('foo') tree.insert('bar') - const subtree = cli.tree() + const subtree = CliUx.ux.tree() subtree.insert('qux') tree.nodes.bar.insert('baz', subtree) From 4811a6c50e313ba36abe749e3ed324c7d7b0cd02 Mon Sep 17 00:00:00 2001 From: Peter Hale Date: Thu, 20 Jan 2022 05:52:49 -0700 Subject: [PATCH 14/28] chore: document migration --- README.md | 4 + src/cli-ux/README.md | 329 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 333 insertions(+) create mode 100644 src/cli-ux/README.md diff --git a/README.md b/README.md index b471adf65..1f53ea056 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,10 @@ Migrating If you're migrating from the old oclif libraries (`@oclif/config`, `@oclif/command`, `@oclif/error`, `@oclif/parser`), see the [migration guide](./MIGRATION.md). +The @oclif/core module now includes the cli-ux module. The merge of cli-ux into core resolves a circular dependency between @oclif/core and cli-ux. +Please see the [cli-ux README](./src/cli-ux/README.md) for instructions on how to replace module cli-ux with @oclif/core. +The [cli-ux README](./src/cli-ux/README.md) also contains detail usage examples. + Usage ===== diff --git a/src/cli-ux/README.md b/src/cli-ux/README.md new file mode 100644 index 000000000..76021f06d --- /dev/null +++ b/src/cli-ux/README.md @@ -0,0 +1,329 @@ +# Migrating from cli-ux module to use the ux components now present in @oclif/core + +The capability of `cli-ux` has been retained in `@oclif/core`, but the code has been reorganized to expose the exported members via a namespace. +The exported member `cli` had been removed, since it is equivalent to exported member `ux`. +Updating your project to use cli IO utilities should be straight forward. + +- Remove the `cli-ux` dependency +- Change imports that reference `cli-ux` to `@oclif/core` +- Add namespace member `CliUx` to your `@oclif/core` import +- Preface previous `cli-ux` members with namespace `CliUx` +- Replace references to member `cli` with `ux` + +cli-ux +====== + +cli IO utilities + +# Usage + +The following assumes you have installed `@oclif/core` to your project with `npm install @oclif/core` or `yarn add @oclif/core` and have it required in your script (TypeScript example): + +```typescript +import {CliUx} from '@oclif/core' +CliUx.ux.prompt('What is your name?') +``` + +JavaScript: + +```javascript +const {CliUx} = require('@oclif/core') + +CliUx.ux.prompt('What is your name?') +``` + +# CliUx.ux.prompt() + +Prompt for user input. + +```typescript +// just prompt for input +await CliUx.ux.prompt('What is your name?') + +// mask input after enter is pressed +await CliUx.ux.prompt('What is your two-factor token?', {type: 'mask'}) + +// mask input on keypress (before enter is pressed) +await CliUx.ux.prompt('What is your password?', {type: 'hide'}) + +// yes/no confirmation +await CliUx.ux.confirm('Continue?') + +// "press any key to continue" +await CliUx.ux.anykey() +``` + +![prompt demo](assets/prompt.gif) + +# CliUx.ux.url(text, uri) + +Create a hyperlink (if supported in the terminal) + +```typescript +await CliUx.ux.url('sometext', 'https://google.com') +// shows sometext as a hyperlink in supported terminals +// shows https://google.com in unsupported terminals +``` + +![url demo](assets/url.gif) + +# CliUx.ux.open + +Open a url in the browser + +```typescript +await CliUx.ux.open('https://oclif.io') +``` + +# CliUx.ux.action + +Shows a spinner + +```typescript +// start the spinner +CliUx.ux.action.start('starting a process') +// show on stdout instead of stderr +CliUx.ux.action.start('starting a process', 'initializing', {stdout: true}) + +// stop the spinner +CliUx.ux.action.stop() // shows 'starting a process... done' +CliUx.ux.action.stop('custom message') // shows 'starting a process... custom message' +``` + +This degrades gracefully when not connected to a TTY. It queues up any writes to stdout/stderr so they are displayed above the spinner. + +![action demo](assets/action.gif) + +# CliUx.ux.annotation + +Shows an iterm annotation + +```typescript +CliUx.ux.annotation('sometext', 'annotated with this text') +``` + +![annotation demo](assets/annotation.png) + +# CliUx.ux.wait + +Waits for 1 second or given milliseconds + +```typescript +await CliUx.ux.wait() +await CliUx.ux.wait(3000) +``` + +# CliUx.ux.table + +Displays tabular data + +```typescript +CliUx.ux.table(data, columns, options) +``` + +Where: + +- `data`: array of data objects to display +- `columns`: [Table.Columns](./src/styled/table.ts) +- `options`: [Table.Options](./src/styled/table.ts) + +`CliUx.ux.table.flags()` returns an object containing all the table flags to include in your command. + +```typescript +{ + columns: Flags.string({exclusive: ['additional'], description: 'only show provided columns (comma-separated)'}), + sort: Flags.string({description: 'property to sort by (prepend '-' for descending)'}), + filter: Flags.string({description: 'filter property by partial string matching, ex: name=foo'}), + csv: Flags.boolean({exclusive: ['no-truncate'], description: 'output is csv format'}), + extended: Flags.boolean({char: 'x', description: 'show extra columns'}), + 'no-truncate': Flags.boolean({exclusive: ['csv'], description: 'do not truncate output to fit screen'}), + 'no-header': Flags.boolean({exclusive: ['csv'], description: 'hide table header from output'}), +} +``` + +Passing `{only: ['columns']}` or `{except: ['columns']}` as an argument into `CliUx.ux.table.flags()` will allow/block those flags from the returned object. + +`Table.Columns` defines the table columns and their display options. + +```typescript +const columns: Table.Columns = { + // where `.name` is a property of a data object + name: {}, // "Name" inferred as the column header + id: { + header: 'ID', // override column header + minWidth: '10', // column must display at this width or greater + extended: true, // only display this column when the --extended flag is present + get: row => `US-O1-${row.id}`, // custom getter for data row object + }, +} +``` + +`Table.Options` defines the table options, most of which are the parsed flags from the user for display customization, all of which are optional. + +```typescript +const options: Table.Options = { + printLine: myLogger, // custom logger + columns: flags.columns, + sort: flags.sort, + filter: flags.filter, + csv: flags.csv, + extended: flags.extended, + 'no-truncate': flags['no-truncate'], + 'no-header': flags['no-header'], +} +``` + +Example class: + +```typescript +import {Command} from '@oclif/core' +import {cli} from '@oclif/core' +import axios from 'axios' + +export default class Users extends Command { + static flags = { + ...CliUx.ux.table.flags() + } + + async run() { + const {flags} = this.parse(Users) + const {data: users} = await axios.get('https://jsonplaceholder.typicode.com/users') + + CliUx.ux.table(users, { + name: { + minWidth: 7, + }, + company: { + get: row => row.company && row.company.name + }, + id: { + header: 'ID', + extended: true + } + }, { + printLine: this.log, + ...flags, // parsed flags + }) + } +} +``` + +Displays: + +```shell +$ example-cli users +Name Company +Leanne Graham Romaguera-Crona +Ervin Howell Deckow-Crist +Clementine Bauch Romaguera-Jacobson +Patricia Lebsack Robel-Corkery +Chelsey Dietrich Keebler LLC +Mrs. Dennis Schulist Considine-Lockman +Kurtis Weissnat Johns Group +Nicholas Runolfsdottir V Abernathy Group +Glenna Reichert Yost and Sons +Clementina DuBuque Hoeger LLC + +$ example-cli users --extended +Name Company ID +Leanne Graham Romaguera-Crona 1 +Ervin Howell Deckow-Crist 2 +Clementine Bauch Romaguera-Jacobson 3 +Patricia Lebsack Robel-Corkery 4 +Chelsey Dietrich Keebler LLC 5 +Mrs. Dennis Schulist Considine-Lockman 6 +Kurtis Weissnat Johns Group 7 +Nicholas Runolfsdottir V Abernathy Group 8 +Glenna Reichert Yost and Sons 9 +Clementina DuBuque Hoeger LLC 10 + +$ example-cli users --columns=name +Name +Leanne Graham +Ervin Howell +Clementine Bauch +Patricia Lebsack +Chelsey Dietrich +Mrs. Dennis Schulist +Kurtis Weissnat +Nicholas Runolfsdottir V +Glenna Reichert +Clementina DuBuque + +$ example-cli users --filter="company=Group" +Name Company +Kurtis Weissnat Johns Group +Nicholas Runolfsdottir V Abernathy Group + +$ example-cli users --sort=company +Name Company +Nicholas Runolfsdottir V Abernathy Group +Mrs. Dennis Schulist Considine-Lockman +Ervin Howell Deckow-Crist +Clementina DuBuque Hoeger LLC +Kurtis Weissnat Johns Group +Chelsey Dietrich Keebler LLC +Patricia Lebsack Robel-Corkery +Leanne Graham Romaguera-Crona +Clementine Bauch Romaguera-Jacobson +Glenna Reichert Yost and Sons +``` + +# CliUx.ux.tree + +Generate a tree and display it + +```typescript +let tree = CliUx.ux.tree() +tree.insert('foo') +tree.insert('bar') + +let subtree = CliUx.ux.tree() +subtree.insert('qux') +tree.nodes.bar.insert('baz', subtree) + +tree.display() +``` + +Outputs: +```shell +├─ foo +└─ bar + └─ baz + └─ qux +``` + +# CliUx.ux.progress + +Generate a customizable progress bar and display it + +```typescript +const simpleBar = CliUx.ux.progress() +simpleBar.start() + +const customBar = CliUx.ux.progress({ + format: 'PROGRESS | {bar} | {value}/{total} Files', + barCompleteChar: '\u2588', + barIncompleteChar: '\u2591', + }) +customBar.start() +``` + +Outputs: +```shell +bar1: +progress [=====================-------------------] 53% | ETA: 1s | 53/100 +bar2: +PROGRESS | █████████████████████████████░░░░░░░░░░░ | 146/204 Files +``` + +To see a more detailed example, run +```shell script +$ ts-node examples/progress.ts +``` + +This extends [cli-progress](https://www.npmjs.com/package/cli-progress) +see all of the options and customizations there, which can be passed in with the options object. +Only the single bar variant of cli-progress is currently supported. + + From 65a40b4c50854cbc147fc798909f44b0a07ffd47 Mon Sep 17 00:00:00 2001 From: peternhale Date: Thu, 20 Jan 2022 10:40:55 -0700 Subject: [PATCH 15/28] chore: apply suggestions from code review Co-authored-by: Juliet Shackell <63259011+jshackell-sfdc@users.noreply.github.com> --- README.md | 6 +++--- src/cli-ux/README.md | 22 +++++++++++----------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 1f53ea056..5e21aa549 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,9 @@ Migrating If you're migrating from the old oclif libraries (`@oclif/config`, `@oclif/command`, `@oclif/error`, `@oclif/parser`), see the [migration guide](./MIGRATION.md). -The @oclif/core module now includes the cli-ux module. The merge of cli-ux into core resolves a circular dependency between @oclif/core and cli-ux. -Please see the [cli-ux README](./src/cli-ux/README.md) for instructions on how to replace module cli-ux with @oclif/core. -The [cli-ux README](./src/cli-ux/README.md) also contains detail usage examples. +The `@oclif/core` module now also includes the `cli-ux` module. Merging `cli-ux` into `@oclif/core` resolves a circular dependency between the two modules. +See the [cli-ux README](./src/cli-ux/README.md) for instructions on how to replace the `cli-ux` module with `@oclif/core`. +The [cli-ux README](./src/cli-ux/README.md) also contains detailed usage examples. Usage ===== diff --git a/src/cli-ux/README.md b/src/cli-ux/README.md index 76021f06d..cefd9c9ea 100644 --- a/src/cli-ux/README.md +++ b/src/cli-ux/README.md @@ -1,14 +1,14 @@ -# Migrating from cli-ux module to use the ux components now present in @oclif/core +# How to migrate from the `cli-ux` module and use the `ux` components now contained in `@oclif/core` -The capability of `cli-ux` has been retained in `@oclif/core`, but the code has been reorganized to expose the exported members via a namespace. -The exported member `cli` had been removed, since it is equivalent to exported member `ux`. +We've retained the capabilities of `cli-ux` in `@oclif/core`, but we've reorganized the code to expose the exported members via a namespace. +We've removed the exported member `cli`, because it's equivalent to the exported member `ux`. Updating your project to use cli IO utilities should be straight forward. -- Remove the `cli-ux` dependency -- Change imports that reference `cli-ux` to `@oclif/core` -- Add namespace member `CliUx` to your `@oclif/core` import -- Preface previous `cli-ux` members with namespace `CliUx` -- Replace references to member `cli` with `ux` +1. Remove the `cli-ux` dependency. +1. Change all imports that reference `cli-ux` to `@oclif/core`. +1. Add the namespace member `CliUx` to your `@oclif/core` import. +1. Preface previous `cli-ux` members with the namespace `CliUx`. +1. Replace all references to member `cli` with `ux`. cli-ux ====== @@ -17,7 +17,7 @@ cli IO utilities # Usage -The following assumes you have installed `@oclif/core` to your project with `npm install @oclif/core` or `yarn add @oclif/core` and have it required in your script (TypeScript example): +The following example assumes you've installed `@oclif/core` to your project with `npm install @oclif/core` or `yarn add @oclif/core` and have it required in your script (TypeScript example): ```typescript import {CliUx} from '@oclif/core' @@ -141,7 +141,7 @@ Where: } ``` -Passing `{only: ['columns']}` or `{except: ['columns']}` as an argument into `CliUx.ux.table.flags()` will allow/block those flags from the returned object. +Passing `{only: ['columns']}` or `{except: ['columns']}` as an argument into `CliUx.ux.table.flags()` allows or blocks, respectively, those flags from the returned object. `Table.Columns` defines the table columns and their display options. @@ -322,7 +322,7 @@ To see a more detailed example, run $ ts-node examples/progress.ts ``` -This extends [cli-progress](https://www.npmjs.com/package/cli-progress) +This example extends [cli-progress](https://www.npmjs.com/package/cli-progress). see all of the options and customizations there, which can be passed in with the options object. Only the single bar variant of cli-progress is currently supported. From c7df81d3c020ce19dd8eac326383307f686993f8 Mon Sep 17 00:00:00 2001 From: Peter Hale Date: Thu, 20 Jan 2022 10:48:41 -0700 Subject: [PATCH 16/28] chore: remove unneeded console.log statement --- test/integration/sf.e2e.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/integration/sf.e2e.ts b/test/integration/sf.e2e.ts index eae051f57..bcd2efa77 100644 --- a/test/integration/sf.e2e.ts +++ b/test/integration/sf.e2e.ts @@ -85,7 +85,6 @@ describe('Salesforce CLI (sf)', () => { it('should have formatted json success output', async () => { const config = await executor.executeCommand('config list --json') - console.log(config.output!) const result = parseJson(config.output!) expect(result).to.have.property('status') expect(result).to.have.property('result') From 917a4ba593b9db9e1e93b654e16c5a44e88ac633 Mon Sep 17 00:00:00 2001 From: Peter Hale Date: Thu, 20 Jan 2022 14:22:36 -0700 Subject: [PATCH 17/28] chore: update migration guide --- MIGRATION.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index 76b9d34ef..cbf1e32e5 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -10,13 +10,13 @@ Replace imports from the old libraries with `@oclif/core`. For example, ```typescript import Help from '@oclif/plugin-help'; import {Topic} from '@oclif/config'; -import {Command, flags} from '@oclif/command' +import {Command, Flags} from '@oclif/command' ``` With this import: ```typescript -import {Command, flags, Topic, Help} from '@oclif/core'; +import {Command, Flags, Topic, Help} from '@oclif/core'; ``` ## Update your bin scirpts From 37dfcf7fae5a1ddae145c0169875aed81a384e32 Mon Sep 17 00:00:00 2001 From: Peter Hale Date: Thu, 20 Jan 2022 14:30:45 -0700 Subject: [PATCH 18/28] chore: fix up examples --- src/cli-ux/README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/cli-ux/README.md b/src/cli-ux/README.md index cefd9c9ea..0347c941f 100644 --- a/src/cli-ux/README.md +++ b/src/cli-ux/README.md @@ -146,7 +146,7 @@ Passing `{only: ['columns']}` or `{except: ['columns']}` as an argument into `Cl `Table.Columns` defines the table columns and their display options. ```typescript -const columns: Table.Columns = { +const columns: CliUx.Table.Columns = { // where `.name` is a property of a data object name: {}, // "Name" inferred as the column header id: { @@ -161,7 +161,7 @@ const columns: Table.Columns = { `Table.Options` defines the table options, most of which are the parsed flags from the user for display customization, all of which are optional. ```typescript -const options: Table.Options = { +const options: CliUx.Table.Options = { printLine: myLogger, // custom logger columns: flags.columns, sort: flags.sort, @@ -176,8 +176,7 @@ const options: Table.Options = { Example class: ```typescript -import {Command} from '@oclif/core' -import {cli} from '@oclif/core' +import {Command, CliUx} from '@oclif/core' import axios from 'axios' export default class Users extends Command { From 86ca40556ebeee1a452046cd25818b895401c701 Mon Sep 17 00:00:00 2001 From: Peter Hale Date: Thu, 20 Jan 2022 14:33:44 -0700 Subject: [PATCH 19/28] chore: apply review suggestions --- src/cli-ux/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli-ux/README.md b/src/cli-ux/README.md index 0347c941f..c2c362e04 100644 --- a/src/cli-ux/README.md +++ b/src/cli-ux/README.md @@ -322,7 +322,7 @@ $ ts-node examples/progress.ts ``` This example extends [cli-progress](https://www.npmjs.com/package/cli-progress). -see all of the options and customizations there, which can be passed in with the options object. +See the cli-progress module for all the options and the customizations that can be passed in with the options object. Only the single bar variant of cli-progress is currently supported. From 5edba2e56f029d71bfe4fd6268955e7107831e43 Mon Sep 17 00:00:00 2001 From: Peter Hale Date: Tue, 25 Jan 2022 06:35:53 -0700 Subject: [PATCH 20/28] chore: adjust exports --- src/index.ts | 10 +++++++++- test/cli-ux/export.test.ts | 24 ++++++++++++++++++++++++ test/cli-ux/index.test.ts | 4 ++-- 3 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 test/cli-ux/export.test.ts diff --git a/src/index.ts b/src/index.ts index afefb1f6a..93c08d240 100644 --- a/src/index.ts +++ b/src/index.ts @@ -50,7 +50,15 @@ export namespace CliUx { export const config = cliUx.config export const ExitError = cliUx.ExitError export const ActionBase = cliUx.ActionBase - export const Table = cliUx.Table + export namespace Table { + export const table = cliUx.Table.table + // @ts-ignore + export type Column> = cliUx.Table.table.Column + // eslint-disable-next-line @typescript-eslint/no-unused-vars + export type Columns> = cliUx.Table.table.Columns + // eslint-disable-next-line @typescript-eslint/no-empty-interface + export interface Options extends cliUx.Table.table.Options {} + } // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface IPromptOptions extends _IPromptOptions {} } diff --git a/test/cli-ux/export.test.ts b/test/cli-ux/export.test.ts new file mode 100644 index 000000000..1c2c47a47 --- /dev/null +++ b/test/cli-ux/export.test.ts @@ -0,0 +1,24 @@ +import {CliUx} from '../../src' +import {expect} from 'chai' + +type MyColumns = Record +const options: CliUx.Table.Options = {} +const columns: CliUx.Table.Columns = {} +const iPromptOptions: CliUx.IPromptOptions = {} + +describe('cli-ux exports', () => { + it('should have exported members on par with old cli-ux module', () => { + expect(options).to.be.ok + expect(columns).to.be.ok + expect(iPromptOptions).to.be.ok + expect(CliUx.Table.table.Flags).to.be.ok + expect(typeof CliUx.Table.table.flags).to.be.equal('function') + expect(typeof CliUx.Table.table).to.be.equal('function') + expect(CliUx.ux).to.be.ok + expect(CliUx.config).to.be.ok + expect(typeof CliUx.Config).to.be.equal('function') + expect(typeof CliUx.ActionBase).to.be.equal('function') + expect(typeof CliUx.ExitError).to.be.equal('function') + }) +}) + diff --git a/test/cli-ux/index.test.ts b/test/cli-ux/index.test.ts index 81f6b0d52..2488b0778 100644 --- a/test/cli-ux/index.test.ts +++ b/test/cli-ux/index.test.ts @@ -1,4 +1,4 @@ -import {ux} from '../../src/cli-ux' +import {CliUx} from '../../src' import {expect, fancy} from './fancy' const hyperlinker = require('hyperlinker') @@ -7,7 +7,7 @@ describe('url', () => { fancy .env({FORCE_HYPERLINK: '1'}, {clear: true}) .stdout() - .do(() => ux.url('sometext', 'https://google.com')) + .do(() => CliUx.ux.url('sometext', 'https://google.com')) .it('renders hyperlink', async ({stdout}) => { expect(stdout).to.equal('sometext\n') }) From 9806fa8c8bc7f3964fcfb873fa9434c28561efe4 Mon Sep 17 00:00:00 2001 From: Peter Hale Date: Tue, 25 Jan 2022 09:17:17 -0700 Subject: [PATCH 21/28] chore: abandon use of namesapce --- src/index.ts | 21 +-------------------- test/cli-ux/export.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 22 deletions(-) diff --git a/src/index.ts b/src/index.ts index 93c08d240..05e1f8859 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,7 +14,6 @@ import {Hook} from './interfaces/hooks' import {settings, Settings} from './settings' import {HelpSection, HelpSectionRenderer, HelpSectionKeyValueTable} from './help/formatter' import * as cliUx from './cli-ux' -import {IPromptOptions as _IPromptOptions} from './cli-ux' const flush = require('../flush') @@ -42,25 +41,7 @@ export { settings, Settings, flush, -} - -export namespace CliUx { - export const ux = cliUx.ux - export const Config = cliUx.Config - export const config = cliUx.config - export const ExitError = cliUx.ExitError - export const ActionBase = cliUx.ActionBase - export namespace Table { - export const table = cliUx.Table.table - // @ts-ignore - export type Column> = cliUx.Table.table.Column - // eslint-disable-next-line @typescript-eslint/no-unused-vars - export type Columns> = cliUx.Table.table.Columns - // eslint-disable-next-line @typescript-eslint/no-empty-interface - export interface Options extends cliUx.Table.table.Options {} - } - // eslint-disable-next-line @typescript-eslint/no-empty-interface - export interface IPromptOptions extends _IPromptOptions {} + cliUx as CliUx, } function checkCWD() { diff --git a/test/cli-ux/export.test.ts b/test/cli-ux/export.test.ts index 1c2c47a47..8e3038769 100644 --- a/test/cli-ux/export.test.ts +++ b/test/cli-ux/export.test.ts @@ -2,8 +2,8 @@ import {CliUx} from '../../src' import {expect} from 'chai' type MyColumns = Record -const options: CliUx.Table.Options = {} -const columns: CliUx.Table.Columns = {} +const options: CliUx.Table.table.Options = {} +const columns: CliUx.Table.table.Columns = {} const iPromptOptions: CliUx.IPromptOptions = {} describe('cli-ux exports', () => { From 703286a01fb8e7e6837a5a5fa0778f558a6da4e5 Mon Sep 17 00:00:00 2001 From: Peter Hale Date: Tue, 25 Jan 2022 13:49:29 -0700 Subject: [PATCH 22/28] v1.1.2-test-07 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3324120a8..1434f7891 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@oclif/core", "description": "base library for oclif CLIs", - "version": "1.1.2", + "version": "1.1.2-test-07", "author": "Salesforce", "bugs": "https://github.com/oclif/core/issues", "dependencies": { From baacd2eda2e4a6112f87340017aab7079c453fe2 Mon Sep 17 00:00:00 2001 From: Peter Hale Date: Tue, 25 Jan 2022 15:04:32 -0700 Subject: [PATCH 23/28] v1.1.2-test-08 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1434f7891..bce091c22 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@oclif/core", "description": "base library for oclif CLIs", - "version": "1.1.2-test-07", + "version": "1.1.2-test-08", "author": "Salesforce", "bugs": "https://github.com/oclif/core/issues", "dependencies": { From 598bf34a28401267ce0231c5e3c64e975b53dcd3 Mon Sep 17 00:00:00 2001 From: Peter Hale Date: Tue, 25 Jan 2022 15:11:54 -0700 Subject: [PATCH 24/28] v1.1.2-test-09 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bce091c22..af39b949d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@oclif/core", "description": "base library for oclif CLIs", - "version": "1.1.2-test-08", + "version": "1.1.2-test-09", "author": "Salesforce", "bugs": "https://github.com/oclif/core/issues", "dependencies": { From 61eee521e853f987c5bd155f1f376bdc2f9d597e Mon Sep 17 00:00:00 2001 From: Peter Hale Date: Tue, 25 Jan 2022 15:16:17 -0700 Subject: [PATCH 25/28] chore: remove use of lodash/castArray --- package.json | 2 +- src/cli-ux/action/base.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index af39b949d..3324120a8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@oclif/core", "description": "base library for oclif CLIs", - "version": "1.1.2-test-09", + "version": "1.1.2", "author": "Salesforce", "bugs": "https://github.com/oclif/core/issues", "dependencies": { diff --git a/src/cli-ux/action/base.ts b/src/cli-ux/action/base.ts index cfb51b50b..b2b141915 100644 --- a/src/cli-ux/action/base.ts +++ b/src/cli-ux/action/base.ts @@ -1,5 +1,5 @@ -import castArray from 'lodash/castArray' import {inspect} from 'util' +import {castArray} from '../../util' export interface ITask { action: string; From 9f31ac3bdefe1d0e65cf9fe94aea22ec45254b6a Mon Sep 17 00:00:00 2001 From: Rodrigo Espinosa de los Monteros <1084688+RodEsp@users.noreply.github.com> Date: Wed, 26 Jan 2022 09:31:18 -0500 Subject: [PATCH 26/28] chore: exclude windows latest, see npm/cli#4234 --- .circleci/config.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 12f808dec..af360dd78 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -18,6 +18,9 @@ workflows: - latest - lts - maintenance + exclude: + - os: windows + node_version: latest alias: unit-tests - release-management/test-package: matrix: @@ -31,6 +34,9 @@ workflows: - maintenance command: - yarn test:e2e + exclude: + - os: windows + node_version: latest alias: integration-tests - release-management/release-package: context: SF-CLI-RELEASE-PROCESS From 9f2a7c6ba59b433069ba618aa782c3a12d67c3ad Mon Sep 17 00:00:00 2001 From: Rodrigo Espinosa de los Monteros <1084688+RodEsp@users.noreply.github.com> Date: Wed, 26 Jan 2022 09:43:01 -0500 Subject: [PATCH 27/28] fix: integration test command in ci config --- .circleci/config.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index af360dd78..fec022a2e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -23,6 +23,7 @@ workflows: node_version: latest alias: unit-tests - release-management/test-package: + command: yarn test:e2e matrix: parameters: os: @@ -32,8 +33,6 @@ workflows: - latest - lts - maintenance - command: - - yarn test:e2e exclude: - os: windows node_version: latest From b964947afd80b2cf6ef710bd6a225dd862a86cb9 Mon Sep 17 00:00:00 2001 From: Rodrigo Espinosa de los Monteros <1084688+RodEsp@users.noreply.github.com> Date: Wed, 26 Jan 2022 10:00:54 -0500 Subject: [PATCH 28/28] chore: fix integration test job name --- .circleci/config.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index fec022a2e..2a4ed5c07 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -23,6 +23,7 @@ workflows: node_version: latest alias: unit-tests - release-management/test-package: + name: release-management/test-package-yarn test:e2e-<< matrix.node_version >>-<< matrix.os >> command: yarn test:e2e matrix: parameters: