mirror of
https://github.com/balena-io/balena-cli.git
synced 2024-12-19 21:57:51 +00:00
Begin the transition to oclif with 'balena env add' (fix dropped leading
zero in device UUID). This commit is fairly chunky because it adds the oclif dependency for the first time, and refactors the CLI help and docs generation code to accommodate both Capitano and oclif. Change-type: patch Signed-off-by: Paulo Castro <paulo@balena.io>
This commit is contained in:
parent
13e3e5e8ea
commit
abf573fa47
@ -48,7 +48,10 @@ const capitanoDoc = {
|
||||
},
|
||||
{
|
||||
title: 'Environment Variables',
|
||||
files: ['build/actions/environment-variables.js'],
|
||||
files: [
|
||||
'build/actions/environment-variables.js',
|
||||
'build/actions-oclif/env/add.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Tags',
|
||||
|
25
automation/capitanodoc/doc-types.d.ts
vendored
25
automation/capitanodoc/doc-types.d.ts
vendored
@ -1,4 +1,23 @@
|
||||
import { CommandDefinition } from 'capitano';
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Command as OclifCommandClass } from '@oclif/command';
|
||||
import { CommandDefinition as CapitanoCommand } from 'capitano';
|
||||
|
||||
type OclifCommand = typeof OclifCommandClass;
|
||||
|
||||
export interface Document {
|
||||
title: string;
|
||||
@ -8,7 +27,7 @@ export interface Document {
|
||||
|
||||
export interface Category {
|
||||
title: string;
|
||||
commands: CommandDefinition[];
|
||||
commands: Array<CapitanoCommand | OclifCommand>;
|
||||
}
|
||||
|
||||
export { CommandDefinition as Command };
|
||||
export { CapitanoCommand, OclifCommand };
|
||||
|
@ -18,7 +18,7 @@ import * as _ from 'lodash';
|
||||
import * as path from 'path';
|
||||
|
||||
import { getCapitanoDoc } from './capitanodoc';
|
||||
import { Category, Document } from './doc-types';
|
||||
import { CapitanoCommand, Category, Document, OclifCommand } from './doc-types';
|
||||
import * as markdown from './markdown';
|
||||
|
||||
/**
|
||||
@ -39,25 +39,40 @@ export async function renderMarkdown(): Promise<string> {
|
||||
commands: [],
|
||||
};
|
||||
|
||||
for (const file of commandCategory.files) {
|
||||
const actions: any = require(path.join(process.cwd(), file));
|
||||
|
||||
if (actions.signature) {
|
||||
category.commands.push(_.omit(actions, 'action'));
|
||||
} else {
|
||||
for (const actionName of Object.keys(actions)) {
|
||||
const actionCommand = actions[actionName];
|
||||
category.commands.push(_.omit(actionCommand, 'action'));
|
||||
}
|
||||
}
|
||||
for (const jsFilename of commandCategory.files) {
|
||||
category.commands.push(
|
||||
...(jsFilename.includes('actions-oclif')
|
||||
? importOclifCommands(jsFilename)
|
||||
: importCapitanoCommands(jsFilename)),
|
||||
);
|
||||
}
|
||||
|
||||
result.categories.push(category);
|
||||
}
|
||||
|
||||
return markdown.render(result);
|
||||
}
|
||||
|
||||
function importCapitanoCommands(jsFilename: string): CapitanoCommand[] {
|
||||
const actions = require(path.join(process.cwd(), jsFilename));
|
||||
const commands: CapitanoCommand[] = [];
|
||||
|
||||
if (actions.signature) {
|
||||
commands.push(_.omit(actions, 'action'));
|
||||
} else {
|
||||
for (const actionName of Object.keys(actions)) {
|
||||
const actionCommand = actions[actionName];
|
||||
commands.push(_.omit(actionCommand, 'action'));
|
||||
}
|
||||
}
|
||||
return commands;
|
||||
}
|
||||
|
||||
function importOclifCommands(jsFilename: string): OclifCommand[] {
|
||||
const command: OclifCommand = require(path.join(process.cwd(), jsFilename))
|
||||
.default as OclifCommand;
|
||||
return [command];
|
||||
}
|
||||
|
||||
/**
|
||||
* Print the CLI docs markdown to stdout.
|
||||
* See package.json for how the output is redirected to a file.
|
||||
|
@ -14,81 +14,136 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { flagUsages } from '@oclif/parser';
|
||||
import * as ent from 'ent';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { Category, Command, Document } from './doc-types';
|
||||
import { getManualSortCompareFunction } from '../../lib/utils/helpers';
|
||||
import { CapitanoCommand, Category, Document, OclifCommand } from './doc-types';
|
||||
import * as utils from './utils';
|
||||
|
||||
export function renderCommand(command: Command) {
|
||||
let result = `## ${ent.encode(command.signature)}\n\n${command.help}\n`;
|
||||
function renderCapitanoCommand(command: CapitanoCommand): string[] {
|
||||
const result = [`## ${ent.encode(command.signature)}`, command.help];
|
||||
|
||||
if (!_.isEmpty(command.options)) {
|
||||
result += '\n### Options';
|
||||
result.push('### Options');
|
||||
|
||||
for (const option of command.options!) {
|
||||
result += `\n\n#### ${utils.parseSignature(option)}\n\n${
|
||||
option.description
|
||||
}`;
|
||||
result.push(
|
||||
`#### ${utils.parseCapitanoOption(option)}`,
|
||||
option.description,
|
||||
);
|
||||
}
|
||||
|
||||
result += '\n';
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function renderCategory(category: Category) {
|
||||
let result = `# ${category.title}\n`;
|
||||
function renderOclifCommand(command: OclifCommand): string[] {
|
||||
const result = [`## ${ent.encode(command.usage)}`];
|
||||
const description = (command.description || '')
|
||||
.split('\n')
|
||||
.slice(1) // remove the first line, which oclif uses as help header
|
||||
.join('\n')
|
||||
.trim();
|
||||
result.push(description);
|
||||
|
||||
if (!_.isEmpty(command.examples)) {
|
||||
result.push('Examples:', command.examples!.map(v => `\t${v}`).join('\n'));
|
||||
}
|
||||
|
||||
if (!_.isEmpty(command.args)) {
|
||||
result.push('### Arguments');
|
||||
for (const arg of command.args!) {
|
||||
result.push(`#### ${arg.name.toUpperCase()}`, arg.description || '');
|
||||
}
|
||||
}
|
||||
|
||||
if (!_.isEmpty(command.flags)) {
|
||||
result.push('### Options');
|
||||
for (const [name, flag] of Object.entries(command.flags!)) {
|
||||
if (name === 'help') {
|
||||
continue;
|
||||
}
|
||||
flag.name = name;
|
||||
const flagUsage = flagUsages([flag])
|
||||
.map(([usage, _description]) => usage)
|
||||
.join()
|
||||
.trim();
|
||||
result.push(`#### ${flagUsage}`);
|
||||
result.push(flag.description || '');
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function renderCategory(category: Category): string[] {
|
||||
const result = [`# ${category.title}`];
|
||||
for (const command of category.commands) {
|
||||
result += `\n${renderCommand(command)}`;
|
||||
result.push(
|
||||
...(typeof command === 'object'
|
||||
? renderCapitanoCommand(command)
|
||||
: renderOclifCommand(command)),
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function getAnchor(command: Command) {
|
||||
return (
|
||||
'#' +
|
||||
command.signature
|
||||
.replace(/\s/g, '-')
|
||||
.replace(/</g, '-')
|
||||
.replace(/>/g, '-')
|
||||
.replace(/\[/g, '-')
|
||||
.replace(/\]/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/-$/, '')
|
||||
.replace(/\.\.\./g, '')
|
||||
.replace(/\|/g, '')
|
||||
.toLowerCase()
|
||||
);
|
||||
function getAnchor(cmdSignature: string): string {
|
||||
return `#${_.trim(cmdSignature.replace(/\W+/g, '-'), '-').toLowerCase()}`;
|
||||
}
|
||||
|
||||
export function renderToc(categories: Category[]) {
|
||||
let result = `# CLI Command Reference\n`;
|
||||
function renderToc(categories: Category[]): string[] {
|
||||
const result = [`# CLI Command Reference`];
|
||||
|
||||
for (const category of categories) {
|
||||
result += `\n- ${category.title}\n\n`;
|
||||
result.push(`- ${category.title}`);
|
||||
result.push(
|
||||
category.commands
|
||||
.map(command => {
|
||||
const signature =
|
||||
typeof command === 'object'
|
||||
? command.signature // Capitano
|
||||
: utils.capitanoizeOclifUsage(command.usage); // oclif
|
||||
return `\t- [${ent.encode(signature)}](${getAnchor(signature)})`;
|
||||
})
|
||||
.join('\n'),
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
for (const command of category.commands) {
|
||||
result += `\t- [${ent.encode(command.signature)}](${getAnchor(
|
||||
command,
|
||||
)})\n`;
|
||||
const manualCategorySorting: { [category: string]: string[] } = {
|
||||
'Environment Variables': ['envs', 'env rm', 'env add', 'env rename'],
|
||||
};
|
||||
|
||||
function sortCommands(doc: Document): void {
|
||||
for (const category of doc.categories) {
|
||||
if (category.title in manualCategorySorting) {
|
||||
category.commands = category.commands.sort(
|
||||
getManualSortCompareFunction<CapitanoCommand | OclifCommand, string>(
|
||||
manualCategorySorting[category.title],
|
||||
(cmd: CapitanoCommand | OclifCommand, x: string) =>
|
||||
typeof cmd === 'object' // Capitano vs oclif command
|
||||
? cmd.signature.replace(/\W+/g, ' ').includes(x)
|
||||
: (cmd.usage || '')
|
||||
.toString()
|
||||
.replace(/\W+/g, ' ')
|
||||
.includes(x),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function render(doc: Document) {
|
||||
let result = `# ${doc.title}\n\n${doc.introduction}\n\n${renderToc(
|
||||
doc.categories,
|
||||
)}`;
|
||||
|
||||
sortCommands(doc);
|
||||
const result = [
|
||||
`# ${doc.title}`,
|
||||
doc.introduction,
|
||||
...renderToc(doc.categories),
|
||||
];
|
||||
for (const category of doc.categories) {
|
||||
result += `\n${renderCategory(category)}`;
|
||||
result.push(...renderCategory(category));
|
||||
}
|
||||
|
||||
return result;
|
||||
return result.join('\n\n');
|
||||
}
|
||||
|
@ -14,6 +14,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { OptionDefinition } from 'capitano';
|
||||
import * as ent from 'ent';
|
||||
import * as fs from 'fs';
|
||||
@ -32,7 +33,7 @@ export function getOptionSignature(signature: string) {
|
||||
return `${getOptionPrefix(signature)}${signature}`;
|
||||
}
|
||||
|
||||
export function parseSignature(option: OptionDefinition) {
|
||||
export function parseCapitanoOption(option: OptionDefinition): string {
|
||||
let result = getOptionSignature(option.signature);
|
||||
|
||||
if (_.isArray(option.alias)) {
|
||||
@ -50,6 +51,16 @@ export function parseSignature(option: OptionDefinition) {
|
||||
return ent.encode(result);
|
||||
}
|
||||
|
||||
/** Convert e.g. 'env add NAME [VALUE]' to 'env add <name> [value]' */
|
||||
export function capitanoizeOclifUsage(
|
||||
oclifUsage: string | string[] | undefined,
|
||||
): string {
|
||||
return (oclifUsage || '')
|
||||
.toString()
|
||||
.replace(/(?<=\s)[A-Z]+(?=(\s|$))/g, match => `<${match}>`)
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
export class MarkdownFileParser {
|
||||
constructor(public mdFilePath: string) {}
|
||||
|
||||
|
@ -8,4 +8,5 @@ process.env.UV_THREADPOOL_SIZE = '64';
|
||||
require('fast-boot2').start({
|
||||
cacheFile: __dirname + '/.fast-boot.json'
|
||||
})
|
||||
require('../build/app');
|
||||
// Run the CLI
|
||||
require('../build/app').run();
|
||||
|
@ -20,4 +20,4 @@ require('coffeescript/register');
|
||||
// it is supposed to run faster. We still benefit from type checking when
|
||||
// running 'npm run build'.
|
||||
require('ts-node/register/transpile-only');
|
||||
require('../lib/app');
|
||||
require('../lib/app').run();
|
||||
|
@ -109,7 +109,7 @@ If you come across any problems or would like to get in touch:
|
||||
|
||||
- [envs](#envs)
|
||||
- [env rm <id>](#env-rm-id)
|
||||
- [env add <key> [value]](#env-add-key-value)
|
||||
- [env add <name> [value]](#env-add-name-value)
|
||||
- [env rename <id> <value>](#env-rename-id-value)
|
||||
|
||||
- Tags
|
||||
@ -633,38 +633,47 @@ confirm non interactively
|
||||
|
||||
device
|
||||
|
||||
## env add <key> [value]
|
||||
## env add NAME [VALUE]
|
||||
|
||||
Use this command to add an enviroment or config variable to an application
|
||||
or device.
|
||||
Add an enviroment or config variable to an application or device, as selected
|
||||
by the respective command-line options.
|
||||
|
||||
If value is omitted, the tool will attempt to use the variable's value
|
||||
as defined in your host machine.
|
||||
If VALUE is omitted, the CLI will attempt to use the value of the environment
|
||||
variable of same name in the CLI process' environment. In this case, a warning
|
||||
message will be printed. Use `--quiet` to suppress it.
|
||||
|
||||
Use the `--device` option if you want to assign the environment variable
|
||||
to a specific device.
|
||||
|
||||
If the value is grabbed from the environment, a warning message will be printed.
|
||||
Use `--quiet` to remove it.
|
||||
|
||||
Service-specific variables are not currently supported. The following
|
||||
examples set variables that apply to all services in an app or device.
|
||||
Service-specific variables are not currently supported. The given command line
|
||||
examples variables that apply to all services in an app or device.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena env add EDITOR vim --application MyApp
|
||||
$ balena env add TERM --application MyApp
|
||||
$ balena env add EDITOR vim --application MyApp
|
||||
$ balena env add EDITOR vim --device 7cf02a6
|
||||
|
||||
### Arguments
|
||||
|
||||
#### NAME
|
||||
|
||||
environment or config variable name
|
||||
|
||||
#### VALUE
|
||||
|
||||
variable value; if omitted, use value from CLI's enviroment
|
||||
|
||||
### Options
|
||||
|
||||
#### --application, -a, --app <application>
|
||||
#### -a, --application APPLICATION
|
||||
|
||||
application name
|
||||
|
||||
#### --device, -d <device>
|
||||
#### -d, --device DEVICE
|
||||
|
||||
device uuid
|
||||
device UUID
|
||||
|
||||
#### -q, --quiet
|
||||
|
||||
suppress warning messages
|
||||
|
||||
## env rename <id> <value>
|
||||
|
||||
@ -2082,4 +2091,3 @@ Examples:
|
||||
|
||||
Use this command to list your machine's drives usable for writing the OS image to.
|
||||
Skips the system drives.
|
||||
|
||||
|
150
lib/actions-oclif/env/add.ts
vendored
Normal file
150
lib/actions-oclif/env/add.ts
vendored
Normal file
@ -0,0 +1,150 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import { stripIndent } from 'common-tags';
|
||||
|
||||
import { CommandHelp } from '../../utils/oclif-utils';
|
||||
|
||||
interface FlagsDef {
|
||||
application?: string;
|
||||
device?: string;
|
||||
help: void;
|
||||
quiet: boolean;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
name: string;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
export default class EnvAddCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Add an enviroment or config variable to an application or device.
|
||||
|
||||
Add an enviroment or config variable to an application or device, as selected
|
||||
by the respective command-line options.
|
||||
|
||||
If VALUE is omitted, the CLI will attempt to use the value of the environment
|
||||
variable of same name in the CLI process' environment. In this case, a warning
|
||||
message will be printed. Use \`--quiet\` to suppress it.
|
||||
|
||||
Service-specific variables are not currently supported. The given command line
|
||||
examples variables that apply to all services in an app or device.
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena env add TERM --application MyApp',
|
||||
'$ balena env add EDITOR vim --application MyApp',
|
||||
'$ balena env add EDITOR vim --device 7cf02a6',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'name',
|
||||
required: true,
|
||||
description: 'environment or config variable name',
|
||||
},
|
||||
{
|
||||
name: 'value',
|
||||
required: false,
|
||||
description:
|
||||
"variable value; if omitted, use value from CLI's enviroment",
|
||||
},
|
||||
];
|
||||
|
||||
// hardcoded 'env add' to avoid oclif's 'env:add' topic syntax
|
||||
public static usage =
|
||||
'env add ' + new CommandHelp({ args: EnvAddCmd.args }).defaultUsage();
|
||||
|
||||
public static flags = {
|
||||
application: flags.string({
|
||||
char: 'a',
|
||||
description: 'application name',
|
||||
exclusive: ['device'],
|
||||
}),
|
||||
device: flags.string({
|
||||
char: 'd',
|
||||
description: 'device UUID',
|
||||
exclusive: ['application'],
|
||||
}),
|
||||
help: flags.help({ char: 'h' }),
|
||||
quiet: flags.boolean({
|
||||
char: 'q',
|
||||
description: 'suppress warning messages',
|
||||
default: false,
|
||||
}),
|
||||
};
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
EnvAddCmd,
|
||||
);
|
||||
const Bluebird = await import('bluebird');
|
||||
const _ = await import('lodash');
|
||||
const balena = (await import('balena-sdk')).fromSharedOptions();
|
||||
const { exitWithExpectedError } = await import('../../utils/patterns');
|
||||
|
||||
const cmd = this;
|
||||
|
||||
await Bluebird.try(async function() {
|
||||
if (params.value == null) {
|
||||
params.value = process.env[params.name];
|
||||
|
||||
if (params.value == null) {
|
||||
throw new Error(
|
||||
`Environment value not found for variable: ${params.name}`,
|
||||
);
|
||||
} else if (!options.quiet) {
|
||||
cmd.warn(
|
||||
`Using ${params.name}=${params.value} from CLI process environment`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const reservedPrefixes = await getReservedPrefixes();
|
||||
const isConfigVar = _.some(reservedPrefixes, prefix =>
|
||||
_.startsWith(params.name, prefix),
|
||||
);
|
||||
|
||||
if (options.application) {
|
||||
return balena.models.application[
|
||||
isConfigVar ? 'configVar' : 'envVar'
|
||||
].set(options.application, params.name, params.value);
|
||||
} else if (options.device) {
|
||||
return balena.models.device[isConfigVar ? 'configVar' : 'envVar'].set(
|
||||
options.device,
|
||||
params.name,
|
||||
params.value,
|
||||
);
|
||||
} else {
|
||||
exitWithExpectedError('You must specify an application or device');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function getReservedPrefixes(): Promise<string[]> {
|
||||
const balena = (await import('balena-sdk')).fromSharedOptions();
|
||||
const settings = await balena.settings.getAll();
|
||||
|
||||
const response = await balena.request.send({
|
||||
baseUrl: settings.apiUrl,
|
||||
url: '/config/vars',
|
||||
});
|
||||
|
||||
return response.body.reservedNamespaces;
|
||||
}
|
@ -13,7 +13,6 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { ApplicationVariable, DeviceVariable } from 'balena-sdk';
|
||||
import * as Bluebird from 'bluebird';
|
||||
import { CommandDefinition } from 'capitano';
|
||||
@ -22,18 +21,6 @@ import { stripIndent } from 'common-tags';
|
||||
import { normalizeUuidProp } from '../utils/normalization';
|
||||
import * as commandOptions from './command-options';
|
||||
|
||||
const getReservedPrefixes = async (): Promise<string[]> => {
|
||||
const balena = (await import('balena-sdk')).fromSharedOptions();
|
||||
const settings = await balena.settings.getAll();
|
||||
|
||||
const response = await balena.request.send({
|
||||
baseUrl: settings.apiUrl,
|
||||
url: '/config/vars',
|
||||
});
|
||||
|
||||
return response.body.reservedNamespaces;
|
||||
};
|
||||
|
||||
export const list: CommandDefinition<
|
||||
{},
|
||||
{
|
||||
@ -171,86 +158,6 @@ export const remove: CommandDefinition<
|
||||
},
|
||||
};
|
||||
|
||||
export const add: CommandDefinition<
|
||||
{
|
||||
key: string;
|
||||
value?: string;
|
||||
},
|
||||
{
|
||||
application?: string;
|
||||
device?: string;
|
||||
}
|
||||
> = {
|
||||
signature: 'env add <key> [value]',
|
||||
description: 'add an environment or config variable',
|
||||
help: stripIndent`
|
||||
Use this command to add an enviroment or config variable to an application
|
||||
or device.
|
||||
|
||||
If value is omitted, the tool will attempt to use the variable's value
|
||||
as defined in your host machine.
|
||||
|
||||
Use the \`--device\` option if you want to assign the environment variable
|
||||
to a specific device.
|
||||
|
||||
If the value is grabbed from the environment, a warning message will be printed.
|
||||
Use \`--quiet\` to remove it.
|
||||
|
||||
Service-specific variables are not currently supported. The following
|
||||
examples set variables that apply to all services in an app or device.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena env add EDITOR vim --application MyApp
|
||||
$ balena env add TERM --application MyApp
|
||||
$ balena env add EDITOR vim --device 7cf02a6
|
||||
`,
|
||||
options: [commandOptions.optionalApplication, commandOptions.optionalDevice],
|
||||
permission: 'user',
|
||||
async action(params, options, done) {
|
||||
normalizeUuidProp(options, 'device');
|
||||
const _ = await import('lodash');
|
||||
const balena = (await import('balena-sdk')).fromSharedOptions();
|
||||
|
||||
const { exitWithExpectedError } = await import('../utils/patterns');
|
||||
|
||||
return Bluebird.try(async function() {
|
||||
if (params.value == null) {
|
||||
params.value = process.env[params.key];
|
||||
|
||||
if (params.value == null) {
|
||||
throw new Error(`Environment value not found for key: ${params.key}`);
|
||||
} else {
|
||||
console.info(
|
||||
`Warning: using ${params.key}=${
|
||||
params.value
|
||||
} from host environment`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const reservedPrefixes = await getReservedPrefixes();
|
||||
const isConfigVar = _.some(reservedPrefixes, prefix =>
|
||||
_.startsWith(params.key, prefix),
|
||||
);
|
||||
|
||||
if (options.application) {
|
||||
return balena.models.application[
|
||||
isConfigVar ? 'configVar' : 'envVar'
|
||||
].set(options.application, params.key, params.value);
|
||||
} else if (options.device) {
|
||||
return balena.models.device[isConfigVar ? 'configVar' : 'envVar'].set(
|
||||
options.device,
|
||||
params.key,
|
||||
params.value,
|
||||
);
|
||||
} else {
|
||||
exitWithExpectedError('You must specify an application or device');
|
||||
}
|
||||
}).nodeify(done);
|
||||
},
|
||||
};
|
||||
|
||||
export const rename: CommandDefinition<
|
||||
{
|
||||
id: number;
|
||||
|
@ -17,11 +17,13 @@ limitations under the License.
|
||||
_ = require('lodash')
|
||||
capitano = require('capitano')
|
||||
columnify = require('columnify')
|
||||
|
||||
messages = require('../utils/messages')
|
||||
{ exitWithExpectedError } = require('../utils/patterns')
|
||||
{ getOclifHelpLinePairs } = require('./help_ts')
|
||||
|
||||
parse = (object) ->
|
||||
return _.fromPairs _.map(object, (item) ->
|
||||
return _.map object, (item) ->
|
||||
|
||||
# Hacky way to determine if an object is
|
||||
# a function or a command
|
||||
@ -33,14 +35,15 @@ parse = (object) ->
|
||||
return [
|
||||
signature
|
||||
item.description
|
||||
]).sort()
|
||||
]
|
||||
|
||||
indent = (text) ->
|
||||
text = _.map text.split('\n'), (line) ->
|
||||
return ' ' + line
|
||||
return text.join('\n')
|
||||
|
||||
print = (data) ->
|
||||
print = (usageDescriptionPairs...) ->
|
||||
data = _.fromPairs([].concat(usageDescriptionPairs...).sort())
|
||||
console.log indent columnify data,
|
||||
showHeaders: false
|
||||
minWidth: 35
|
||||
@ -64,7 +67,7 @@ general = (params, options, done) ->
|
||||
|
||||
if options.verbose
|
||||
console.log('\nAdditional commands:\n')
|
||||
print(parse(groupedCommands.secondary))
|
||||
print(parse(groupedCommands.secondary), getOclifHelpLinePairs())
|
||||
else
|
||||
console.log('\nRun `balena help --verbose` to list additional commands')
|
||||
|
||||
|
35
lib/actions/help_ts.ts
Normal file
35
lib/actions/help_ts.ts
Normal file
@ -0,0 +1,35 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Command } from '@oclif/command';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import EnvAddCmd from '../actions-oclif/env/add';
|
||||
|
||||
export function getOclifHelpLinePairs(): [[string, string]] {
|
||||
return [getCmdUsageDescriptionLinePair(EnvAddCmd)];
|
||||
}
|
||||
|
||||
function getCmdUsageDescriptionLinePair(cmd: typeof Command): [string, string] {
|
||||
const usage = (cmd.usage || '').toString().toLowerCase();
|
||||
let description = '';
|
||||
const matches = /\s*(.+?)\n.*/s.exec(cmd.description || '');
|
||||
if (matches && matches.length > 1) {
|
||||
description = _.lowerFirst(_.trimEnd(matches[1], '.'));
|
||||
}
|
||||
return [usage, description];
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
###
|
||||
Copyright 2016-2017 Balena
|
||||
Copyright 2016-2019 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -14,77 +14,13 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
Raven = require('raven')
|
||||
Raven.disableConsoleAlerts()
|
||||
Raven.config require('./config').sentryDsn,
|
||||
captureUnhandledRejections: true,
|
||||
autoBreadcrumbs: true,
|
||||
release: require('../package.json').version
|
||||
.install (logged, error) ->
|
||||
console.error(error)
|
||||
process.exit(1)
|
||||
Raven.setContext
|
||||
extra:
|
||||
args: process.argv
|
||||
node_version: process.version
|
||||
|
||||
validNodeVersions = require('../package.json').engines.node
|
||||
if not require('semver').satisfies(process.version, validNodeVersions)
|
||||
console.warn """
|
||||
Warning: this version of Node does not match the requirements of this package.
|
||||
This package expects #{validNodeVersions}, but you're using #{process.version}.
|
||||
This may cause unexpected behaviour.
|
||||
|
||||
To upgrade your Node, visit https://nodejs.org/en/download/
|
||||
|
||||
"""
|
||||
|
||||
|
||||
# Doing this before requiring any other modules,
|
||||
# including the 'balena-sdk', to prevent any module from reading the http proxy config
|
||||
# before us
|
||||
globalTunnel = require('global-tunnel-ng')
|
||||
settings = require('balena-settings-client')
|
||||
try
|
||||
proxy = settings.get('proxy') or null
|
||||
catch
|
||||
proxy = null
|
||||
# Init the tunnel even if the proxy is not configured
|
||||
# because it can also get the proxy from the http(s)_proxy env var
|
||||
# If that is not set as well the initialize will do nothing
|
||||
globalTunnel.initialize(proxy)
|
||||
|
||||
# TODO: make this a feature of capitano https://github.com/balena-io/capitano/issues/48
|
||||
global.PROXY_CONFIG = globalTunnel.proxyConfig
|
||||
|
||||
Promise = require('bluebird')
|
||||
capitano = require('capitano')
|
||||
capitanoExecuteAsync = Promise.promisify(capitano.execute)
|
||||
|
||||
# We don't yet use balena-sdk directly everywhere, but we set up shared
|
||||
# options correctly so we can do safely in submodules
|
||||
BalenaSdk = require('balena-sdk')
|
||||
BalenaSdk.setSharedOptions(
|
||||
apiUrl: settings.get('apiUrl')
|
||||
imageMakerUrl: settings.get('imageMakerUrl')
|
||||
dataDirectory: settings.get('dataDirectory')
|
||||
retries: 2
|
||||
)
|
||||
|
||||
actions = require('./actions')
|
||||
errors = require('./errors')
|
||||
events = require('./events')
|
||||
update = require('./utils/update')
|
||||
{ exitIfNotLoggedIn } = require('./utils/patterns')
|
||||
|
||||
# Assign bluebird as the global promise library
|
||||
# stream-to-promise will produce native promises if not
|
||||
# for this module, which could wreak havoc in this
|
||||
# bluebird-only codebase.
|
||||
require('any-promise/register/bluebird')
|
||||
|
||||
capitano.permission 'user', (done) ->
|
||||
exitIfNotLoggedIn()
|
||||
require('./utils/patterns').exitIfNotLoggedIn()
|
||||
.then(done, done)
|
||||
|
||||
capitano.command
|
||||
@ -147,7 +83,6 @@ capitano.command(actions.keys.remove)
|
||||
|
||||
# ---------- Env Module ----------
|
||||
capitano.command(actions.env.list)
|
||||
capitano.command(actions.env.add)
|
||||
capitano.command(actions.env.rename)
|
||||
capitano.command(actions.env.remove)
|
||||
|
||||
@ -216,14 +151,13 @@ capitano.command(actions.push.push)
|
||||
capitano.command(actions.join.join)
|
||||
capitano.command(actions.leave.leave)
|
||||
|
||||
update.notify()
|
||||
|
||||
cli = capitano.parse(process.argv)
|
||||
runCommand = ->
|
||||
capitanoExecuteAsync = Promise.promisify(capitano.execute)
|
||||
if cli.global?.help
|
||||
capitanoExecuteAsync(command: "help #{cli.command ? ''}")
|
||||
else
|
||||
capitanoExecuteAsync(cli)
|
||||
|
||||
Promise.all([events.trackCommand(cli), runCommand()])
|
||||
.catch(errors.handle)
|
||||
.catch(require('./errors').handleError)
|
107
lib/app-common.ts
Normal file
107
lib/app-common.ts
Normal file
@ -0,0 +1,107 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Sentry.io setup
|
||||
* @see https://docs.sentry.io/clients/node/
|
||||
*/
|
||||
function setupRaven() {
|
||||
const Raven = require('raven');
|
||||
Raven.disableConsoleAlerts();
|
||||
Raven.config(require('./config').sentryDsn, {
|
||||
captureUnhandledRejections: true,
|
||||
autoBreadcrumbs: true,
|
||||
release: require('../package.json').version,
|
||||
}).install(function(_logged: any, error: Error) {
|
||||
console.error(error);
|
||||
return process.exit(1);
|
||||
});
|
||||
|
||||
Raven.setContext({
|
||||
extra: {
|
||||
args: process.argv,
|
||||
node_version: process.version,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function checkNodeVersion() {
|
||||
const validNodeVersions = require('../package.json').engines.node;
|
||||
if (!require('semver').satisfies(process.version, validNodeVersions)) {
|
||||
const { stripIndent } = require('common-tags');
|
||||
console.warn(stripIndent`
|
||||
------------------------------------------------------------------------------
|
||||
Warning: Node version "${
|
||||
process.version
|
||||
}" does not match required versions "${validNodeVersions}".
|
||||
This may cause unexpected behaviour. To upgrade Node, visit:
|
||||
https://nodejs.org/en/download/
|
||||
------------------------------------------------------------------------------
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
function setupGlobalHttpProxy() {
|
||||
// Doing this before requiring any other modules,
|
||||
// including the 'balena-sdk', to prevent any module from reading the http proxy config
|
||||
// before us
|
||||
const globalTunnel = require('global-tunnel-ng');
|
||||
const settings = require('balena-settings-client');
|
||||
let proxy;
|
||||
try {
|
||||
proxy = settings.get('proxy') || null;
|
||||
} catch (error1) {
|
||||
proxy = null;
|
||||
}
|
||||
|
||||
// Init the tunnel even if the proxy is not configured
|
||||
// because it can also get the proxy from the http(s)_proxy env var
|
||||
// If that is not set as well the initialize will do nothing
|
||||
globalTunnel.initialize(proxy);
|
||||
|
||||
// TODO: make this a feature of capitano https://github.com/balena-io/capitano/issues/48
|
||||
(global as any).PROXY_CONFIG = globalTunnel.proxyConfig;
|
||||
}
|
||||
|
||||
function setupBalenaSdkSharedOptions() {
|
||||
// We don't yet use balena-sdk directly everywhere, but we set up shared
|
||||
// options correctly so we can do safely in submodules
|
||||
const BalenaSdk = require('balena-sdk');
|
||||
const settings = require('balena-settings-client');
|
||||
BalenaSdk.setSharedOptions({
|
||||
apiUrl: settings.get('apiUrl'),
|
||||
imageMakerUrl: settings.get('imageMakerUrl'),
|
||||
dataDirectory: settings.get('dataDirectory'),
|
||||
retries: 2,
|
||||
});
|
||||
}
|
||||
|
||||
export function globalInit() {
|
||||
setupRaven();
|
||||
checkNodeVersion();
|
||||
setupGlobalHttpProxy();
|
||||
setupBalenaSdkSharedOptions();
|
||||
|
||||
// Assign bluebird as the global promise library.
|
||||
// stream-to-promise will produce native promises if not for this module,
|
||||
// which is likely to lead to errors as much of the CLI coffeescript code
|
||||
// expects bluebird promises.
|
||||
require('any-promise/register/bluebird');
|
||||
|
||||
// check for CLI updates once a day
|
||||
require('./utils/update').notify();
|
||||
}
|
37
lib/app-oclif.ts
Normal file
37
lib/app-oclif.ts
Normal file
@ -0,0 +1,37 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ExitError } from '@oclif/errors';
|
||||
|
||||
import { handleError } from './errors';
|
||||
|
||||
/**
|
||||
* oclif CLI entrypoint
|
||||
*/
|
||||
export function run(argv: string[]) {
|
||||
process.argv = argv;
|
||||
require('@oclif/command')
|
||||
.run()
|
||||
.then(require('@oclif/command/flush'))
|
||||
.catch((error: Error) => {
|
||||
// oclif sometimes exits with ExitError code 0 (not an error)
|
||||
if (error instanceof ExitError && error.oclif.exit === 0) {
|
||||
return;
|
||||
}
|
||||
handleError(error);
|
||||
});
|
||||
}
|
86
lib/app.ts
Normal file
86
lib/app.ts
Normal file
@ -0,0 +1,86 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Simple command-line pre-parsing to choose between oclif or Capitano.
|
||||
* @param argv process.argv
|
||||
*/
|
||||
function routeCliFramework(argv: string[]): void {
|
||||
if (process.env.DEBUG) {
|
||||
console.log(
|
||||
`Debug: original argv0="${process.argv0}" argv=[${argv}] length=${
|
||||
argv.length
|
||||
}`,
|
||||
);
|
||||
}
|
||||
const cmdSlice = argv.slice(2);
|
||||
let isOclif = false;
|
||||
|
||||
if (cmdSlice.length > 1) {
|
||||
// convert e.g. 'balena help env add' to 'balena env add --help'
|
||||
if (cmdSlice[0] === 'help') {
|
||||
cmdSlice.shift();
|
||||
cmdSlice.push('--help');
|
||||
}
|
||||
// Look for commands that have been transitioned to oclif
|
||||
isOclif = isOclifCommand(cmdSlice);
|
||||
if (isOclif) {
|
||||
// convert space-separated commands to oclif's topic:command syntax
|
||||
argv = [
|
||||
argv[0],
|
||||
argv[1],
|
||||
cmdSlice[0] + ':' + cmdSlice[1],
|
||||
...cmdSlice.slice(2),
|
||||
];
|
||||
}
|
||||
}
|
||||
if (isOclif) {
|
||||
if (process.env.DEBUG) {
|
||||
console.log(`Debug: oclif new argv=[${argv}] length=${argv.length}`);
|
||||
}
|
||||
require('./app-oclif').run(argv);
|
||||
} else {
|
||||
require('./app-capitano');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the CLI command has been converted from Capitano to ocif.
|
||||
* @param argvSlice process.argv.slice(2)
|
||||
*/
|
||||
function isOclifCommand(argvSlice: string[]): boolean {
|
||||
// Look for commands that have been transitioned to oclif
|
||||
if (argvSlice.length > 1) {
|
||||
// balena env add
|
||||
if (argvSlice[0] === 'env' && argvSlice[1] === 'add') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* CLI entrypoint, but see also `bin/balena` and `bin/balena-dev` which
|
||||
* call this function.
|
||||
*/
|
||||
export function run(): void {
|
||||
// globalInit() must be called very early on (before other imports) because
|
||||
// it sets up Sentry error reporting, global HTTP proxy settings, balena-sdk
|
||||
// shared options, and performs node version requirement checks.
|
||||
require('./app-common').globalInit();
|
||||
routeCliFramework(process.argv);
|
||||
}
|
@ -104,7 +104,7 @@ const messages: {
|
||||
$ balena login`,
|
||||
};
|
||||
|
||||
exports.handle = function(error: any) {
|
||||
export function handleError(error: any) {
|
||||
let message = interpret(error);
|
||||
if (message == null) {
|
||||
return;
|
||||
@ -122,4 +122,4 @@ exports.handle = function(error: any) {
|
||||
// Ignore any errors (from error logging, or timeouts)
|
||||
})
|
||||
.finally(() => process.exit(error.exitCode || 1));
|
||||
};
|
||||
}
|
||||
|
@ -223,3 +223,50 @@ export function retry<T>(
|
||||
}
|
||||
return promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a compare(a, b) function suitable for use as the argument for the
|
||||
* sort() method of an array. That function will use the given manuallySortedArray
|
||||
* as "sorting guidance":
|
||||
* - If both a and b are found in the manuallySortedArray, the returned
|
||||
* compare(a, b) function will follow that ordering.
|
||||
* - If neither a nor b are found in the manuallySortedArray, the returned
|
||||
* compare(a, b) function will compare a and b using the standard '<' and
|
||||
* '>' Javascript operators.
|
||||
* - If only a or only b are found in the manuallySortedArray, the returned
|
||||
* compare(a, b) function will consider the found element as being
|
||||
* "smaller than" the not-found element (i.e. found elements appeare before
|
||||
* not-found elements in sorted order).
|
||||
*
|
||||
* The equalityFunc() argument is a function used to compare the array items
|
||||
* against the manuallySortedArray. For example, if equalityFunc was (a, x) =>
|
||||
* a.startsWith(x), where a is an item being sorted and x is an item in the
|
||||
* manuallySortedArray, then the manuallySortedArray could contain prefix
|
||||
* substrings to guide the sorting.
|
||||
*
|
||||
* @param manuallySortedArray A pre-sorted array to guide the sorting
|
||||
* @param equalityFunc An optional function used to compare the items being
|
||||
* sorted against items in manuallySortedArray. It should return true if
|
||||
* the two items compare equal, otherwise false. The arguments are the
|
||||
* same as provided by the standard Javascript array.findIndex() method.
|
||||
*/
|
||||
export function getManualSortCompareFunction<T, U = T>(
|
||||
manuallySortedArray: U[],
|
||||
equalityFunc: (a: T, x: U, index: number, array: U[]) => boolean,
|
||||
): (a: T, b: T) => number {
|
||||
return function(a: T, b: T): number {
|
||||
const indexA = manuallySortedArray.findIndex((x, index, array) =>
|
||||
equalityFunc(a, x, index, array),
|
||||
);
|
||||
const indexB = manuallySortedArray.findIndex((x, index, array) =>
|
||||
equalityFunc(b, x, index, array),
|
||||
);
|
||||
if (indexA >= 0 && indexB >= 0) {
|
||||
return indexA - indexB;
|
||||
} else if (indexA < 0 && indexB < 0) {
|
||||
return a < b ? -1 : a > b ? 1 : 0;
|
||||
} else {
|
||||
return indexA < 0 ? 1 : -1;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
53
lib/utils/oclif-utils.ts
Normal file
53
lib/utils/oclif-utils.ts
Normal file
@ -0,0 +1,53 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as Config from '@oclif/config';
|
||||
|
||||
export const convertedCommands = {
|
||||
'env:add': 'env add',
|
||||
};
|
||||
|
||||
/**
|
||||
* This class is a partial copy-and-paste of
|
||||
* @oclif/plugin-help/command/CommandHelp, which is used to generate oclif's
|
||||
* command help output.
|
||||
*/
|
||||
export class CommandHelp {
|
||||
constructor(public command: { args: any[] }) {}
|
||||
|
||||
protected arg(arg: Config.Command['args'][0]): string {
|
||||
const name = arg.name.toUpperCase();
|
||||
if (arg.required) {
|
||||
return `${name}`;
|
||||
}
|
||||
return `[${name}]`;
|
||||
}
|
||||
|
||||
public defaultUsage(): string {
|
||||
return CommandHelp.compact([
|
||||
// this.command.id,
|
||||
this.command.args
|
||||
.filter(a => !a.hidden)
|
||||
.map(a => this.arg(a))
|
||||
.join(' '),
|
||||
]).join(' ');
|
||||
}
|
||||
|
||||
public static compact<T>(array: Array<T | undefined>): T[] {
|
||||
return array.filter((a): a is T => !!a);
|
||||
}
|
||||
}
|
14
package.json
14
package.json
@ -62,13 +62,22 @@
|
||||
"engines": {
|
||||
"node": ">=8.0"
|
||||
},
|
||||
"oclif": {
|
||||
"bin": "balena",
|
||||
"commands": "./build/actions-oclif",
|
||||
"macos": {
|
||||
"identifier": "io.balena.cli"
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@oclif/dev-cli": "^1.22.0",
|
||||
"@oclif/config": "^1.12.12",
|
||||
"@oclif/parser": "^3.7.3",
|
||||
"@types/archiver": "2.1.2",
|
||||
"@types/bluebird": "3.5.21",
|
||||
"@types/chokidar": "^1.7.5",
|
||||
"@types/common-tags": "1.4.0",
|
||||
"@types/dockerode": "2.5.5",
|
||||
"@types/es6-promise": "0.0.32",
|
||||
"@types/fs-extra": "5.0.4",
|
||||
"@types/is-root": "1.0.0",
|
||||
"@types/lodash": "4.14.112",
|
||||
@ -104,6 +113,8 @@
|
||||
"typescript": "3.4.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@oclif/command": "^1.5.12",
|
||||
"@oclif/errors": "^1.2.2",
|
||||
"@resin.io/valid-email": "^0.1.0",
|
||||
"@zeit/dockerignore": "0.0.3",
|
||||
"JSONStream": "^1.0.3",
|
||||
@ -156,6 +167,7 @@
|
||||
"moment-duration-format": "~2.2.2",
|
||||
"mz": "^2.6.0",
|
||||
"node-cleanup": "^2.1.2",
|
||||
"oclif": "^1.13.1",
|
||||
"opn": "^5.5.0",
|
||||
"prettyjson": "^1.1.3",
|
||||
"progress-stream": "^2.0.0",
|
||||
|
Loading…
Reference in New Issue
Block a user