Merge pull request #2312 from balena-io/remove-exitWithExpectedError

build, deploy: Extend CTRL-C coverage on Windows (PowerShell, cmd.exe)
This commit is contained in:
bulldozer-balena[bot] 2021-08-27 00:21:24 +00:00 committed by GitHub
commit 8db36ccec9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 182 additions and 216 deletions

View File

@ -101,7 +101,7 @@ async function printMarkdown() {
console.log(await renderMarkdown()); console.log(await renderMarkdown());
} catch (error) { } catch (error) {
console.error(error); console.error(error);
process.exit(1); process.exitCode = 1;
} }
} }

View File

@ -41,17 +41,25 @@ function checkNpmVersion() {
// the reason is that it would unnecessarily prevent end users from // the reason is that it would unnecessarily prevent end users from
// using npm v6.4.1 that ships with Node 8. (It is OK for the // using npm v6.4.1 that ships with Node 8. (It is OK for the
// shrinkwrap file to get damaged if it is not going to be reused.) // shrinkwrap file to get damaged if it is not going to be reused.)
console.error(`\ throw new Error(`\
------------------------------------------------------------------------------- -----------------------------------------------------------------------------
Error: npm version '${npmVersion}' detected. Please upgrade to npm v${requiredVersion} or later Error: npm version '${npmVersion}' detected. Please upgrade to npm v${requiredVersion} or later
because of a bug that causes the 'npm-shrinkwrap.json' file to be damaged. because of a bug that causes the 'npm-shrinkwrap.json' file to be damaged.
At this point, however, your 'npm-shrinkwrap.json' file has already been At this point, however, your 'npm-shrinkwrap.json' file has already been
damaged. Please revert it to the master branch state with a command such as: damaged. Please revert it to the master branch state with a command such as:
"git checkout master -- npm-shrinkwrap.json" "git checkout master -- npm-shrinkwrap.json"
Then re-run "npm install" using npm version ${requiredVersion} or later. Then re-run "npm install" using npm version ${requiredVersion} or later.
-------------------------------------------------------------------------------`); -----------------------------------------------------------------------------`);
process.exit(1);
} }
} }
checkNpmVersion(); function main() {
try {
checkNpmVersion();
} catch (e) {
console.error(e.message || e);
process.exitCode = 1;
}
}
main();

View File

@ -54,9 +54,7 @@ export async function release() {
try { try {
await createGitHubRelease(); await createGitHubRelease();
} catch (err) { } catch (err) {
console.error('Release failed'); throw new Error(`Error creating GitHub release:\n${err}`);
console.error(err);
process.exit(1);
} }
} }

View File

@ -35,11 +35,6 @@ process.env.DEBUG = ['0', 'no', 'false', '', undefined].includes(
? '' ? ''
: '1'; : '1';
function exitWithError(error: Error | string): never {
console.error(`Error: ${error}`);
process.exit(1);
}
/** /**
* Trivial command-line parser. Check whether the command-line argument is one * Trivial command-line parser. Check whether the command-line argument is one
* of the following strings, then call the appropriate functions: * of the following strings, then call the appropriate functions:
@ -49,12 +44,12 @@ function exitWithError(error: Error | string): never {
* *
* @param args Arguments to parse (default is process.argv.slice(2)) * @param args Arguments to parse (default is process.argv.slice(2))
*/ */
export async function run(args?: string[]) { async function parse(args?: string[]) {
args = args || process.argv.slice(2); args = args || process.argv.slice(2);
console.log(`automation/run.ts process.argv=[${process.argv}]\n`); console.error(`[debug] automation/run.ts process.argv=[${process.argv}]`);
console.log(`automation/run.ts args=[${args}]`); console.error(`[debug] automation/run.ts args=[${args}]`);
if (_.isEmpty(args)) { if (_.isEmpty(args)) {
return exitWithError('missing command-line arguments'); throw new Error('missing command-line arguments');
} }
const commands: { [cmd: string]: () => void | Promise<void> } = { const commands: { [cmd: string]: () => void | Promise<void> } = {
'build:installer': buildOclifInstaller, 'build:installer': buildOclifInstaller,
@ -66,7 +61,7 @@ export async function run(args?: string[]) {
}; };
for (const arg of args) { for (const arg of args) {
if (!commands.hasOwnProperty(arg)) { if (!commands.hasOwnProperty(arg)) {
return exitWithError(`command unknown: ${arg}`); throw new Error(`command unknown: ${arg}`);
} }
} }
@ -90,9 +85,22 @@ export async function run(args?: string[]) {
const cmdFunc = commands[arg]; const cmdFunc = commands[arg];
await cmdFunc(); await cmdFunc();
} catch (err) { } catch (err) {
return exitWithError(`"${arg}": ${err}`); if (typeof err === 'object') {
err.message = `"${arg}": ${err.message}`;
}
throw err;
} }
} }
} }
/** See jsdoc for parse() function above */
export async function run(args?: string[]) {
try {
await parse(args);
} catch (e) {
console.error(e.message ? `Error: ${e.message}` : e);
process.exitCode = 1;
}
}
run(); run();

View File

@ -11,8 +11,7 @@ const validateChangeType = (maybeChangeType: string = 'minor') => {
case 'major': case 'major':
return maybeChangeType; return maybeChangeType;
default: default:
console.error(`Invalid change type: '${maybeChangeType}'`); throw new Error(`Invalid change type: '${maybeChangeType}'`);
return process.exit(1);
} }
}; };
@ -65,24 +64,17 @@ const getUpstreams = async () => {
return upstream; return upstream;
}; };
const printUsage = (upstreams: Upstream[], upstreamName: string) => { const getUsage = (upstreams: Upstream[], upstreamName: string) => `
console.error(
`
Usage: npm run update ${upstreamName} $version [$changeType=minor] Usage: npm run update ${upstreamName} $version [$changeType=minor]
Upstream names: ${upstreams.map(({ repo }) => repo).join(', ')} Upstream names: ${upstreams.map(({ repo }) => repo).join(', ')}
`, `;
);
return process.exit(1);
};
// TODO: Drop the wrapper function once we move to TS 3.8, async function $main() {
// which will support top level await.
async function main() {
const upstreams = await getUpstreams(); const upstreams = await getUpstreams();
if (process.argv.length < 3) { if (process.argv.length < 3) {
return printUsage(upstreams, '$upstreamName'); throw new Error(getUsage(upstreams, '$upstreamName'));
} }
const upstreamName = process.argv[2]; const upstreamName = process.argv[2];
@ -90,16 +82,15 @@ async function main() {
const upstream = upstreams.find((v) => v.repo === upstreamName); const upstream = upstreams.find((v) => v.repo === upstreamName);
if (!upstream) { if (!upstream) {
console.error( throw new Error(
`Invalid upstream name '${upstreamName}', valid options: ${upstreams `Invalid upstream name '${upstreamName}', valid options: ${upstreams
.map(({ repo }) => repo) .map(({ repo }) => repo)
.join(', ')}`, .join(', ')}`,
); );
return process.exit(1);
} }
if (process.argv.length < 4) { if (process.argv.length < 4) {
printUsage(upstreams, upstreamName); throw new Error(getUsage(upstreams, upstreamName));
} }
const packageName = upstream.module || upstream.repo; const packageName = upstream.module || upstream.repo;
@ -108,8 +99,7 @@ async function main() {
await run(`npm install ${packageName}@${process.argv[3]}`); await run(`npm install ${packageName}@${process.argv[3]}`);
const newVersion = await getVersion(packageName); const newVersion = await getVersion(packageName);
if (newVersion === oldVersion) { if (newVersion === oldVersion) {
console.error(`Already on version '${newVersion}'`); throw new Error(`Already on version '${newVersion}'`);
return process.exit(1);
} }
console.log(`Updated ${upstreamName} from ${oldVersion} to ${newVersion}`); console.log(`Updated ${upstreamName} from ${oldVersion} to ${newVersion}`);
@ -137,4 +127,13 @@ async function main() {
); );
} }
async function main() {
try {
await $main();
} catch (e) {
console.error(e);
process.exitCode = 1;
}
}
main(); main();

View File

@ -91,8 +91,6 @@ export default class EnvRmCmd extends Command {
await confirm( await confirm(
opt.yes || false, opt.yes || false,
'Are you sure you want to delete the environment variable?', 'Are you sure you want to delete the environment variable?',
undefined,
true,
); );
const balena = getBalenaSdk(); const balena = getBalenaSdk();

View File

@ -20,12 +20,7 @@ import type { BlockDevice } from 'etcher-sdk/build/source-destination';
import Command from '../../command'; import Command from '../../command';
import { ExpectedError } from '../../errors'; import { ExpectedError } from '../../errors';
import * as cf from '../../utils/common-flags'; import * as cf from '../../utils/common-flags';
import { import { getChalk, getVisuals, stripIndent } from '../../utils/lazy';
getChalk,
getCliForm,
getVisuals,
stripIndent,
} from '../../utils/lazy';
interface FlagsDef { interface FlagsDef {
yes: boolean; yes: boolean;
@ -93,24 +88,15 @@ export default class LocalFlashCmd extends Command {
} }
} }
const { sourceDestination, multiWrite } = await import('etcher-sdk');
const drive = await this.getDrive(options); const drive = await this.getDrive(options);
const yes = const { confirm } = await import('../../utils/patterns');
options.yes || await confirm(
(await getCliForm().ask({ options.yes,
message: 'This will erase the selected drive. Are you sure?', 'This will erase the selected drive. Are you sure?',
type: 'confirm', );
name: 'yes',
default: false,
}));
if (!yes) {
console.log(getChalk().red.bold('Aborted image flash'));
process.exit(0);
}
const { sourceDestination, multiWrite } = await import('etcher-sdk');
const file = new sourceDestination.File({ const file = new sourceDestination.File({
path: params.image, path: params.image,
}); });

View File

@ -92,7 +92,6 @@ export default class OsInitializeCmd extends Command {
options.yes, options.yes,
`This will erase ${answers.drive}. Are you sure?`, `This will erase ${answers.drive}. Are you sure?`,
`Going to erase ${answers.drive}.`, `Going to erase ${answers.drive}.`,
true,
); );
const { safeUmount } = await import('../../utils/umount'); const { safeUmount } = await import('../../utils/umount');
await safeUmount(answers.drive); await safeUmount(answers.drive);

View File

@ -286,24 +286,3 @@ export const printErrorMessage = function (message: string) {
export const printExpectedErrorMessage = function (message: string) { export const printExpectedErrorMessage = function (message: string) {
console.error(`${message}\n`); console.error(`${message}\n`);
}; };
/**
* Print a friendly error message and exit the CLI with an error code, BYPASSING
* error reporting through Sentry.io's platform (raven.Raven.captureException).
* Note that lib/errors.ts provides top-level error handling code to catch any
* otherwise uncaught errors, AND to report them through Sentry.io. But many
* "expected" errors (say, a JSON parsing error in a file provided by the user)
* don't warrant reporting through Sentry.io. For such mundane errors, catch
* them and call this function.
*
* DEPRECATED: Use `throw new ExpectedError(<message>)` instead.
* If a specific process exit code x must be set, use process.exitCode = x
*/
export function exitWithExpectedError(message: string | Error): never {
if (message instanceof Error) {
({ message } = message);
}
printErrorMessage(message);
process.exit(1);
}

View File

@ -447,7 +447,6 @@ var pushProgressRenderer = function (tty, prefix) {
export class BuildProgressUI { export class BuildProgressUI {
constructor(tty, descriptors) { constructor(tty, descriptors) {
this._handleEvent = this._handleEvent.bind(this); this._handleEvent = this._handleEvent.bind(this);
this._handleInterrupt = this._handleInterrupt.bind(this);
this.start = this.start.bind(this); this.start = this.start.bind(this);
this.end = this.end.bind(this); this.end = this.end.bind(this);
this._display = this._display.bind(this); this._display = this._display.bind(this);
@ -499,14 +498,7 @@ export class BuildProgressUI {
this._serviceToDataMap[service] = event; this._serviceToDataMap[service] = event;
} }
_handleInterrupt() {
this._cancelled = true;
this.end();
return process.exit(130); // 128 + SIGINT
}
start() { start() {
process.on('SIGINT', this._handleInterrupt);
this._tty.hideCursor(); this._tty.hideCursor();
this._services.forEach((service) => { this._services.forEach((service) => {
this.streams[service].write({ status: 'Preparing...' }); this.streams[service].write({ status: 'Preparing...' });
@ -520,7 +512,6 @@ export class BuildProgressUI {
return; return;
} }
this._ended = true; this._ended = true;
process.removeListener('SIGINT', this._handleInterrupt);
this._runloop?.end(); this._runloop?.end();
this._runloop = null; this._runloop = null;

View File

@ -237,7 +237,7 @@ interface Renderer {
streams: Dictionary<NodeJS.ReadWriteStream>; streams: Dictionary<NodeJS.ReadWriteStream>;
} }
export async function buildProject(opts: { export interface BuildProjectOpts {
docker: Dockerode; docker: Dockerode;
logger: Logger; logger: Logger;
projectPath: string; projectPath: string;
@ -252,82 +252,99 @@ export async function buildProject(opts: {
dockerfilePath?: string; dockerfilePath?: string;
nogitignore: boolean; nogitignore: boolean;
multiDockerignore: boolean; multiDockerignore: boolean;
}): Promise<BuiltImage[]> { }
const { logger, projectName } = opts;
logger.logInfo(`Building for ${opts.arch}/${opts.deviceType}`);
let buildSummaryByService: Dictionary<string> | undefined; export async function buildProject(
opts: BuildProjectOpts,
): Promise<BuiltImage[]> {
await checkBuildSecretsRequirements(opts.docker, opts.projectPath);
const compose = await import('resin-compose-parse'); const compose = await import('resin-compose-parse');
const imageDescriptors = compose.parse(opts.composition); const imageDescriptors = compose.parse(opts.composition);
const imageDescriptorsByServiceName = _.keyBy(
imageDescriptors,
'serviceName',
);
const renderer = await startRenderer({ imageDescriptors, ...opts }); const renderer = await startRenderer({ imageDescriptors, ...opts });
let buildSummaryByService: Dictionary<string> | undefined;
try { try {
await checkBuildSecretsRequirements(opts.docker, opts.projectPath); const { awaitInterruptibleTask } = await import('./helpers');
const [images, summaryMsgByService] = await awaitInterruptibleTask(
const needsQemu = await installQemuIfNeeded({ ...opts, imageDescriptors }); $buildProject,
imageDescriptors,
const tarStream = await tarDirectory(opts.projectPath, opts); renderer,
const tasks: BuildTaskPlus[] = await makeBuildTasks(
opts.composition,
tarStream,
opts, opts,
logger,
projectName,
); );
setTaskAttributes({ tasks, imageDescriptorsByServiceName, ...opts });
const transposeOptArray: Array<TransposeOptions | undefined> =
await Promise.all(
tasks.map((task) => {
// Setup emulation if needed
if (needsQemu && !task.external) {
return qemuTransposeBuildStream({ task, ...opts });
}
}),
);
await Promise.all(
// transposeOptions may be undefined. That's OK.
transposeOptArray.map((transposeOptions, index) =>
setTaskProgressHooks({
task: tasks[index],
renderer,
transposeOptions,
...opts,
}),
),
);
logger.logDebug('Prepared tasks; building...');
const { BALENA_ENGINE_TMP_PATH } = await import('../config');
const builder = await import('resin-multibuild');
const builtImages = await builder.performBuilds(
tasks,
opts.docker,
BALENA_ENGINE_TMP_PATH,
);
const [images, summaryMsgByService] = await inspectBuiltImages({
builtImages,
imageDescriptorsByServiceName,
tasks,
...opts,
});
buildSummaryByService = summaryMsgByService; buildSummaryByService = summaryMsgByService;
return images; return images;
} finally { } finally {
renderer.end(buildSummaryByService); renderer.end(buildSummaryByService);
} }
} }
async function $buildProject(
imageDescriptors: ImageDescriptor[],
renderer: Renderer,
opts: BuildProjectOpts,
): Promise<[BuiltImage[], Dictionary<string>]> {
const { logger, projectName } = opts;
logger.logInfo(`Building for ${opts.arch}/${opts.deviceType}`);
const needsQemu = await installQemuIfNeeded({ ...opts, imageDescriptors });
const tarStream = await tarDirectory(opts.projectPath, opts);
const tasks: BuildTaskPlus[] = await makeBuildTasks(
opts.composition,
tarStream,
opts,
logger,
projectName,
);
const imageDescriptorsByServiceName = _.keyBy(
imageDescriptors,
'serviceName',
);
setTaskAttributes({ tasks, imageDescriptorsByServiceName, ...opts });
const transposeOptArray: Array<TransposeOptions | undefined> =
await Promise.all(
tasks.map((task) => {
// Setup emulation if needed
if (needsQemu && !task.external) {
return qemuTransposeBuildStream({ task, ...opts });
}
}),
);
await Promise.all(
// transposeOptions may be undefined. That's OK.
transposeOptArray.map((transposeOptions, index) =>
setTaskProgressHooks({
task: tasks[index],
renderer,
transposeOptions,
...opts,
}),
),
);
logger.logDebug('Prepared tasks; building...');
const { BALENA_ENGINE_TMP_PATH } = await import('../config');
const builder = await import('resin-multibuild');
const builtImages = await builder.performBuilds(
tasks,
opts.docker,
BALENA_ENGINE_TMP_PATH,
);
return await inspectBuiltImages({
builtImages,
imageDescriptorsByServiceName,
tasks,
...opts,
});
}
async function startRenderer({ async function startRenderer({
imageDescriptors, imageDescriptors,
inlineLogs, inlineLogs,
@ -1344,20 +1361,25 @@ export async function deployProject(
logger.logDebug('Tagging images...'); logger.logDebug('Tagging images...');
const taggedImages = await tagServiceImages(docker, images, serviceImages); const taggedImages = await tagServiceImages(docker, images, serviceImages);
try { try {
const token = await getTokenForPreviousRepos( const { awaitInterruptibleTask } = await import('./helpers');
logger, // awaitInterruptibleTask throws SIGINTError on CTRL-C,
appId, // causing the release status to be set to 'failed'
apiEndpoint, await awaitInterruptibleTask(async () => {
taggedImages, const token = await getTokenForPreviousRepos(
); logger,
await pushServiceImages( appId,
docker, apiEndpoint,
logger, taggedImages,
pineClient, );
taggedImages, await pushServiceImages(
token, docker,
skipLogUpload, logger,
); pineClient,
taggedImages,
token,
skipLogUpload,
);
});
release.status = 'success'; release.status = 'success';
} catch (err) { } catch (err) {
release.status = 'failed'; release.status = 'failed';

View File

@ -16,18 +16,12 @@ limitations under the License.
import type * as BalenaSdk from 'balena-sdk'; import type * as BalenaSdk from 'balena-sdk';
import _ = require('lodash'); import _ = require('lodash');
import { import { instanceOf, NotLoggedInError, ExpectedError } from '../errors';
exitWithExpectedError,
instanceOf,
NotLoggedInError,
ExpectedError,
} from '../errors';
import { getBalenaSdk, getVisuals, stripIndent, getCliForm } from './lazy'; import { getBalenaSdk, getVisuals, stripIndent, getCliForm } from './lazy';
import validation = require('./validation'); import validation = require('./validation');
import { delay } from './helpers'; import { delay } from './helpers';
import { isV13 } from './version'; import { isV13 } from './version';
import type { Application, Device, Organization } from 'balena-sdk'; import type { Application, Device, Organization } from 'balena-sdk';
import { getApplication } from './sdk';
export function authenticate(options: {}): Promise<void> { export function authenticate(options: {}): Promise<void> {
const balena = getBalenaSdk(); const balena = getBalenaSdk();
@ -135,18 +129,16 @@ export function selectDeviceType() {
/** /**
* Display interactive confirmation prompt. * Display interactive confirmation prompt.
* If the user declines, then either an error will be thrown, * Throw ExpectedError if the user declines.
* or `exitWithExpectedError` will be called (if exitIfDeclined true).
* @param yesOption - automatically confirm if true * @param yesOption - automatically confirm if true
* @param message - message to display with prompt * @param message - message to display with prompt
* @param yesMessage - message to display if automatically confirming * @param yesMessage - message to display if automatically confirming
* @param exitIfDeclined - exitWithExpectedError when decline if true
*/ */
export async function confirm( export async function confirm(
yesOption: boolean, yesOption: boolean,
message: string, message: string,
yesMessage?: string, yesMessage?: string,
exitIfDeclined = false, defaultValue = false,
) { ) {
if (yesOption) { if (yesOption) {
if (yesMessage) { if (yesMessage) {
@ -162,16 +154,11 @@ export async function confirm(
const confirmed = await getCliForm().ask<boolean>({ const confirmed = await getCliForm().ask<boolean>({
message, message,
type: 'confirm', type: 'confirm',
default: false, default: defaultValue,
}); });
if (!confirmed) { if (!confirmed) {
const err = new ExpectedError('Aborted'); throw new ExpectedError('Aborted');
// TODO remove this deprecated function (exitWithExpectedError)
if (exitIfDeclined) {
exitWithExpectedError(err);
}
throw err;
} }
} }
@ -281,11 +268,9 @@ export async function awaitDeviceOsUpdate(
} }
if (osUpdateStatus.error) { if (osUpdateStatus.error) {
console.error( throw new ExpectedError(
`Failed to complete Host OS update on device ${deviceName}!`, `Failed to complete Host OS update on device ${deviceName}\n${osUpdateStatus.error}`,
); );
exitWithExpectedError(osUpdateStatus.error);
return;
} }
if (osUpdateProgress !== null) { if (osUpdateProgress !== null) {
@ -379,6 +364,7 @@ export async function getOnlineTargetDeviceUuid(
let app: Application; let app: Application;
try { try {
logger.logDebug(`Fetching fleet ${applicationOrDevice}`); logger.logDebug(`Fetching fleet ${applicationOrDevice}`);
const { getApplication } = await import('./sdk');
app = await getApplication(sdk, applicationOrDevice); app = await getApplication(sdk, applicationOrDevice);
} catch (err) { } catch (err) {
const { BalenaApplicationNotFound } = await import('balena-errors'); const { BalenaApplicationNotFound } = await import('balena-errors');

View File

@ -19,6 +19,7 @@ import type * as BalenaSdk from 'balena-sdk';
import { ExpectedError, printErrorMessage } from '../errors'; import { ExpectedError, printErrorMessage } from '../errors';
import { getVisuals, stripIndent, getCliForm } from './lazy'; import { getVisuals, stripIndent, getCliForm } from './lazy';
import Logger = require('./logger'); import Logger = require('./logger');
import { confirm } from './patterns';
import { exec, execBuffered, getDeviceOsRelease } from './ssh'; import { exec, execBuffered, getDeviceOsRelease } from './ssh';
const MIN_BALENAOS_VERSION = 'v2.14.0'; const MIN_BALENAOS_VERSION = 'v2.14.0';
@ -211,7 +212,7 @@ async function getOrSelectApplication(
.value(); .value();
if (!appName) { if (!appName) {
return createOrSelectAppOrExit(sdk, compatibleDeviceTypes, deviceType); return createOrSelectApp(sdk, compatibleDeviceTypes, deviceType);
} }
const options: BalenaSdk.PineOptions<BalenaSdk.Application> = { const options: BalenaSdk.PineOptions<BalenaSdk.Application> = {
@ -239,17 +240,14 @@ async function getOrSelectApplication(
)) as ApplicationWithDeviceType[]; )) as ApplicationWithDeviceType[];
if (applications.length === 0) { if (applications.length === 0) {
const shouldCreateApp = await getCliForm().ask({ await confirm(
message: false,
`No fleet found with name "${appName}".\n` + `No fleet found with name "${appName}".\n` +
'Would you like to create it now?', 'Would you like to create it now?',
type: 'confirm', undefined,
default: true, true,
}); );
if (shouldCreateApp) { return await createApplication(sdk, deviceType, name);
return createApplication(sdk, deviceType, name);
}
process.exit(1);
} }
// We've found at least one fleet with the given name. // We've found at least one fleet with the given name.
@ -269,10 +267,7 @@ async function getOrSelectApplication(
return selectAppFromList(applications); return selectAppFromList(applications);
} }
// TODO: revisit this function's purpose. It was refactored out of async function createOrSelectApp(
// `getOrSelectApplication` above in order to satisfy some resin-lint v3
// rules, but it looks like there's a fair amount of duplicate logic.
async function createOrSelectAppOrExit(
sdk: BalenaSdk.BalenaSDK, sdk: BalenaSdk.BalenaSDK,
compatibleDeviceTypes: string[], compatibleDeviceTypes: string[],
deviceType: string, deviceType: string,
@ -291,17 +286,14 @@ async function createOrSelectAppOrExit(
})) as ApplicationWithDeviceType[]; })) as ApplicationWithDeviceType[];
if (applications.length === 0) { if (applications.length === 0) {
const shouldCreateApp = await getCliForm().ask({ await confirm(
message: false,
'You have no fleets this device can join.\n' + 'You have no fleets this device can join.\n' +
'Would you like to create one now?', 'Would you like to create one now?',
type: 'confirm', undefined,
default: true, true,
}); );
if (shouldCreateApp) { return await createApplication(sdk, deviceType);
return createApplication(sdk, deviceType);
}
process.exit(1);
} }
return selectAppFromList(applications); return selectAppFromList(applications);