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 { EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
|
||||||
import * as PackageManagerUtils from '@expo/package-manager';
|
import * as PackageManagerUtils from '@expo/package-manager';
|
||||||
import { Command } from '@oclif/core';
|
import { Command, Flags } from '@oclif/core';
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
|
// eslint-disable-next-line no-restricted-imports
|
||||||
|
import { Options, Ora } from 'ora';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import semver from 'semver';
|
import semver from 'semver';
|
||||||
|
|
||||||
@@ -12,15 +14,48 @@ import {
|
|||||||
initAsync as initAnalyticsAsync,
|
initAsync as initAnalyticsAsync,
|
||||||
logEvent,
|
logEvent,
|
||||||
} from '../analytics/rudderstackClient';
|
} from '../analytics/rudderstackClient';
|
||||||
import { learnMore } from '../log';
|
import Log, { learnMore } from '../log';
|
||||||
|
import { interactiveOra, nonInteractiveOra } from '../ora';
|
||||||
import { findProjectRootAsync } from '../project/projectUtils';
|
import { findProjectRootAsync } from '../project/projectUtils';
|
||||||
|
import { InteractivePrompts, NonInteractivePrompts, Prompts } from '../prompts';
|
||||||
import { getUserAsync } from '../user/User';
|
import { getUserAsync } from '../user/User';
|
||||||
import { ensureLoggedInAsync } from '../user/actions';
|
import { ensureLoggedInAsync } from '../user/actions';
|
||||||
import { easCliVersion } from '../utils/easCli';
|
import { easCliVersion } from '../utils/easCli';
|
||||||
import { setVcsClient } from '../vcs';
|
import { setVcsClient } from '../vcs';
|
||||||
import GitClient from '../vcs/clients/git';
|
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 {
|
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
|
* When user data is unavailable locally, determines if the command will
|
||||||
* force the user to log in
|
* force the user to log in
|
||||||
@@ -28,10 +63,26 @@ export default abstract class EasCommand extends Command {
|
|||||||
protected requiresAuthentication = true;
|
protected requiresAuthentication = true;
|
||||||
protected mustBeRunInsideProject = 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
|
// eslint-disable-next-line async-protect/async-suffix
|
||||||
async run(): Promise<any> {
|
async run(): Promise<any> {
|
||||||
|
const { flags } = await this.parse();
|
||||||
|
const json = flags.json ?? false;
|
||||||
|
const nonInteractive = (flags as any)['non-interactive'] ?? false;
|
||||||
|
|
||||||
await initAnalyticsAsync();
|
await initAnalyticsAsync();
|
||||||
|
|
||||||
if (this.mustBeRunInsideProject) {
|
if (this.mustBeRunInsideProject) {
|
||||||
@@ -41,8 +92,6 @@ export default abstract class EasCommand extends Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.requiresAuthentication) {
|
if (this.requiresAuthentication) {
|
||||||
const { flags } = await this.parse();
|
|
||||||
const nonInteractive = (flags as any)['non-interactive'] ?? false;
|
|
||||||
await ensureLoggedInAsync({ nonInteractive });
|
await ensureLoggedInAsync({ nonInteractive });
|
||||||
} else {
|
} else {
|
||||||
await getUserAsync();
|
await getUserAsync();
|
||||||
@@ -52,7 +101,18 @@ export default abstract class EasCommand extends Command {
|
|||||||
// commands/submit === submit, commands/build/list === build:list
|
// commands/submit === submit, commands/build/list === build:list
|
||||||
action: `eas ${this.id}`,
|
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
|
// eslint-disable-next-line async-protect/async-suffix
|
||||||
@@ -119,4 +179,26 @@ export default abstract class EasCommand extends Command {
|
|||||||
packageJson?.devDependencies?.['eas-cli'] !== undefined
|
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';
|
import { Flags } from '@oclif/core';
|
||||||
|
|
||||||
export const getPaginatedQueryOptions = (
|
export const getPaginatedQueryOptions = (flags: {
|
||||||
flags: Record<keyof typeof EasPaginatedQueryFlags, any>
|
json: boolean;
|
||||||
): PaginatedQueryOptions => {
|
'non-interactive': boolean;
|
||||||
|
offset: number | undefined;
|
||||||
|
limit: number | undefined;
|
||||||
|
}): PaginatedQueryOptions => {
|
||||||
return {
|
return {
|
||||||
json: flags.json ?? false,
|
json: flags.json,
|
||||||
offset: flags.offset ?? 0,
|
offset: flags.offset ?? 0,
|
||||||
nonInteractive: flags['non-interactive'] ?? false,
|
nonInteractive: flags['non-interactive'],
|
||||||
...('limit' in flags && { limit: flags.limit }),
|
...('limit' in flags && { limit: flags.limit }),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -40,12 +43,6 @@ export const EasPaginatedQueryFlags = {
|
|||||||
// eslint-disable-next-line async-protect/async-suffix
|
// eslint-disable-next-line async-protect/async-suffix
|
||||||
parse: async input => parseFlagInputStringAsInteger(input, 'limit', 1, 100),
|
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
|
// options required to control a paginated query
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { Flags } from '@oclif/core';
|
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import gql from 'graphql-tag';
|
import gql from 'graphql-tag';
|
||||||
|
|
||||||
import EasCommand from '../../commandUtils/EasCommand';
|
import EasCommand, { CommandContext } from '../../commandUtils/EasCommand';
|
||||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||||
import {
|
import {
|
||||||
DeleteUpdateBranchMutation,
|
DeleteUpdateBranchMutation,
|
||||||
@@ -11,15 +10,12 @@ import {
|
|||||||
GetBranchInfoQuery,
|
GetBranchInfoQuery,
|
||||||
GetBranchInfoQueryVariables,
|
GetBranchInfoQueryVariables,
|
||||||
} from '../../graphql/generated';
|
} from '../../graphql/generated';
|
||||||
import Log from '../../log';
|
|
||||||
import { getExpoConfig } from '../../project/expoConfig';
|
import { getExpoConfig } from '../../project/expoConfig';
|
||||||
import {
|
import {
|
||||||
findProjectRootAsync,
|
findProjectRootAsync,
|
||||||
getProjectFullNameAsync,
|
getProjectFullNameAsync,
|
||||||
getProjectIdAsync,
|
getProjectIdAsync,
|
||||||
} from '../../project/projectUtils';
|
} from '../../project/projectUtils';
|
||||||
import { promptAsync, toggleConfirmAsync } from '../../prompts';
|
|
||||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
|
||||||
|
|
||||||
async function getBranchInfoAsync({
|
async function getBranchInfoAsync({
|
||||||
appId,
|
appId,
|
||||||
@@ -86,21 +82,15 @@ export default class BranchDelete extends EasCommand {
|
|||||||
description: 'Name of the branch to delete',
|
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 {
|
let {
|
||||||
args: { name },
|
args: { name },
|
||||||
flags: { json: jsonFlag },
|
|
||||||
} = await this.parse(BranchDelete);
|
} = await this.parse(BranchDelete);
|
||||||
if (jsonFlag) {
|
|
||||||
enableJsonOutput();
|
|
||||||
}
|
|
||||||
|
|
||||||
const projectDir = await findProjectRootAsync();
|
const projectDir = await findProjectRootAsync();
|
||||||
const exp = getExpoConfig(projectDir);
|
const exp = getExpoConfig(projectDir);
|
||||||
@@ -109,7 +99,7 @@ export default class BranchDelete extends EasCommand {
|
|||||||
|
|
||||||
if (!name) {
|
if (!name) {
|
||||||
const validationMessage = 'branch name may not be empty.';
|
const validationMessage = 'branch name may not be empty.';
|
||||||
if (jsonFlag) {
|
if (nonInteractive) {
|
||||||
throw new Error(validationMessage);
|
throw new Error(validationMessage);
|
||||||
}
|
}
|
||||||
({ name } = await promptAsync({
|
({ name } = await promptAsync({
|
||||||
@@ -126,16 +116,16 @@ export default class BranchDelete extends EasCommand {
|
|||||||
throw new Error(`Could not find branch ${name} on ${fullName}`);
|
throw new Error(`Could not find branch ${name} on ${fullName}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!jsonFlag) {
|
if (!nonInteractive) {
|
||||||
Log.addNewLineIfNone();
|
logger.addNewLineIfNone();
|
||||||
Log.warn(
|
logger.warn(
|
||||||
`You are about to permanently delete branch: "${name}" and all of the updates published on it.` +
|
`You are about to permanently delete branch: "${name}" and all of the updates published on it.` +
|
||||||
`\nThis action is irreversible.`
|
`\nThis action is irreversible.`
|
||||||
);
|
);
|
||||||
Log.newLine();
|
logger.newLine();
|
||||||
const confirmed = await toggleConfirmAsync({ message: 'Are you sure you wish to proceed?' });
|
const confirmed = await toggleConfirmAsync({ message: 'Are you sure you wish to proceed?' });
|
||||||
if (!confirmed) {
|
if (!confirmed) {
|
||||||
Log.error(`Cancelled deletion of branch: "${name}".`);
|
logger.error(`Cancelled deletion of branch: "${name}".`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -144,12 +134,10 @@ export default class BranchDelete extends EasCommand {
|
|||||||
branchId,
|
branchId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (jsonFlag) {
|
logger.withTick(
|
||||||
printJsonOnlyOutput(deletionResult);
|
`Deleted branch "${name}" and all of its updates on project ${chalk.bold(fullName)}.`
|
||||||
} else {
|
);
|
||||||
Log.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 { Flags } from '@oclif/core';
|
||||||
import assert from 'assert';
|
import assert from 'assert';
|
||||||
|
|
||||||
import EasCommand from '../../commandUtils/EasCommand';
|
import EasCommand, { CommandContext } from '../../commandUtils/EasCommand';
|
||||||
import { chooseDevicesToDeleteAsync } from '../../credentials/ios/actions/DeviceUtils';
|
import { chooseDevicesToDeleteAsync } from '../../credentials/ios/actions/DeviceUtils';
|
||||||
import { AppleDeviceMutation } from '../../credentials/ios/api/graphql/mutations/AppleDeviceMutation';
|
import { AppleDeviceMutation } from '../../credentials/ios/api/graphql/mutations/AppleDeviceMutation';
|
||||||
import {
|
import {
|
||||||
@@ -14,11 +14,8 @@ import { AppleTeamQuery } from '../../credentials/ios/api/graphql/queries/AppleT
|
|||||||
import { authenticateAsync, getRequestContext } from '../../credentials/ios/appstore/authenticate';
|
import { authenticateAsync, getRequestContext } from '../../credentials/ios/appstore/authenticate';
|
||||||
import formatDevice from '../../devices/utils/formatDevice';
|
import formatDevice from '../../devices/utils/formatDevice';
|
||||||
import { AppleDevice, Maybe } from '../../graphql/generated';
|
import { AppleDevice, Maybe } from '../../graphql/generated';
|
||||||
import Log from '../../log';
|
|
||||||
import { ora } from '../../ora';
|
|
||||||
import { getExpoConfig } from '../../project/expoConfig';
|
import { getExpoConfig } from '../../project/expoConfig';
|
||||||
import { findProjectRootAsync, getProjectAccountNameAsync } from '../../project/projectUtils';
|
import { findProjectRootAsync, getProjectAccountNameAsync } from '../../project/projectUtils';
|
||||||
import { promptAsync, toggleConfirmAsync } from '../../prompts';
|
|
||||||
|
|
||||||
export default class DeviceDelete extends EasCommand {
|
export default class DeviceDelete extends EasCommand {
|
||||||
static override description = 'remove a registered device from your account';
|
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 }),
|
udid: Flags.string({ multiple: true }),
|
||||||
};
|
};
|
||||||
|
|
||||||
async runAsync(): Promise<void> {
|
protected async runAsync(commandContext: CommandContext): Promise<{ jsonOutput: object }> {
|
||||||
let {
|
let {
|
||||||
flags: { 'apple-team-id': appleTeamIdentifier, udid: udids },
|
flags: { 'apple-team-id': appleTeamIdentifier, udid: udids },
|
||||||
} = await this.parse(DeviceDelete);
|
} = await this.parse(DeviceDelete);
|
||||||
@@ -38,46 +35,61 @@ export default class DeviceDelete extends EasCommand {
|
|||||||
const accountName = await getProjectAccountNameAsync(exp);
|
const accountName = await getProjectAccountNameAsync(exp);
|
||||||
|
|
||||||
if (!appleTeamIdentifier) {
|
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');
|
assert(appleTeamIdentifier, 'No team identifier is specified');
|
||||||
|
|
||||||
const appleDevicesResult = await this.getDevicesForTeamAsync(accountName, appleTeamIdentifier);
|
const appleDevicesResult = await this.getDevicesForTeamAsync(
|
||||||
|
commandContext,
|
||||||
|
accountName,
|
||||||
|
appleTeamIdentifier
|
||||||
|
);
|
||||||
|
|
||||||
if (!appleDevicesResult) {
|
if (!appleDevicesResult) {
|
||||||
return;
|
return { jsonOutput: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
const { appleTeamName, appleDevices } = appleDevicesResult;
|
const { appleTeamName, appleDevices } = appleDevicesResult;
|
||||||
|
|
||||||
const chosenDevices = await this.chooseDevicesToDeleteAsync(appleDevices, udids);
|
const chosenDevices = await this.chooseDevicesToDeleteAsync(
|
||||||
|
commandContext,
|
||||||
|
appleDevices,
|
||||||
|
udids
|
||||||
|
);
|
||||||
|
|
||||||
if (chosenDevices.length === 0) {
|
if (chosenDevices.length === 0) {
|
||||||
Log.newLine();
|
commandContext.logger.newLine();
|
||||||
Log.warn('No devices were chosen to be removed.');
|
commandContext.logger.warn('No devices were chosen to be removed.');
|
||||||
return;
|
return { jsonOutput: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logChosenDevices(chosenDevices, appleTeamName, appleTeamIdentifier);
|
this.logChosenDevices(commandContext, chosenDevices, appleTeamName, appleTeamIdentifier);
|
||||||
|
|
||||||
const hasRemoved = await this.askAndRemoveFromExpoAsync(chosenDevices);
|
|
||||||
|
|
||||||
|
const hasRemoved = await this.askAndRemoveFromExpoAsync(commandContext, chosenDevices);
|
||||||
if (!hasRemoved) {
|
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(
|
async askAndDisableOnAppleAsync(
|
||||||
|
{ nonInteractive, logger, prompts: { toggleConfirmAsync }, ora }: CommandContext,
|
||||||
chosenDevices: (AppleDevice | AppleDeviceQueryResult)[],
|
chosenDevices: (AppleDevice | AppleDeviceQueryResult)[],
|
||||||
appleTeamIdentifier: string
|
appleTeamIdentifier: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
Log.newLine();
|
logger.newLine();
|
||||||
const deleteOnApple = await toggleConfirmAsync({
|
const deleteOnApple = nonInteractive
|
||||||
message: 'Do you want to disable these devices on your Apple account as well?',
|
? true
|
||||||
});
|
: await toggleConfirmAsync({
|
||||||
|
message: 'Do you want to disable these devices on your Apple account as well?',
|
||||||
|
});
|
||||||
|
|
||||||
if (!deleteOnApple) {
|
if (!deleteOnApple) {
|
||||||
return;
|
return;
|
||||||
@@ -86,7 +98,7 @@ export default class DeviceDelete extends EasCommand {
|
|||||||
const ctx = await authenticateAsync({ teamId: appleTeamIdentifier });
|
const ctx = await authenticateAsync({ teamId: appleTeamIdentifier });
|
||||||
const context = getRequestContext(ctx);
|
const context = getRequestContext(ctx);
|
||||||
|
|
||||||
Log.addNewLineIfNone();
|
logger.addNewLineIfNone();
|
||||||
const removeAppleSpinner = ora('Disabling devices on Apple').start();
|
const removeAppleSpinner = ora('Disabling devices on Apple').start();
|
||||||
try {
|
try {
|
||||||
const chosenDeviceIdentifiers = chosenDevices.map(cd => cd.identifier);
|
const chosenDeviceIdentifiers = chosenDevices.map(cd => cd.identifier);
|
||||||
@@ -107,18 +119,21 @@ export default class DeviceDelete extends EasCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async askAndRemoveFromExpoAsync(
|
async askAndRemoveFromExpoAsync(
|
||||||
|
{ nonInteractive, logger, prompts: { toggleConfirmAsync }, ora }: CommandContext,
|
||||||
chosenDevices: (AppleDevice | AppleDeviceQueryResult)[]
|
chosenDevices: (AppleDevice | AppleDeviceQueryResult)[]
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
Log.warn(
|
logger.warn(
|
||||||
`You are about to remove the Apple device${
|
`You are about to remove the Apple device${
|
||||||
chosenDevices.length > 1 ? 's' : ''
|
chosenDevices.length > 1 ? 's' : ''
|
||||||
} listed above from your Expo account.`
|
} listed above from your Expo account.`
|
||||||
);
|
);
|
||||||
Log.newLine();
|
logger.newLine();
|
||||||
|
|
||||||
const confirmed = await toggleConfirmAsync({
|
const confirmed = nonInteractive
|
||||||
message: 'Are you sure you wish to proceed?',
|
? true
|
||||||
});
|
: await toggleConfirmAsync({
|
||||||
|
message: 'Are you sure you wish to proceed?',
|
||||||
|
});
|
||||||
|
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
const removalSpinner = ora(`Removing Apple devices on Expo`).start();
|
const removalSpinner = ora(`Removing Apple devices on Expo`).start();
|
||||||
@@ -137,49 +152,55 @@ export default class DeviceDelete extends EasCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logChosenDevices(
|
logChosenDevices(
|
||||||
|
{ logger }: CommandContext,
|
||||||
chosenDevices: (AppleDevice | AppleDeviceQueryResult)[],
|
chosenDevices: (AppleDevice | AppleDeviceQueryResult)[],
|
||||||
appleTeamName: Maybe<string> | undefined,
|
appleTeamName: Maybe<string> | undefined,
|
||||||
appleTeamIdentifier: string
|
appleTeamIdentifier: string
|
||||||
): void {
|
): void {
|
||||||
Log.addNewLineIfNone();
|
logger.addNewLineIfNone();
|
||||||
chosenDevices.forEach(device => {
|
chosenDevices.forEach(device => {
|
||||||
Log.log(
|
logger.log(
|
||||||
formatDevice(device, {
|
formatDevice(device, {
|
||||||
appleTeamName,
|
appleTeamName,
|
||||||
appleTeamIdentifier: appleTeamIdentifier!,
|
appleTeamIdentifier: appleTeamIdentifier!,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
Log.newLine();
|
logger.newLine();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async chooseDevicesToDeleteAsync(
|
async chooseDevicesToDeleteAsync(
|
||||||
|
{ nonInteractive, logger }: CommandContext,
|
||||||
appleDevices: AppleDeviceQueryResult[],
|
appleDevices: AppleDeviceQueryResult[],
|
||||||
udids: string[]
|
udids: string[]
|
||||||
): Promise<(AppleDevice | AppleDeviceQueryResult)[]> {
|
): Promise<(AppleDevice | AppleDeviceQueryResult)[]> {
|
||||||
let chosenDevices: (AppleDeviceQueryResult | AppleDevice)[] = [];
|
let chosenDevices: (AppleDeviceQueryResult | AppleDevice)[] = [];
|
||||||
Log.newLine();
|
logger.newLine();
|
||||||
if (udids) {
|
if (udids) {
|
||||||
udids.forEach(udid => {
|
udids.forEach(udid => {
|
||||||
const foundDevice = appleDevices.find(device => device.identifier === udid);
|
const foundDevice = appleDevices.find(device => device.identifier === udid);
|
||||||
if (foundDevice) {
|
if (foundDevice) {
|
||||||
chosenDevices.push(foundDevice);
|
chosenDevices.push(foundDevice);
|
||||||
} else {
|
} else {
|
||||||
Log.warn(`No device found with UDID ${udid}.`);
|
logger.warn(`No device found with UDID ${udid}.`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (chosenDevices.length === 0) {
|
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);
|
chosenDevices = await chooseDevicesToDeleteAsync(appleDevices);
|
||||||
Log.newLine();
|
logger.newLine();
|
||||||
}
|
}
|
||||||
|
|
||||||
return chosenDevices;
|
return chosenDevices;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDevicesForTeamAsync(
|
async getDevicesForTeamAsync(
|
||||||
|
{ ora }: CommandContext,
|
||||||
accountName: string,
|
accountName: string,
|
||||||
appleTeamIdentifier: string
|
appleTeamIdentifier: string
|
||||||
): Promise<AppleDevicesByTeamIdentifierQueryResult | undefined> {
|
): 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…');
|
const teamSpinner = ora().start('Fetching the list of teams for the project…');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
+38
-31
@@ -9,70 +9,77 @@ type Color = (...text: string[]) => string;
|
|||||||
export default class Log {
|
export default class Log {
|
||||||
public static readonly isDebug = boolish('EXPO_DEBUG', false);
|
public static readonly isDebug = boolish('EXPO_DEBUG', false);
|
||||||
|
|
||||||
public static log(...args: any[]): void {
|
constructor(private readonly jsonOutputMode: boolean) {}
|
||||||
Log.consoleLog(...args);
|
|
||||||
|
public log(...args: any[]): void {
|
||||||
|
this.consoleLog(...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static newLine(): void {
|
public newLine(): void {
|
||||||
Log.consoleLog();
|
this.consoleLog();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static addNewLineIfNone(): void {
|
public addNewLineIfNone(): void {
|
||||||
if (!Log.isLastLineNewLine) {
|
if (!this.isLastLineNewLine) {
|
||||||
Log.newLine();
|
this.newLine();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static error(...args: any[]): void {
|
public error(...args: any[]): void {
|
||||||
Log.consoleLog(...Log.withTextColor(args, chalk.red));
|
this.consoleLog(...this.withTextColor(args, chalk.red));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static warn(...args: any[]): void {
|
public warn(...args: any[]): void {
|
||||||
Log.consoleLog(...Log.withTextColor(args, chalk.yellow));
|
this.consoleLog(...this.withTextColor(args, chalk.yellow));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static debug(...args: any[]): void {
|
public debug(...args: any[]): void {
|
||||||
if (Log.isDebug) {
|
if (Log.isDebug) {
|
||||||
Log.consoleLog(...args);
|
this.consoleLog(...args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static gray(...args: any[]): void {
|
public gray(...args: any[]): void {
|
||||||
Log.consoleLog(...Log.withTextColor(args, chalk.gray));
|
this.consoleLog(...this.withTextColor(args, chalk.gray));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static warnDeprecatedFlag(flag: string, message: string): void {
|
public warnDeprecatedFlag(flag: string, message: string): void {
|
||||||
Log.warn(`› ${chalk.bold('--' + flag)} flag is deprecated. ${message}`);
|
this.warn(`› ${chalk.bold('--' + flag)} flag is deprecated. ${message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static succeed(message: string): void {
|
public succeed(message: string): void {
|
||||||
Log.log(`${chalk.green(logSymbols.success)} ${message}`);
|
this.log(`${chalk.green(logSymbols.success)} ${message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static withTick(...args: any[]): void {
|
public withTick(...args: any[]): void {
|
||||||
Log.consoleLog(chalk.green(figures.tick), ...args);
|
this.consoleLog(chalk.green(figures.tick), ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static consoleLog(...args: any[]): void {
|
private consoleLog(...args: any[]): void {
|
||||||
Log.updateIsLastLineNewLine(args);
|
this.updateIsLastLineNewLine(args);
|
||||||
// eslint-disable-next-line no-console
|
if (this.jsonOutputMode) {
|
||||||
console.log(...args);
|
// 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));
|
return args.map(arg => chalkColor(arg));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static isLastLineNewLine = false;
|
private isLastLineNewLine = false;
|
||||||
private static updateIsLastLineNewLine(args: any[]): void {
|
private updateIsLastLineNewLine(args: any[]): void {
|
||||||
if (args.length === 0) {
|
if (args.length === 0) {
|
||||||
Log.isLastLineNewLine = true;
|
this.isLastLineNewLine = true;
|
||||||
} else {
|
} else {
|
||||||
const lastArg = args[args.length - 1];
|
const lastArg = args[args.length - 1];
|
||||||
if (typeof lastArg === 'string' && (lastArg === '' || lastArg.match(/[\r\n]$/))) {
|
if (typeof lastArg === 'string' && (lastArg === '' || lastArg.match(/[\r\n]$/))) {
|
||||||
Log.isLastLineNewLine = true;
|
this.isLastLineNewLine = true;
|
||||||
} else {
|
} else {
|
||||||
Log.isLastLineNewLine = false;
|
this.isLastLineNewLine = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,28 @@ import oraReal, { Options, Ora } from 'ora';
|
|||||||
|
|
||||||
import Log from './log';
|
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 };
|
export { Ora, Options };
|
||||||
|
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
@@ -20,7 +42,7 @@ const errorReal = console.error;
|
|||||||
* @param options
|
* @param options
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function ora(options?: Options | string): Ora {
|
export function interactiveOra(options?: Options | string): Ora {
|
||||||
const inputOptions = typeof options === 'string' ? { text: options } : options ?? {};
|
const inputOptions = typeof options === 'string' ? { text: options } : options ?? {};
|
||||||
const disabled = Log.isDebug || !process.stdin.isTTY;
|
const disabled = Log.isDebug || !process.stdin.isTTY;
|
||||||
const spinner = oraReal({
|
const spinner = oraReal({
|
||||||
|
|||||||
+120
-79
@@ -9,87 +9,128 @@ export interface ExpoChoice<T> extends Choice {
|
|||||||
|
|
||||||
type NamelessQuestion = Omit<Question<'value'>, 'name' | 'type'>;
|
type NamelessQuestion = Omit<Question<'value'>, 'name' | 'type'>;
|
||||||
|
|
||||||
export async function promptAsync<T extends string = string>(
|
export interface Prompts {
|
||||||
questions: Question<T> | Question<T>[],
|
promptAsync<T extends string = string>(
|
||||||
options: Options = {}
|
questions: Question<T> | Question<T>[],
|
||||||
): Promise<Answers<T>> {
|
options?: Options
|
||||||
if (!process.stdin.isTTY && !global.test) {
|
): Promise<Answers<T>>;
|
||||||
throw new Error('Input is required, but stdin is not readable.');
|
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(
|
export class InteractivePrompts implements Prompts {
|
||||||
question: NamelessQuestion,
|
async promptAsync<T extends string = string>(
|
||||||
options?: Options
|
questions: Question<T> | Question<T>[],
|
||||||
): Promise<boolean> {
|
options: Options = {}
|
||||||
const { value } = await promptAsync(
|
): Promise<Answers<T>> {
|
||||||
{
|
if (!process.stdin.isTTY && !global.test) {
|
||||||
initial: true,
|
throw new Error('Input is required, but stdin is not readable.');
|
||||||
...question,
|
}
|
||||||
name: 'value',
|
return await prompts<T>(questions, {
|
||||||
type: 'confirm',
|
onCancel() {
|
||||||
},
|
process.exit(constants.signals.SIGINT + 128); // Exit code 130 used when process is interrupted with ctrl+c.
|
||||||
options
|
},
|
||||||
);
|
...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();
|
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
|
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