mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-14 10:08:06 -10:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9147ae631c |
No files matched your search
@@ -1,8 +1,10 @@
|
||||
import { EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
|
||||
import * as PackageManagerUtils from '@expo/package-manager';
|
||||
import { Command } from '@oclif/core';
|
||||
import { Command, Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Options, Ora } from 'ora';
|
||||
import path from 'path';
|
||||
import semver from 'semver';
|
||||
|
||||
@@ -12,15 +14,48 @@ import {
|
||||
initAsync as initAnalyticsAsync,
|
||||
logEvent,
|
||||
} from '../analytics/rudderstackClient';
|
||||
import { learnMore } from '../log';
|
||||
import Log, { learnMore } from '../log';
|
||||
import { interactiveOra, nonInteractiveOra } from '../ora';
|
||||
import { findProjectRootAsync } from '../project/projectUtils';
|
||||
import { InteractivePrompts, NonInteractivePrompts, Prompts } from '../prompts';
|
||||
import { getUserAsync } from '../user/User';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
import { easCliVersion } from '../utils/easCli';
|
||||
import { setVcsClient } from '../vcs';
|
||||
import GitClient from '../vcs/clients/git';
|
||||
|
||||
type CommandOptions =
|
||||
| {
|
||||
nonInteractive: true;
|
||||
json: true;
|
||||
}
|
||||
| {
|
||||
nonInteractive: boolean;
|
||||
json: false;
|
||||
};
|
||||
|
||||
export type CommandContext = Readonly<
|
||||
CommandOptions & {
|
||||
logger: Log;
|
||||
prompts: Prompts;
|
||||
ora: (options?: Options | string) => Ora;
|
||||
}
|
||||
>;
|
||||
|
||||
export default abstract class EasCommand extends Command {
|
||||
static override globalFlags = {
|
||||
json: Flags.boolean({
|
||||
description: 'Enable JSON output, non-JSON messages will be printed to stderr.',
|
||||
dependsOn: ['non-interactive'],
|
||||
}),
|
||||
'non-interactive': Flags.boolean({
|
||||
description: 'Run the command in non-interactive mode.',
|
||||
}),
|
||||
};
|
||||
|
||||
// disable built in oclif json stuff
|
||||
enableJsonFlag = false;
|
||||
|
||||
/**
|
||||
* When user data is unavailable locally, determines if the command will
|
||||
* force the user to log in
|
||||
@@ -28,10 +63,26 @@ export default abstract class EasCommand extends Command {
|
||||
protected requiresAuthentication = true;
|
||||
protected mustBeRunInsideProject = true;
|
||||
|
||||
protected abstract runAsync(): Promise<any>;
|
||||
protected abstract runAsync({
|
||||
nonInteractive,
|
||||
json,
|
||||
logger,
|
||||
prompts: {
|
||||
promptAsync,
|
||||
confirmAsync,
|
||||
selectAsync,
|
||||
toggleConfirmAsync,
|
||||
pressAnyKeyToContinueAsync,
|
||||
},
|
||||
ora,
|
||||
}: CommandContext): Promise<{ jsonOutput: object }>;
|
||||
|
||||
// eslint-disable-next-line async-protect/async-suffix
|
||||
async run(): Promise<any> {
|
||||
const { flags } = await this.parse();
|
||||
const json = flags.json ?? false;
|
||||
const nonInteractive = (flags as any)['non-interactive'] ?? false;
|
||||
|
||||
await initAnalyticsAsync();
|
||||
|
||||
if (this.mustBeRunInsideProject) {
|
||||
@@ -41,8 +92,6 @@ export default abstract class EasCommand extends Command {
|
||||
}
|
||||
|
||||
if (this.requiresAuthentication) {
|
||||
const { flags } = await this.parse();
|
||||
const nonInteractive = (flags as any)['non-interactive'] ?? false;
|
||||
await ensureLoggedInAsync({ nonInteractive });
|
||||
} else {
|
||||
await getUserAsync();
|
||||
@@ -52,7 +101,18 @@ export default abstract class EasCommand extends Command {
|
||||
// commands/submit === submit, commands/build/list === build:list
|
||||
action: `eas ${this.id}`,
|
||||
});
|
||||
return this.runAsync();
|
||||
|
||||
const jsonOutput = await this.runAsync({
|
||||
nonInteractive,
|
||||
json: json ?? false,
|
||||
logger: new Log(json),
|
||||
prompts: nonInteractive ? new InteractivePrompts() : new NonInteractivePrompts(),
|
||||
ora: nonInteractive ? nonInteractiveOra : interactiveOra,
|
||||
});
|
||||
|
||||
if (json) {
|
||||
this.printJsonOnlyOutput(jsonOutput);
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line async-protect/async-suffix
|
||||
@@ -119,4 +179,26 @@ export default abstract class EasCommand extends Command {
|
||||
packageJson?.devDependencies?.['eas-cli'] !== undefined
|
||||
);
|
||||
}
|
||||
|
||||
private printJsonOnlyOutput(value: object): void {
|
||||
try {
|
||||
new Log(false).log(JSON.stringify(this.sanitizeValue(value), null, 2));
|
||||
} catch {}
|
||||
}
|
||||
|
||||
private sanitizeValue(value: any): unknown {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(val => this.sanitizeValue(val));
|
||||
} else if (value && typeof value === 'object') {
|
||||
const result: Record<string, any> = {};
|
||||
Object.keys(value).forEach(key => {
|
||||
if (key !== '__typename' && value[key] !== null) {
|
||||
result[key] = this.sanitizeValue(value[key]);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
|
||||
export const getPaginatedQueryOptions = (
|
||||
flags: Record<keyof typeof EasPaginatedQueryFlags, any>
|
||||
): PaginatedQueryOptions => {
|
||||
export const getPaginatedQueryOptions = (flags: {
|
||||
json: boolean;
|
||||
'non-interactive': boolean;
|
||||
offset: number | undefined;
|
||||
limit: number | undefined;
|
||||
}): PaginatedQueryOptions => {
|
||||
return {
|
||||
json: flags.json ?? false,
|
||||
json: flags.json,
|
||||
offset: flags.offset ?? 0,
|
||||
nonInteractive: flags['non-interactive'] ?? false,
|
||||
nonInteractive: flags['non-interactive'],
|
||||
...('limit' in flags && { limit: flags.limit }),
|
||||
};
|
||||
};
|
||||
@@ -40,12 +43,6 @@ export const EasPaginatedQueryFlags = {
|
||||
// eslint-disable-next-line async-protect/async-suffix
|
||||
parse: async input => parseFlagInputStringAsInteger(input, 'limit', 1, 100),
|
||||
}),
|
||||
json: Flags.boolean({
|
||||
description: 'Enable JSON output, non-JSON messages will be printed to stderr.',
|
||||
}),
|
||||
'non-interactive': Flags.boolean({
|
||||
description: 'Run the command in non-interactive mode.',
|
||||
}),
|
||||
};
|
||||
|
||||
// options required to control a paginated query
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import EasCommand, { CommandContext } from '../../commandUtils/EasCommand';
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
import {
|
||||
DeleteUpdateBranchMutation,
|
||||
@@ -11,15 +10,12 @@ import {
|
||||
GetBranchInfoQuery,
|
||||
GetBranchInfoQueryVariables,
|
||||
} from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { getExpoConfig } from '../../project/expoConfig';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getProjectFullNameAsync,
|
||||
getProjectIdAsync,
|
||||
} from '../../project/projectUtils';
|
||||
import { promptAsync, toggleConfirmAsync } from '../../prompts';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
|
||||
async function getBranchInfoAsync({
|
||||
appId,
|
||||
@@ -86,21 +82,15 @@ export default class BranchDelete extends EasCommand {
|
||||
description: 'Name of the branch to delete',
|
||||
},
|
||||
];
|
||||
static override flags = {
|
||||
json: Flags.boolean({
|
||||
description: `return JSON with the edited branch's ID and name.`,
|
||||
default: false,
|
||||
}),
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
protected async runAsync({
|
||||
nonInteractive,
|
||||
logger,
|
||||
prompts: { promptAsync, toggleConfirmAsync },
|
||||
}: CommandContext): Promise<{ jsonOutput: object }> {
|
||||
let {
|
||||
args: { name },
|
||||
flags: { json: jsonFlag },
|
||||
} = await this.parse(BranchDelete);
|
||||
if (jsonFlag) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const exp = getExpoConfig(projectDir);
|
||||
@@ -109,7 +99,7 @@ export default class BranchDelete extends EasCommand {
|
||||
|
||||
if (!name) {
|
||||
const validationMessage = 'branch name may not be empty.';
|
||||
if (jsonFlag) {
|
||||
if (nonInteractive) {
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
({ name } = await promptAsync({
|
||||
@@ -126,16 +116,16 @@ export default class BranchDelete extends EasCommand {
|
||||
throw new Error(`Could not find branch ${name} on ${fullName}`);
|
||||
}
|
||||
|
||||
if (!jsonFlag) {
|
||||
Log.addNewLineIfNone();
|
||||
Log.warn(
|
||||
if (!nonInteractive) {
|
||||
logger.addNewLineIfNone();
|
||||
logger.warn(
|
||||
`You are about to permanently delete branch: "${name}" and all of the updates published on it.` +
|
||||
`\nThis action is irreversible.`
|
||||
);
|
||||
Log.newLine();
|
||||
logger.newLine();
|
||||
const confirmed = await toggleConfirmAsync({ message: 'Are you sure you wish to proceed?' });
|
||||
if (!confirmed) {
|
||||
Log.error(`Cancelled deletion of branch: "${name}".`);
|
||||
logger.error(`Cancelled deletion of branch: "${name}".`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -144,12 +134,10 @@ export default class BranchDelete extends EasCommand {
|
||||
branchId,
|
||||
});
|
||||
|
||||
if (jsonFlag) {
|
||||
printJsonOnlyOutput(deletionResult);
|
||||
} else {
|
||||
Log.withTick(
|
||||
`️Deleted branch "${name}" and all of its updates on project ${chalk.bold(fullName)}.`
|
||||
);
|
||||
}
|
||||
logger.withTick(
|
||||
`Deleted branch "${name}" and all of its updates on project ${chalk.bold(fullName)}.`
|
||||
);
|
||||
|
||||
return { jsonOutput: deletionResult };
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { Device, DeviceStatus } from '@expo/apple-utils';
|
||||
import { Flags } from '@oclif/core';
|
||||
import assert from 'assert';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import EasCommand, { CommandContext } from '../../commandUtils/EasCommand';
|
||||
import { chooseDevicesToDeleteAsync } from '../../credentials/ios/actions/DeviceUtils';
|
||||
import { AppleDeviceMutation } from '../../credentials/ios/api/graphql/mutations/AppleDeviceMutation';
|
||||
import {
|
||||
@@ -14,11 +14,8 @@ import { AppleTeamQuery } from '../../credentials/ios/api/graphql/queries/AppleT
|
||||
import { authenticateAsync, getRequestContext } from '../../credentials/ios/appstore/authenticate';
|
||||
import formatDevice from '../../devices/utils/formatDevice';
|
||||
import { AppleDevice, Maybe } from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import { getExpoConfig } from '../../project/expoConfig';
|
||||
import { findProjectRootAsync, getProjectAccountNameAsync } from '../../project/projectUtils';
|
||||
import { promptAsync, toggleConfirmAsync } from '../../prompts';
|
||||
|
||||
export default class DeviceDelete extends EasCommand {
|
||||
static override description = 'remove a registered device from your account';
|
||||
@@ -28,7 +25,7 @@ export default class DeviceDelete extends EasCommand {
|
||||
udid: Flags.string({ multiple: true }),
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
protected async runAsync(commandContext: CommandContext): Promise<{ jsonOutput: object }> {
|
||||
let {
|
||||
flags: { 'apple-team-id': appleTeamIdentifier, udid: udids },
|
||||
} = await this.parse(DeviceDelete);
|
||||
@@ -38,46 +35,61 @@ export default class DeviceDelete extends EasCommand {
|
||||
const accountName = await getProjectAccountNameAsync(exp);
|
||||
|
||||
if (!appleTeamIdentifier) {
|
||||
appleTeamIdentifier = await this.askForAppleTeamAsync(accountName);
|
||||
if (commandContext.nonInteractive) {
|
||||
throw new Error('Must supply `apple-team-id` in non-interactive mode');
|
||||
}
|
||||
appleTeamIdentifier = await this.askForAppleTeamAsync(commandContext, accountName);
|
||||
}
|
||||
|
||||
assert(appleTeamIdentifier, 'No team identifier is specified');
|
||||
|
||||
const appleDevicesResult = await this.getDevicesForTeamAsync(accountName, appleTeamIdentifier);
|
||||
const appleDevicesResult = await this.getDevicesForTeamAsync(
|
||||
commandContext,
|
||||
accountName,
|
||||
appleTeamIdentifier
|
||||
);
|
||||
|
||||
if (!appleDevicesResult) {
|
||||
return;
|
||||
return { jsonOutput: [] };
|
||||
}
|
||||
|
||||
const { appleTeamName, appleDevices } = appleDevicesResult;
|
||||
|
||||
const chosenDevices = await this.chooseDevicesToDeleteAsync(appleDevices, udids);
|
||||
const chosenDevices = await this.chooseDevicesToDeleteAsync(
|
||||
commandContext,
|
||||
appleDevices,
|
||||
udids
|
||||
);
|
||||
|
||||
if (chosenDevices.length === 0) {
|
||||
Log.newLine();
|
||||
Log.warn('No devices were chosen to be removed.');
|
||||
return;
|
||||
commandContext.logger.newLine();
|
||||
commandContext.logger.warn('No devices were chosen to be removed.');
|
||||
return { jsonOutput: [] };
|
||||
}
|
||||
|
||||
this.logChosenDevices(chosenDevices, appleTeamName, appleTeamIdentifier);
|
||||
|
||||
const hasRemoved = await this.askAndRemoveFromExpoAsync(chosenDevices);
|
||||
this.logChosenDevices(commandContext, chosenDevices, appleTeamName, appleTeamIdentifier);
|
||||
|
||||
const hasRemoved = await this.askAndRemoveFromExpoAsync(commandContext, chosenDevices);
|
||||
if (!hasRemoved) {
|
||||
return;
|
||||
return { jsonOutput: [] };
|
||||
}
|
||||
|
||||
await this.askAndDisableOnAppleAsync(chosenDevices, appleTeamIdentifier);
|
||||
await this.askAndDisableOnAppleAsync(commandContext, chosenDevices, appleTeamIdentifier);
|
||||
|
||||
return { jsonOutput: chosenDevices.map(it => it.id) };
|
||||
}
|
||||
|
||||
async askAndDisableOnAppleAsync(
|
||||
{ nonInteractive, logger, prompts: { toggleConfirmAsync }, ora }: CommandContext,
|
||||
chosenDevices: (AppleDevice | AppleDeviceQueryResult)[],
|
||||
appleTeamIdentifier: string
|
||||
): Promise<void> {
|
||||
Log.newLine();
|
||||
const deleteOnApple = await toggleConfirmAsync({
|
||||
message: 'Do you want to disable these devices on your Apple account as well?',
|
||||
});
|
||||
logger.newLine();
|
||||
const deleteOnApple = nonInteractive
|
||||
? true
|
||||
: await toggleConfirmAsync({
|
||||
message: 'Do you want to disable these devices on your Apple account as well?',
|
||||
});
|
||||
|
||||
if (!deleteOnApple) {
|
||||
return;
|
||||
@@ -86,7 +98,7 @@ export default class DeviceDelete extends EasCommand {
|
||||
const ctx = await authenticateAsync({ teamId: appleTeamIdentifier });
|
||||
const context = getRequestContext(ctx);
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
logger.addNewLineIfNone();
|
||||
const removeAppleSpinner = ora('Disabling devices on Apple').start();
|
||||
try {
|
||||
const chosenDeviceIdentifiers = chosenDevices.map(cd => cd.identifier);
|
||||
@@ -107,18 +119,21 @@ export default class DeviceDelete extends EasCommand {
|
||||
}
|
||||
|
||||
async askAndRemoveFromExpoAsync(
|
||||
{ nonInteractive, logger, prompts: { toggleConfirmAsync }, ora }: CommandContext,
|
||||
chosenDevices: (AppleDevice | AppleDeviceQueryResult)[]
|
||||
): Promise<boolean> {
|
||||
Log.warn(
|
||||
logger.warn(
|
||||
`You are about to remove the Apple device${
|
||||
chosenDevices.length > 1 ? 's' : ''
|
||||
} listed above from your Expo account.`
|
||||
);
|
||||
Log.newLine();
|
||||
logger.newLine();
|
||||
|
||||
const confirmed = await toggleConfirmAsync({
|
||||
message: 'Are you sure you wish to proceed?',
|
||||
});
|
||||
const confirmed = nonInteractive
|
||||
? true
|
||||
: await toggleConfirmAsync({
|
||||
message: 'Are you sure you wish to proceed?',
|
||||
});
|
||||
|
||||
if (confirmed) {
|
||||
const removalSpinner = ora(`Removing Apple devices on Expo`).start();
|
||||
@@ -137,49 +152,55 @@ export default class DeviceDelete extends EasCommand {
|
||||
}
|
||||
|
||||
logChosenDevices(
|
||||
{ logger }: CommandContext,
|
||||
chosenDevices: (AppleDevice | AppleDeviceQueryResult)[],
|
||||
appleTeamName: Maybe<string> | undefined,
|
||||
appleTeamIdentifier: string
|
||||
): void {
|
||||
Log.addNewLineIfNone();
|
||||
logger.addNewLineIfNone();
|
||||
chosenDevices.forEach(device => {
|
||||
Log.log(
|
||||
logger.log(
|
||||
formatDevice(device, {
|
||||
appleTeamName,
|
||||
appleTeamIdentifier: appleTeamIdentifier!,
|
||||
})
|
||||
);
|
||||
Log.newLine();
|
||||
logger.newLine();
|
||||
});
|
||||
}
|
||||
|
||||
async chooseDevicesToDeleteAsync(
|
||||
{ nonInteractive, logger }: CommandContext,
|
||||
appleDevices: AppleDeviceQueryResult[],
|
||||
udids: string[]
|
||||
): Promise<(AppleDevice | AppleDeviceQueryResult)[]> {
|
||||
let chosenDevices: (AppleDeviceQueryResult | AppleDevice)[] = [];
|
||||
Log.newLine();
|
||||
logger.newLine();
|
||||
if (udids) {
|
||||
udids.forEach(udid => {
|
||||
const foundDevice = appleDevices.find(device => device.identifier === udid);
|
||||
if (foundDevice) {
|
||||
chosenDevices.push(foundDevice);
|
||||
} else {
|
||||
Log.warn(`No device found with UDID ${udid}.`);
|
||||
logger.warn(`No device found with UDID ${udid}.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (chosenDevices.length === 0) {
|
||||
Log.addNewLineIfNone();
|
||||
if (nonInteractive) {
|
||||
throw new Error('Must supply `udid` paramter in non-interactive mode');
|
||||
}
|
||||
logger.addNewLineIfNone();
|
||||
chosenDevices = await chooseDevicesToDeleteAsync(appleDevices);
|
||||
Log.newLine();
|
||||
logger.newLine();
|
||||
}
|
||||
|
||||
return chosenDevices;
|
||||
}
|
||||
|
||||
async getDevicesForTeamAsync(
|
||||
{ ora }: CommandContext,
|
||||
accountName: string,
|
||||
appleTeamIdentifier: string
|
||||
): Promise<AppleDevicesByTeamIdentifierQueryResult | undefined> {
|
||||
@@ -209,7 +230,10 @@ export default class DeviceDelete extends EasCommand {
|
||||
}
|
||||
}
|
||||
|
||||
async askForAppleTeamAsync(accountName: string): Promise<string | undefined> {
|
||||
async askForAppleTeamAsync(
|
||||
{ prompts: { promptAsync }, ora }: CommandContext,
|
||||
accountName: string
|
||||
): Promise<string | undefined> {
|
||||
const teamSpinner = ora().start('Fetching the list of teams for the project…');
|
||||
|
||||
try {
|
||||
|
||||
+38
-31
@@ -9,70 +9,77 @@ type Color = (...text: string[]) => string;
|
||||
export default class Log {
|
||||
public static readonly isDebug = boolish('EXPO_DEBUG', false);
|
||||
|
||||
public static log(...args: any[]): void {
|
||||
Log.consoleLog(...args);
|
||||
constructor(private readonly jsonOutputMode: boolean) {}
|
||||
|
||||
public log(...args: any[]): void {
|
||||
this.consoleLog(...args);
|
||||
}
|
||||
|
||||
public static newLine(): void {
|
||||
Log.consoleLog();
|
||||
public newLine(): void {
|
||||
this.consoleLog();
|
||||
}
|
||||
|
||||
public static addNewLineIfNone(): void {
|
||||
if (!Log.isLastLineNewLine) {
|
||||
Log.newLine();
|
||||
public addNewLineIfNone(): void {
|
||||
if (!this.isLastLineNewLine) {
|
||||
this.newLine();
|
||||
}
|
||||
}
|
||||
|
||||
public static error(...args: any[]): void {
|
||||
Log.consoleLog(...Log.withTextColor(args, chalk.red));
|
||||
public error(...args: any[]): void {
|
||||
this.consoleLog(...this.withTextColor(args, chalk.red));
|
||||
}
|
||||
|
||||
public static warn(...args: any[]): void {
|
||||
Log.consoleLog(...Log.withTextColor(args, chalk.yellow));
|
||||
public warn(...args: any[]): void {
|
||||
this.consoleLog(...this.withTextColor(args, chalk.yellow));
|
||||
}
|
||||
|
||||
public static debug(...args: any[]): void {
|
||||
public debug(...args: any[]): void {
|
||||
if (Log.isDebug) {
|
||||
Log.consoleLog(...args);
|
||||
this.consoleLog(...args);
|
||||
}
|
||||
}
|
||||
|
||||
public static gray(...args: any[]): void {
|
||||
Log.consoleLog(...Log.withTextColor(args, chalk.gray));
|
||||
public gray(...args: any[]): void {
|
||||
this.consoleLog(...this.withTextColor(args, chalk.gray));
|
||||
}
|
||||
|
||||
public static warnDeprecatedFlag(flag: string, message: string): void {
|
||||
Log.warn(`› ${chalk.bold('--' + flag)} flag is deprecated. ${message}`);
|
||||
public warnDeprecatedFlag(flag: string, message: string): void {
|
||||
this.warn(`› ${chalk.bold('--' + flag)} flag is deprecated. ${message}`);
|
||||
}
|
||||
|
||||
public static succeed(message: string): void {
|
||||
Log.log(`${chalk.green(logSymbols.success)} ${message}`);
|
||||
public succeed(message: string): void {
|
||||
this.log(`${chalk.green(logSymbols.success)} ${message}`);
|
||||
}
|
||||
|
||||
public static withTick(...args: any[]): void {
|
||||
Log.consoleLog(chalk.green(figures.tick), ...args);
|
||||
public withTick(...args: any[]): void {
|
||||
this.consoleLog(chalk.green(figures.tick), ...args);
|
||||
}
|
||||
|
||||
private static consoleLog(...args: any[]): void {
|
||||
Log.updateIsLastLineNewLine(args);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(...args);
|
||||
private consoleLog(...args: any[]): void {
|
||||
this.updateIsLastLineNewLine(args);
|
||||
if (this.jsonOutputMode) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(...args);
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(...args);
|
||||
}
|
||||
}
|
||||
|
||||
private static withTextColor(args: any[], chalkColor: Color): string[] {
|
||||
private withTextColor(args: any[], chalkColor: Color): string[] {
|
||||
return args.map(arg => chalkColor(arg));
|
||||
}
|
||||
|
||||
private static isLastLineNewLine = false;
|
||||
private static updateIsLastLineNewLine(args: any[]): void {
|
||||
private isLastLineNewLine = false;
|
||||
private updateIsLastLineNewLine(args: any[]): void {
|
||||
if (args.length === 0) {
|
||||
Log.isLastLineNewLine = true;
|
||||
this.isLastLineNewLine = true;
|
||||
} else {
|
||||
const lastArg = args[args.length - 1];
|
||||
if (typeof lastArg === 'string' && (lastArg === '' || lastArg.match(/[\r\n]$/))) {
|
||||
Log.isLastLineNewLine = true;
|
||||
this.isLastLineNewLine = true;
|
||||
} else {
|
||||
Log.isLastLineNewLine = false;
|
||||
this.isLastLineNewLine = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,28 @@ import oraReal, { Options, Ora } from 'ora';
|
||||
|
||||
import Log from './log';
|
||||
|
||||
export function nonInteractiveOra(_options?: Options | string): Ora {
|
||||
const stubOra: Ora = {
|
||||
text: '',
|
||||
prefixText: '',
|
||||
color: 'black',
|
||||
spinner: 'arc',
|
||||
indent: 0,
|
||||
isSpinning: false,
|
||||
start: () => stubOra,
|
||||
stop: () => stubOra,
|
||||
succeed: () => stubOra,
|
||||
fail: () => stubOra,
|
||||
warn: () => stubOra,
|
||||
info: () => stubOra,
|
||||
stopAndPersist: () => stubOra,
|
||||
clear: () => stubOra,
|
||||
render: () => stubOra,
|
||||
frame: () => '',
|
||||
};
|
||||
return stubOra;
|
||||
}
|
||||
|
||||
export { Ora, Options };
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
@@ -20,7 +42,7 @@ const errorReal = console.error;
|
||||
* @param options
|
||||
* @returns
|
||||
*/
|
||||
export function ora(options?: Options | string): Ora {
|
||||
export function interactiveOra(options?: Options | string): Ora {
|
||||
const inputOptions = typeof options === 'string' ? { text: options } : options ?? {};
|
||||
const disabled = Log.isDebug || !process.stdin.isTTY;
|
||||
const spinner = oraReal({
|
||||
|
||||
+120
-79
@@ -9,87 +9,128 @@ export interface ExpoChoice<T> extends Choice {
|
||||
|
||||
type NamelessQuestion = Omit<Question<'value'>, 'name' | 'type'>;
|
||||
|
||||
export async function promptAsync<T extends string = string>(
|
||||
questions: Question<T> | Question<T>[],
|
||||
options: Options = {}
|
||||
): Promise<Answers<T>> {
|
||||
if (!process.stdin.isTTY && !global.test) {
|
||||
throw new Error('Input is required, but stdin is not readable.');
|
||||
export interface Prompts {
|
||||
promptAsync<T extends string = string>(
|
||||
questions: Question<T> | Question<T>[],
|
||||
options?: Options
|
||||
): Promise<Answers<T>>;
|
||||
confirmAsync(question: NamelessQuestion, options?: Options): Promise<boolean>;
|
||||
selectAsync<T>(message: string, choices: ExpoChoice<T>[], options?: Options): Promise<T>;
|
||||
toggleConfirmAsync(questions: NamelessQuestion, options?: Options): Promise<boolean>;
|
||||
pressAnyKeyToContinueAsync(): Promise<void>;
|
||||
}
|
||||
|
||||
export class NonInteractivePrompts implements Prompts {
|
||||
private static errorMessage =
|
||||
'Command can not be run as specified in non-interactive mode. Run the command with --help to see arguments and flags required to run in non-interactive mode.';
|
||||
|
||||
async promptAsync<T extends string = string>(
|
||||
_questions: prompts.PromptObject<T> | prompts.PromptObject<T>[],
|
||||
_options: prompts.Options
|
||||
): Promise<prompts.Answers<T>> {
|
||||
throw new Error(NonInteractivePrompts.errorMessage);
|
||||
}
|
||||
|
||||
async confirmAsync(
|
||||
_question: NamelessQuestion,
|
||||
_options?: prompts.Options | undefined
|
||||
): Promise<boolean> {
|
||||
throw new Error(NonInteractivePrompts.errorMessage);
|
||||
}
|
||||
|
||||
async selectAsync<T>(
|
||||
_message: string,
|
||||
_choices: ExpoChoice<T>[],
|
||||
_options?: prompts.Options | undefined
|
||||
): Promise<T> {
|
||||
throw new Error(NonInteractivePrompts.errorMessage);
|
||||
}
|
||||
|
||||
async toggleConfirmAsync(
|
||||
_questions: NamelessQuestion,
|
||||
_options?: prompts.Options | undefined
|
||||
): Promise<boolean> {
|
||||
throw new Error(NonInteractivePrompts.errorMessage);
|
||||
}
|
||||
|
||||
async pressAnyKeyToContinueAsync(): Promise<void> {
|
||||
throw new Error(NonInteractivePrompts.errorMessage);
|
||||
}
|
||||
return await prompts<T>(questions, {
|
||||
onCancel() {
|
||||
process.exit(constants.signals.SIGINT + 128); // Exit code 130 used when process is interrupted with ctrl+c.
|
||||
},
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
export async function confirmAsync(
|
||||
question: NamelessQuestion,
|
||||
options?: Options
|
||||
): Promise<boolean> {
|
||||
const { value } = await promptAsync(
|
||||
{
|
||||
initial: true,
|
||||
...question,
|
||||
name: 'value',
|
||||
type: 'confirm',
|
||||
},
|
||||
options
|
||||
);
|
||||
return value;
|
||||
}
|
||||
export async function selectAsync<T>(
|
||||
message: string,
|
||||
choices: ExpoChoice<T>[],
|
||||
options?: Options
|
||||
): Promise<T> {
|
||||
const { value } = await promptAsync(
|
||||
{
|
||||
message,
|
||||
choices,
|
||||
name: 'value',
|
||||
type: 'select',
|
||||
},
|
||||
options
|
||||
);
|
||||
return value ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a more dynamic yes/no confirmation that can be cancelled.
|
||||
*
|
||||
* @param questions
|
||||
* @param options
|
||||
*/
|
||||
export async function toggleConfirmAsync(
|
||||
questions: NamelessQuestion,
|
||||
options?: Options
|
||||
): Promise<boolean> {
|
||||
const { value } = await promptAsync(
|
||||
{
|
||||
active: 'yes',
|
||||
inactive: 'no',
|
||||
...questions,
|
||||
name: 'value',
|
||||
type: 'toggle',
|
||||
},
|
||||
options
|
||||
);
|
||||
return value ?? null;
|
||||
}
|
||||
|
||||
export async function pressAnyKeyToContinueAsync(): Promise<void> {
|
||||
process.stdin.setRawMode(true);
|
||||
process.stdin.resume();
|
||||
process.stdin.setEncoding('utf8');
|
||||
|
||||
await new Promise<void>(res => {
|
||||
process.stdin.on('data', key => {
|
||||
if (String(key) === '\u0003') {
|
||||
process.exit(constants.signals.SIGINT + 128); // ctrl-c
|
||||
}
|
||||
res();
|
||||
export class InteractivePrompts implements Prompts {
|
||||
async promptAsync<T extends string = string>(
|
||||
questions: Question<T> | Question<T>[],
|
||||
options: Options = {}
|
||||
): Promise<Answers<T>> {
|
||||
if (!process.stdin.isTTY && !global.test) {
|
||||
throw new Error('Input is required, but stdin is not readable.');
|
||||
}
|
||||
return await prompts<T>(questions, {
|
||||
onCancel() {
|
||||
process.exit(constants.signals.SIGINT + 128); // Exit code 130 used when process is interrupted with ctrl+c.
|
||||
},
|
||||
...options,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async confirmAsync(question: NamelessQuestion, options?: Options): Promise<boolean> {
|
||||
const { value } = await this.promptAsync(
|
||||
{
|
||||
initial: true,
|
||||
...question,
|
||||
name: 'value',
|
||||
type: 'confirm',
|
||||
},
|
||||
options
|
||||
);
|
||||
return value;
|
||||
}
|
||||
async selectAsync<T>(message: string, choices: ExpoChoice<T>[], options?: Options): Promise<T> {
|
||||
const { value } = await this.promptAsync(
|
||||
{
|
||||
message,
|
||||
choices,
|
||||
name: 'value',
|
||||
type: 'select',
|
||||
},
|
||||
options
|
||||
);
|
||||
return value ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a more dynamic yes/no confirmation that can be cancelled.
|
||||
*
|
||||
* @param questions
|
||||
* @param options
|
||||
*/
|
||||
async toggleConfirmAsync(questions: NamelessQuestion, options?: Options): Promise<boolean> {
|
||||
const { value } = await this.promptAsync(
|
||||
{
|
||||
active: 'yes',
|
||||
inactive: 'no',
|
||||
...questions,
|
||||
name: 'value',
|
||||
type: 'toggle',
|
||||
},
|
||||
options
|
||||
);
|
||||
return value ?? null;
|
||||
}
|
||||
|
||||
async pressAnyKeyToContinueAsync(): Promise<void> {
|
||||
process.stdin.setRawMode(true);
|
||||
process.stdin.resume();
|
||||
process.stdin.setEncoding('utf8');
|
||||
|
||||
await new Promise<void>(res => {
|
||||
process.stdin.on('data', key => {
|
||||
if (String(key) === '\u0003') {
|
||||
process.exit(constants.signals.SIGINT + 128); // ctrl-c
|
||||
}
|
||||
res();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import assert from 'assert';
|
||||
|
||||
import Log from '../log';
|
||||
|
||||
let stdoutWrite: NodeJS.WriteStream['write'] | undefined;
|
||||
|
||||
export function enableJsonOutput(): void {
|
||||
if (stdoutWrite) {
|
||||
return;
|
||||
}
|
||||
stdoutWrite = process.stdout.write;
|
||||
process.stdout.write = process.stderr.write.bind(process.stderr);
|
||||
}
|
||||
|
||||
export function printJsonOnlyOutput(value: object): void {
|
||||
assert(stdoutWrite, 'this should only be called with --json flag');
|
||||
try {
|
||||
process.stdout.write = stdoutWrite;
|
||||
Log.log(JSON.stringify(sanitizeValue(value), null, 2));
|
||||
} finally {
|
||||
process.stdout.write = process.stderr.write.bind(process.stderr);
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeValue(value: any): unknown {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(val => sanitizeValue(val));
|
||||
} else if (value && typeof value === 'object') {
|
||||
const result: Record<string, any> = {};
|
||||
Object.keys(value).forEach(key => {
|
||||
if (key !== '__typename' && value[key] !== null) {
|
||||
result[key] = sanitizeValue(value[key]);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user