Compare commits

...

17 Commits

Author SHA1 Message Date
ab1d8aa6ba (v14) Migrate tabular commands to new output framework
Change-type: patch
Signed-off-by: Scott Lowe <scott@balena.io>
2022-03-25 18:14:57 +01:00
d2330f9ed1 v13.1.13 2022-02-10 14:29:18 +02:00
cc19b00998 Merge pull request #2455 from balena-io/lucianbuzzo/drop-unused-fn-awaitdevice
Drop unused awaitDevice utility function
2022-02-10 12:27:25 +00:00
ed5ac75a10 v13.1.12 2022-02-09 09:24:17 +02:00
465b8a1b5e Merge pull request #2451 from balena-io/bump-preload-v12
Update balena-preload to v12
2022-02-09 07:22:04 +00:00
eccadbdcb9 Drop unused awaitDevice utility function
Change-type: patch
Signed-off-by: Lucian Buzzo <lucian.buzzo@gmail.com>
2022-02-01 17:43:28 +00:00
31eb734af1 Update balena-preload to v12
Update balena-preload from 11.0.0 to 12.0.0

Change-type: patch
Changelog-entry: preload: Stop using the deprecated /device-types/v1 API endpoints
Signed-off-by: Thodoris Greasidis <thodoris@balena.io>
2022-01-27 19:27:58 +02:00
fa7b59d64f v13.1.11 2022-01-20 01:55:52 +02:00
1e42bfa0d5 Merge pull request #2450 from balena-io/types-node-v12
chore: Update @types/node to v12.20.42
2022-01-19 23:53:08 +00:00
5464e550e7 chore: lib/auth/utils.ts: Replace deprecated url.resolve, use async/await
Change-type: patch
2022-01-19 22:48:46 +00:00
c0f27a663d chore: Update @types/node to v12.20.42
Change-type: patch
2022-01-19 22:48:46 +00:00
d1c61c62ab v13.1.10 2022-01-16 21:29:44 +02:00
a9691bff57 Merge pull request #2446 from balena-io/2445-min-node-version-12.8.0
Update docs and package.json re min Node.js supported version (12.8.0)
2022-01-16 19:27:33 +00:00
f5d09a43cd Update docs and package.json re min Node.js supported version (12.8.0)
Resolves: #2445
Change-type: patch
2022-01-16 18:44:45 +00:00
d11e547e11 v13.1.9 2022-01-14 03:00:53 +02:00
bd462aee02 Merge pull request #2443 from balena-io/colors-action
Update packages in response to colors package issues
2022-01-14 00:58:59 +00:00
f633c0468b Update packages in response to colors package issues
Change-type: patch
Signed-off-by: Scott Lowe <scott@balena.io>
2022-01-12 22:12:06 +01:00
38 changed files with 5958 additions and 4123 deletions

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,247 @@ All notable changes to this project will be documented in this file
automatically by Versionist. DO NOT EDIT THIS FILE MANUALLY!
This project adheres to [Semantic Versioning](http://semver.org/).
## 13.1.13 - 2022-02-10
* Drop unused awaitDevice utility function [Lucian Buzzo]
## 13.1.12 - 2022-02-09
<details>
<summary> preload: Stop using the deprecated /device-types/v1 API endpoints [Thodoris Greasidis] </summary>
> ### balena-preload-12.0.0 - 2022-01-27
>
> * Improve types [Thodoris Greasidis]
> * Stop relying on the /device-types/v1 endpoints [Thodoris Greasidis]
> * Bump TypeScript to v4.5 [Thodoris Greasidis]
>
> <details>
> <summary> Bump balena-sdk to v16 [Thodoris Greasidis] </summary>
>
>> #### balena-sdk-16.0.0 - 2021-11-28
>>
>> * **BREAKING**: Merge the hostApp model into the OS model [Thodoris Greasidis]
>> * **BREAKING** Drop os.getSupportedVersions() method in favor of hostapp.getAvailableOsVersions() [Thodoris Greasidis]
>> * os.getMaxSatisfyingVersion: Add optional param to choose OS line type [Thodoris Greasidis]
>> * os.getMaxSatisfyingVersion: Include ESR versions [Thodoris Greasidis]
>> * os.getMaxSatisfyingVersion: Switch to use hostApps [Thodoris Greasidis]
>> * hostapp.getAvailableOsVersions: Add single device type argument overload [Thodoris Greasidis]
>> * hostapp.getAllOsVersions: Add single device type argument overload [Thodoris Greasidis]
>> * models.hostapp: Add a getAvailableOsVersions() convenience method [Thodoris Greasidis]
>> * Support optional extra PineOptions in hostapp.getAllOsVersions() [Thodoris Greasidis]
>> * **BREAKING** Include invalidated versions in hostapp.getAllOsVersions() [Thodoris Greasidis]
>> * models/application: Add getDirectlyAccessible & getAllDirectlyAccessible [Thodoris Greasidis]
>> * application.get: Add 'directly_accessible' convenience filter param [Thodoris Greasidis]
>> * application.getAll: Add 'directly_accessible' convenience filter param [Thodoris Greasidis]
>> * **BREAKING** Change application.getAll to include public apps [Thodoris Greasidis]
>> * **BREAKING** Drop targeting/retrieving apps by name in favor of slugs [Thodoris Greasidis]
>> * Bump minimum supported Typescript to v4.5.2 [Thodoris Greasidis]
>> * **BREAKING**: Stop actively supporting node 10 [Thodoris Greasidis]
>> * **BREAKING** Drop application.getAllWithDeviceServiceDetails() [Thodoris Greasidis]
>> * **BREAKING** Change apiKey.getAll() to return all key variants [Thodoris Greasidis]
>> * types: Drop is_in_local_mode from the Device model [Thodoris Greasidis]
>> * types: Drop user__is_member_of__application in favor of the term form [Thodoris Greasidis]
>> * typings: Drop Subscription's discounts__plan_addon property [Thodoris Greasidis]
>> * typings: Stop extending the JWTUser type in the User model [Thodoris Greasidis]
>> * models/config: Change the BETA device type state to NEW [Thodoris Greasidis]
>> * typings: Drop the PineWithSelectOnGet type [Thodoris Greasidis]
>> * Remove my_application from the supported resources [Thodoris Greasidis]
>> * typings: Properly type some Device properties [Thodoris Greasidis]
>> * typings: Drop the DeviceWithImageInstalls type [Thodoris Greasidis]
>>
>> #### balena-sdk-15.59.2 - 2021-11-28
>>
>>
>> <details>
>> <summary> Update balena-request to 11.5.0 [Thodoris Greasidis] </summary>
>>
>>> ##### balena-request-11.5.0 - 2021-11-28
>>>
>>> * Convert tests to JavaScript and drop coffeescript [Thodoris Greasidis]
>>> * Fix the jsdoc generation [Thodoris Greasidis]
>>> * Convert to typescript and publish typings [Thodoris Greasidis]
>>>
>> </details>
>>
>>
>> #### balena-sdk-15.59.1 - 2021-11-28
>>
>> * Fix the typings of the Image contract field [Thodoris Greasidis]
>> * Fix the typings for the Release contract field [Thodoris Greasidis]
>>
>> #### balena-sdk-15.59.0 - 2021-11-24
>>
>> * Add release setIsInvalidated function [Matthew Yarmolinsky]
>>
>> #### balena-sdk-15.58.1 - 2021-11-17
>>
>> * Update typescript to 4.5.2 [Thodoris Greasidis]
>>
>> #### balena-sdk-15.58.0 - 2021-11-16
>>
>> * models/release: Add note() method [Thodoris Greasidis]
>> * typings: Add the release.invalidation_reason property [Thodoris Greasidis]
>> * typings: Add the release.note property [Thodoris Greasidis]
>>
>> #### balena-sdk-15.57.2 - 2021-11-15
>>
>> * tests/logs: Increase the wait time for retrieving the subscribed logs [Thodoris Greasidis]
>> * tests/logs: Refactor to async-await [Thodoris Greasidis]
>>
>> #### balena-sdk-15.57.1 - 2021-11-11
>>
>> * typings: Fix $filters for resources with non numeric ids [Thodoris Greasidis]
>> * typings: Add application.can_use__application_as_host ReverseNavigation [Thodoris Greasidis]
>> * Add missing apiKey.getDeviceApiKeysByDevice docs [Thodoris Greasidis]
>>
>> #### balena-sdk-15.57.0 - 2021-11-05
>>
>> * models/api-key: Change update() & revoke() to work with all key variants [Thodoris Greasidis]
>>
>> #### balena-sdk-15.56.0 - 2021-11-04
>>
>> * models/apiKey: Add getDeviceApiKeysByDevice() method [Thodoris Greasidis]
>>
>> #### balena-sdk-15.55.0 - 2021-11-01
>>
>> * typings: Add the release.raw_version property [Thodoris Greasidis]
>>
>> #### balena-sdk-15.54.2 - 2021-10-25
>>
>> * application/create: Rely on the hostApps for detecting discontinued DTs [Thodoris Greasidis]
>>
>> #### balena-sdk-15.54.1 - 2021-10-22
>>
>> * tests/device: Async-await conversions & abstraction on multi-field tests [Thodoris Greasidis]
>>
>> #### balena-sdk-15.54.0 - 2021-10-20
>>
>> * tests: Register devices in chunks of 10 to avoid uuid conflicts in node [Thodoris Greasidis]
>> * Add known issue check on release isReccomanded logic [JSReds]
>> * Add known_issue_list to hostApp.getOsVersions() [JSReds]
>>
>> #### balena-sdk-15.53.0 - 2021-10-07
>>
>> * Add support for batch device supervisor updates [Thodoris Greasidis]
>>
>> #### balena-sdk-15.52.0 - 2021-10-06
>>
>> * Add support for batch device pinning to release [Thodoris Greasidis]
>>
>> #### balena-sdk-15.51.4 - 2021-09-28
>>
>> * auth.isLoggedIn: Treat BalenaExpiredToken errors as logged out indicator [Thodoris Greasidis]
>>
>> #### balena-sdk-15.51.3 - 2021-09-28
>>
>> * Convert application spec to TypeScript [Thodoris Greasidis]
>>
>> #### balena-sdk-15.51.2 - 2021-09-28
>>
>> * application.trackLatestRelease: Fix using draft/invalidated releases [Thodoris Greasidis]
>> * application.isTrackingLatestRelease: Exclude draft&invalidated releases [Thodoris Greasidis]
>>
>> #### balena-sdk-15.51.1 - 2021-09-20
>>
>>
>> <details>
>> <summary> Update balena-request to v11.4.2 [Kyle Harding] </summary>
>>
>>> ##### balena-request-11.4.2 - 2021-09-20
>>>
>>> * Allow overriding the default zlib flush setting [Kyle Harding]
>>>
>> </details>
>>
>>
>> #### balena-sdk-15.51.0 - 2021-09-16
>>
>> * os.getConfig: Add typings for the provisioningKeyName option [Nitish Agarwal]
>>
>> #### balena-sdk-15.50.1 - 2021-09-13
>>
>> * models/os: Always first normalize the device type slug [Thodoris Greasidis]
>>
>> #### balena-sdk-15.50.0 - 2021-09-10
>>
>> * Add release.finalize to promote draft releases to final [toochevere]
>>
>> #### balena-sdk-15.49.1 - 2021-09-10
>>
>> * typings: Drop the v5-model-only application_type.is_host_os [Thodoris Greasidis]
>>
>> #### balena-sdk-15.49.0 - 2021-09-06
>>
>> * os.getSupportedOsUpdateVersions: Use the hostApp releases [Thodoris Greasidis]
>> * os.download: Use the hostApp for finding the latest release [Thodoris Greasidis]
>>
>> #### balena-sdk-15.48.3 - 2021-08-27
>>
>>
>> <details>
>> <summary> Update balena-request to 11.4.1 [Kyle Harding] </summary>
>>
>>> ##### balena-request-11.4.1 - 2021-08-27
>>>
>>> * Allow more lenient gzip decompression [Kyle Harding]
>>>
>> </details>
>>
>>
>> #### balena-sdk-15.48.2 - 2021-08-27
>>
>> * Improve hostapp.getAllOsVersions performance & reduce fetched data [Thodoris Greasidis]
>>
>> #### balena-sdk-15.48.1 - 2021-08-27
>>
>> * Update typescript to 4.4.2 [Thodoris Greasidis]
>>
>> #### balena-sdk-15.48.0 - 2021-08-15
>>
>> * Deprecate the release.release_version property [Thodoris Greasidis]
>> * typings: Add the release versioning properties [Thodoris Greasidis]
>>
>> #### balena-sdk-15.47.1 - 2021-08-10
>>
>> * Run browser tests using the minified browser bundle [Thodoris Greasidis]
>> * Move to uglify-js to fix const assignment bug in minified build [Thodoris Greasidis]
>>
>> #### balena-sdk-15.47.0 - 2021-08-09
>>
>> * typings: Add the release.is_final & is_finalized_at__date properties [Thodoris Greasidis]
>>
>> #### balena-sdk-15.46.1 - 2021-07-28
>>
>> * apiKey.getAll: Return only NamedUserApiKeys for backwards compatibility [Thodoris Greasidis]
>>
>> #### balena-sdk-15.46.0 - 2021-07-27
>>
>> * Add email verification & email request methods [Nitish Agarwal]
>>
>> #### balena-sdk-15.45.0 - 2021-07-26
>>
>> * Update generateProvisioningKey to include keyName [Nitish Agarwal]
>>
> </details>
>
>
</details>
## 13.1.11 - 2022-01-19
* chore: lib/auth/utils.ts: Replace deprecated url.resolve, use async/await [Paulo Castro]
* chore: Update @types/node to v12.20.42 [Paulo Castro]
## 13.1.10 - 2022-01-16
* Update docs and package.json re min Node.js supported version (12.8.0) [Paulo Castro]
## 13.1.9 - 2022-01-14
* Update packages in response to `colors` package issues [Scott Lowe]
## 13.1.8 - 2022-01-11
* local push: Fix "invalid character '/' looking for beginning of value" [Paulo Castro]

View File

@ -78,7 +78,7 @@ If you are a Node.js developer, you may wish to install the balena CLI via [npm]
The npm installation involves building native (platform-specific) binary modules, which require
some development tools to be installed first, as follows.
> **The balena CLI currently requires Node.js version 10 (min 10.20.0) or 12.**
> **The balena CLI currently requires Node.js version 12 (min 12.8.0).**
> **Versions 13 and later are not yet fully supported.**
### Install development tools

View File

@ -36,8 +36,8 @@ const run = async (cmd: string) => {
}
resolve({ stdout, stderr });
});
p.stdout.pipe(process.stdout);
p.stderr.pipe(process.stderr);
p.stdout?.pipe(process.stdout);
p.stderr?.pipe(process.stderr);
});
};

View File

@ -117,7 +117,7 @@ export async function which(program: string): Promise<string> {
*/
export async function whichSpawn(
programName: string,
args?: string[],
args: string[] = [],
): Promise<void> {
const program = await which(programName);
let error: Error | undefined;

View File

@ -333,6 +333,30 @@ Examples:
### Options
#### --fields FIELDS
only show provided fields (comma-separated)
#### -j, --json
output in json format
#### --filter FILTER
filter results by substring matching of a given field, eg: --filter field=foo
#### --no-header
hide table header from output
#### --no-truncate
do not truncate output to fit screen
#### --sort SORT
field to sort by (prepend '-' for descending order)
## fleet &#60;fleet&#62;
Display detailed information about a single fleet.
@ -362,6 +386,14 @@ fleet name, slug (preferred), or numeric ID (deprecated)
### Options
#### --fields FIELDS
only show provided fields (comma-separated)
#### -j, --json
output in json format
## fleet create &#60;name&#62;
Create a new balena fleet.
@ -648,9 +680,29 @@ Examples:
fleet name, slug (preferred), or numeric ID (deprecated)
#### --fields FIELDS
only show provided fields (comma-separated)
#### -j, --json
produce JSON output instead of tabular output
output in json format
#### --filter FILTER
filter results by substring matching of a given field, eg: --filter field=foo
#### --no-header
hide table header from output
#### --no-truncate
do not truncate output to fit screen
#### --sort SORT
field to sort by (prepend '-' for descending order)
## devices supported
@ -669,9 +721,29 @@ Examples:
### Options
#### --fields FIELDS
only show provided fields (comma-separated)
#### -j, --json
produce JSON output instead of tabular output
output in json format
#### --filter FILTER
filter results by substring matching of a given field, eg: --filter field=foo
#### --no-header
hide table header from output
#### --no-truncate
do not truncate output to fit screen
#### --sort SORT
field to sort by (prepend '-' for descending order)
## device &#60;uuid&#62;
@ -689,6 +761,14 @@ the device uuid
### Options
#### --fields FIELDS
only show provided fields (comma-separated)
#### -j, --json
output in json format
## device deactivate &#60;uuid&#62;
Deactivate a device.
@ -1152,6 +1232,14 @@ fleet name or slug (preferred)
### Options
#### --fields FIELDS
only show provided fields (comma-separated)
#### -j, --json
output in json format
## release &#60;commitOrId&#62;
@ -1173,6 +1261,14 @@ the commit or ID of the release to get information
Return the release composition
#### --fields FIELDS
only show provided fields (comma-separated)
#### -j, --json
output in json format
## release finalize &#60;commitOrId&#62;
Finalize a release. Releases can be "draft" or "final", and this command
@ -1271,9 +1367,29 @@ show configuration variables only
device UUID
#### --fields FIELDS
only show provided fields (comma-separated)
#### -j, --json
produce JSON output instead of tabular output
output in json format
#### --filter FILTER
filter results by substring matching of a given field, eg: --filter field=foo
#### --no-header
hide table header from output
#### --no-truncate
do not truncate output to fit screen
#### --sort SORT
field to sort by (prepend '-' for descending order)
#### -s, --service SERVICE
@ -1526,6 +1642,30 @@ device UUID
release id
#### --fields FIELDS
only show provided fields (comma-separated)
#### -j, --json
output in json format
#### --filter FILTER
filter results by substring matching of a given field, eg: --filter field=foo
#### --no-header
hide table header from output
#### --no-truncate
do not truncate output to fit screen
#### --sort SORT
field to sort by (prepend '-' for descending order)
## tag rm &#60;tagKey&#62;
Remove a tag from a fleet, device or release.
@ -1694,6 +1834,30 @@ Examples:
### Options
#### --fields FIELDS
only show provided fields (comma-separated)
#### -j, --json
output in json format
#### --filter FILTER
filter results by substring matching of a given field, eg: --filter field=foo
#### --no-header
hide table header from output
#### --no-truncate
do not truncate output to fit screen
#### --sort SORT
field to sort by (prepend '-' for descending order)
## key &#60;id&#62;
Display a single SSH key registered in balenaCloud for the logged in user.
@ -1710,6 +1874,14 @@ balenaCloud ID for the SSH key
### Options
#### --fields FIELDS
only show provided fields (comma-separated)
#### -j, --json
output in json format
## key add &#60;name&#62; [path]
Add an SSH key to the balenaCloud account of the logged in user.
@ -2394,10 +2566,6 @@ the path to the config.json file to inject
### Options
#### -t, --type TYPE
ignored - no longer required
#### -d, --drive DRIVE
path to OS image file (e.g. balena.img) or block device (e.g. /dev/disk2)
@ -2418,10 +2586,6 @@ Examples:
### Options
#### -t, --type TYPE
ignored - no longer required
#### -d, --drive DRIVE
path to OS image file (e.g. balena.img) or block device (e.g. /dev/disk2)
@ -2449,10 +2613,6 @@ Examples:
### Options
#### -t, --type TYPE
ignored - no longer required
#### -d, --drive DRIVE
path to OS image file (e.g. balena.img) or block device (e.g. /dev/disk2)
@ -2491,10 +2651,6 @@ the value of the config parameter to write
### Options
#### -t, --type TYPE
ignored - no longer required
#### -d, --drive DRIVE
path to OS image file (e.g. balena.img) or block device (e.g. /dev/disk2)
@ -2843,6 +2999,14 @@ Examples:
### Options
#### --fields FIELDS
only show provided fields (comma-separated)
#### -j, --json
output in json format
# Local
## local configure &#60;target&#62;

View File

@ -14,57 +14,44 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import * as _ from 'lodash';
import * as url from 'url';
import { getBalenaSdk } from '../utils/lazy';
/**
* @summary Get dashboard CLI login URL
* @function
* @protected
* Get dashboard CLI login URL
*
* @param {String} callbackUrl - callback url
* @fulfil {String} - dashboard login url
* @returns {Promise}
*
* @example
* utils.getDashboardLoginURL('http://127.0.0.1:3000').then (url) ->
* console.log(url)
* @param callbackUrl - Callback url, e.g. 'http://127.0.0.1:3000'
* @returns Dashboard login URL, e.g.:
* 'https://dashboard.balena-cloud.com/login/cli/http%253A%252F%252F127.0.0.1%253A59581%252Fauth'
*/
export const getDashboardLoginURL = (callbackUrl: string) => {
export async function getDashboardLoginURL(
callbackUrl: string,
): Promise<string> {
// Encode percentages signs from the escaped url
// characters to avoid angular getting confused.
callbackUrl = encodeURIComponent(callbackUrl).replace(/%/g, '%25');
return getBalenaSdk()
.settings.get('dashboardUrl')
.then((dashboardUrl) =>
url.resolve(dashboardUrl, `/login/cli/${callbackUrl}`),
);
};
const [{ URL }, dashboardUrl] = await Promise.all([
import('url'),
getBalenaSdk().settings.get('dashboardUrl'),
]);
return new URL(`/login/cli/${callbackUrl}`, dashboardUrl).href;
}
/**
* @summary Log in using a token, but only if the token is valid
* @function
* @protected
* Log in using a token, but only if the token is valid.
*
* @description
* This function checks that the token is not only well-structured
* but that it also authenticates with the server successfully.
*
* If authenticated, the token is persisted, if not then the previous
* login state is restored.
*
* @param {String} token - session token or api key
* @fulfil {Boolean} - whether the login was successful or not
* @returns {Promise}
*
* utils.loginIfTokenValid('...').then (loggedIn) ->
* if loggedIn
* console.log('Token is valid!')
* @param token - session token or api key
* @returns whether the login was successful or not
*/
export const loginIfTokenValid = async (token: string): Promise<boolean> => {
if (_.isEmpty(token?.trim())) {
export async function loginIfTokenValid(token?: string): Promise<boolean> {
token = (token || '').trim();
if (!token) {
return false;
}
const balena = getBalenaSdk();
@ -86,4 +73,4 @@ export const loginIfTokenValid = async (token: string): Promise<boolean> => {
}
}
return isLoggedIn;
};
}

View File

@ -171,4 +171,5 @@ export default abstract class BalenaCommand extends Command {
protected outputMessage = output.outputMessage;
protected outputData = output.outputData;
protected printTitle = output.printTitle;
}

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
* Copyright 2016 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -22,8 +22,10 @@ import * as cf from '../../utils/common-flags';
import { expandForAppName } from '../../utils/helpers';
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
import { tryAsInteger } from '../../utils/validation';
import type { Application, Release } from 'balena-sdk';
import type { DataOutputOptions } from '../../framework';
import { isV14 } from '../../utils/version';
interface ExtendedDevice extends DeviceWithDeviceType {
dashboard_url?: string;
@ -42,7 +44,7 @@ interface ExtendedDevice extends DeviceWithDeviceType {
undervoltage_detected?: boolean;
}
interface FlagsDef {
interface FlagsDef extends DataOutputOptions {
help: void;
}
@ -71,13 +73,16 @@ export default class DeviceCmd extends Command {
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
...(isV14() ? cf.dataOutputFlags : {}),
};
public static authenticated = true;
public static primary = true;
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(DeviceCmd);
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
DeviceCmd,
);
const balena = getBalenaSdk();
@ -163,37 +168,52 @@ export default class DeviceCmd extends Command {
);
}
console.log(
getVisuals().table.vertical(device, [
`$${device.device_name}$`,
'id',
'device_type',
'status',
'is_online',
'ip_address',
'public_address',
'mac_address',
'fleet',
'last_seen',
'uuid',
'commit',
'supervisor_version',
'is_web_accessible',
'note',
'os_version',
'dashboard_url',
'cpu_usage_percent',
'cpu_temp_c',
'cpu_id',
'memory_usage_mb',
'memory_total_mb',
'memory_usage_percent',
'storage_block_device',
'storage_usage_mb',
'storage_total_mb',
'storage_usage_percent',
'undervoltage_detected',
]),
);
const outputFields = [
'device_name',
'id',
'device_type',
'status',
'is_online',
'ip_address',
'public_address',
'mac_address',
'fleet',
'last_seen',
'uuid',
'commit',
'supervisor_version',
'is_web_accessible',
'note',
'os_version',
'dashboard_url',
'cpu_usage_percent',
'cpu_temp_c',
'cpu_id',
'memory_usage_mb',
'memory_total_mb',
'memory_usage_percent',
'storage_block_device',
'storage_usage_mb',
'storage_total_mb',
'storage_usage_percent',
'undervoltage_detected',
];
if (isV14()) {
await this.outputData(device, outputFields, {
...options,
hideNullOrUndefinedValues: true,
titleField: 'device_name',
});
} else {
// Old output implementation
outputFields.unshift(`$${device.device_name}$`);
console.log(
getVisuals().table.vertical(
device,
outputFields.filter((f) => f !== 'device_name'),
),
);
}
}
}

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
* Copyright 2016 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -21,8 +21,10 @@ import * as cf from '../../utils/common-flags';
import { expandForAppName } from '../../utils/helpers';
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
import { applicationIdInfo, jsonInfo } from '../../utils/messages';
import type { Application } from 'balena-sdk';
import type { DataSetOutputOptions } from '../../framework';
import { isV14 } from '../../utils/version';
interface ExtendedDevice extends DeviceWithDeviceType {
dashboard_url?: string;
@ -30,10 +32,10 @@ interface ExtendedDevice extends DeviceWithDeviceType {
device_type?: string | null;
}
interface FlagsDef {
interface FlagsDef extends DataSetOutputOptions {
fleet?: string;
help: void;
json: boolean;
json?: boolean;
}
export default class DevicesCmd extends Command {
@ -58,12 +60,11 @@ export default class DevicesCmd extends Command {
public static flags: flags.Input<FlagsDef> = {
fleet: cf.fleet,
json: cf.json,
...(isV14() ? cf.dataSetOutputFlags : { json: cf.json }),
help: cf.help,
};
public static primary = true;
public static authenticated = true;
public async run() {
@ -99,31 +100,52 @@ export default class DevicesCmd extends Command {
return device;
});
const fields = [
'id',
'uuid',
'device_name',
'device_type',
'fleet',
'status',
'is_online',
'supervisor_version',
'os_version',
'dashboard_url',
];
if (isV14()) {
const outputFields = [
'id',
'uuid',
'device_name',
'device_type',
'fleet',
'status',
'is_online',
'supervisor_version',
'os_version',
'dashboard_url',
];
if (options.json) {
const { pickAndRename } = await import('../../utils/helpers');
const mapped = devices.map((device) => pickAndRename(device, fields));
console.log(JSON.stringify(mapped, null, 4));
await this.outputData(devices, outputFields, {
...options,
displayNullValuesAs: 'N/a',
});
} else {
const _ = await import('lodash');
console.log(
getVisuals().table.horizontal(
devices.map((dev) => _.mapValues(dev, (val) => val ?? 'N/a')),
fields,
),
);
// Old output implementation
const fields = [
'id',
'uuid',
'device_name',
'device_type',
'fleet',
'status',
'is_online',
'supervisor_version',
'os_version',
'dashboard_url',
];
if (options.json) {
const { pickAndRename } = await import('../../utils/helpers');
const mapped = devices.map((device) => pickAndRename(device, fields));
console.log(JSON.stringify(mapped, null, 4));
} else {
const _ = await import('lodash');
console.log(
getVisuals().table.horizontal(
devices.map((dev) => _.mapValues(dev, (val) => val ?? 'N/a')),
fields,
),
);
}
}
}
}

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2016-2021 Balena Ltd.
* Copyright 2016 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -17,12 +17,14 @@
import { flags } from '@oclif/command';
import * as _ from 'lodash';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
import { CommandHelp } from '../../utils/oclif-utils';
import type { DataSetOutputOptions } from '../../framework';
interface FlagsDef {
import { isV14 } from '../../utils/version';
interface FlagsDef extends DataSetOutputOptions {
help: void;
json?: boolean;
}
@ -51,10 +53,7 @@ export default class DevicesSupportedCmd extends Command {
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
json: flags.boolean({
char: 'j',
description: 'produce JSON output instead of tabular output',
}),
...(isV14() ? cf.dataSetOutputFlags : { json: cf.json }),
};
public async run() {
@ -70,7 +69,7 @@ export default class DevicesSupportedCmd extends Command {
const configDTsBySlug = _.keyBy(configDTs, (dt) => dt.slug);
interface DT {
slug: string;
aliases: string[];
aliases: string[] | string;
arch: string;
name: string;
}
@ -84,19 +83,25 @@ export default class DevicesSupportedCmd extends Command {
const dt: Partial<typeof dts[0]> = dtsBySlug[slug] || {};
deviceTypes.push({
slug,
aliases: options.json ? aliases : [aliases.join(', ')],
aliases: options.json ? aliases : aliases.join(', '),
arch: (dt.is_of__cpu_architecture as any)?.[0]?.slug || 'n/a',
name: dt.name || 'N/A',
});
}
const fields = ['slug', 'aliases', 'arch', 'name'];
deviceTypes = _.sortBy(deviceTypes, fields);
if (options.json) {
console.log(JSON.stringify(deviceTypes, null, 4));
if (isV14()) {
await this.outputData(deviceTypes, fields, options);
} else {
const visuals = getVisuals();
const output = await visuals.table.horizontal(deviceTypes, fields);
console.log(output);
// Old output implementation
if (options.json) {
console.log(JSON.stringify(deviceTypes, null, 4));
} else {
const visuals = getVisuals();
const output = await visuals.table.horizontal(deviceTypes, fields);
console.log(output);
}
}
}
}

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2016-2021 Balena Ltd.
* Copyright 2016 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -22,12 +22,15 @@ import { ExpectedError } from '../errors';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
import { applicationIdInfo } from '../utils/messages';
import type { DataSetOutputOptions } from '../framework';
interface FlagsDef {
import { isV14 } from '../utils/version';
interface FlagsDef extends DataSetOutputOptions {
fleet?: string;
config: boolean;
device?: string; // device UUID
json: boolean;
json?: boolean;
help: void;
service?: string; // service name
}
@ -113,7 +116,7 @@ export default class EnvsCmd extends Command {
}),
device: { ...cf.device, exclusive: ['fleet'] },
help: cf.help,
json: cf.json,
...(isV14() ? cf.dataSetOutputFlags : { json: cf.json }),
service: { ...cf.service, exclusive: ['config'] },
};
@ -181,24 +184,59 @@ export default class EnvsCmd extends Command {
return i;
});
if (options.device) {
fields.push(options.json ? 'deviceUUID' : 'deviceUUID => DEVICE');
}
if (!options.config) {
fields.push(options.json ? 'serviceName' : 'serviceName => SERVICE');
}
if (isV14()) {
const results = [...varArray] as any;
if (options.json) {
const { pickAndRename } = await import('../utils/helpers');
const mapped = varArray.map((o) => pickAndRename(o, fields));
this.log(JSON.stringify(mapped, null, 4));
// Rename fields
if (options.device) {
if (options.json) {
fields.push('deviceUUID');
} else {
results.forEach((r: any) => {
r.device = r.deviceUUID;
delete r.deviceUUID;
});
fields.push('device');
}
}
if (!options.config) {
if (options.json) {
fields.push('serviceName');
} else {
results.forEach((r: any) => {
r.service = r.serviceName;
delete r.serviceName;
});
fields.push('service');
}
}
await this.outputData(results, fields, {
...options,
sort: options.sort || 'name',
});
} else {
this.log(
getVisuals().table.horizontal(
_.sortBy(varArray, (v: SDK.EnvironmentVariableBase) => v.name),
fields,
),
);
// Old output implementation
if (options.device) {
fields.push(options.json ? 'deviceUUID' : 'deviceUUID => DEVICE');
}
if (!options.config) {
fields.push(options.json ? 'serviceName' : 'serviceName => SERVICE');
}
if (options.json) {
const { pickAndRename } = await import('../utils/helpers');
const mapped = varArray.map((o) => pickAndRename(o, fields));
this.log(JSON.stringify(mapped, null, 4));
} else {
this.log(
getVisuals().table.horizontal(
_.sortBy(varArray, (v: SDK.EnvironmentVariableBase) => v.name),
fields,
),
);
}
}
}
}

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
* Copyright 2016 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -20,10 +20,13 @@ import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
import { parseAsInteger } from '../../utils/validation';
import type { DataOutputOptions } from '../../framework';
import { isV14 } from '../../utils/version';
type IArg<T> = import('@oclif/parser').args.IArg<T>;
interface FlagsDef {
interface FlagsDef extends DataOutputOptions {
help: void;
}
@ -52,27 +55,52 @@ export default class KeyCmd extends Command {
public static usage = 'key <id>';
public static flags: flags.Input<FlagsDef> = {
...(isV14() ? cf.dataOutputFlags : {}),
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params } = this.parse<{}, ArgsDef>(KeyCmd);
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
KeyCmd,
);
const key = await getBalenaSdk().models.key.get(params.id);
// Use 'name' instead of 'title' to match dashboard.
const displayKey = {
id: key.id,
name: key.title,
};
if (isV14()) {
// Use 'name' instead of 'title' to match dashboard.
const displayKey = {
id: key.id,
name: key.title,
public_key: key.public_key,
};
console.log(getVisuals().table.vertical(displayKey, ['id', 'name']));
if (!options.json) {
// Id is redundant, since user must have provided it in command call
this.printTitle(displayKey.name);
this.outputMessage(displayKey.public_key);
} else {
await this.outputData(
displayKey,
['id', 'name', 'public_key'],
options,
);
}
} else {
// Old output implementation
// Use 'name' instead of 'title' to match dashboard.
const displayKey = {
id: key.id,
name: key.title,
};
// Since the public key string is long, it might
// wrap to lines below, causing the table layout to break.
// See https://github.com/balena-io/balena-cli/issues/151
console.log('\n' + key.public_key);
console.log(getVisuals().table.vertical(displayKey, ['id', 'name']));
// Since the public key string is long, it might
// wrap to lines below, causing the table layout to break.
// See https://github.com/balena-io/balena-cli/issues/151
console.log('\n' + key.public_key);
}
}
}

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
* Copyright 2016-2022 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -19,8 +19,11 @@ import { flags } from '@oclif/command';
import Command from '../command';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
import type { DataSetOutputOptions } from '../framework';
interface FlagsDef {
import { isV14 } from '../utils/version';
interface FlagsDef extends DataSetOutputOptions {
help: void;
}
@ -35,13 +38,14 @@ export default class KeysCmd extends Command {
public static usage = 'keys';
public static flags: flags.Input<FlagsDef> = {
...(isV14() ? cf.dataSetOutputFlags : {}),
help: cf.help,
};
public static authenticated = true;
public async run() {
this.parse<FlagsDef, {}>(KeysCmd);
const { flags: options } = this.parse<FlagsDef, {}>(KeysCmd);
const keys = await getBalenaSdk().models.key.getAll();
@ -50,6 +54,12 @@ export default class KeysCmd extends Command {
return { id: k.id, name: k.title };
});
console.log(getVisuals().table.horizontal(displayKeys, ['id', 'name']));
// Display
if (isV14()) {
await this.outputData(displayKeys, ['id', 'name'], options);
} else {
// Old output implementation
console.log(getVisuals().table.horizontal(displayKeys, ['id', 'name']));
}
}
}

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
* Copyright 2016-2022 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -19,8 +19,11 @@ import { flags } from '@oclif/command';
import Command from '../command';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
import type { DataSetOutputOptions } from '../framework';
interface FlagsDef {
import { isV14 } from '../utils/version';
interface FlagsDef extends DataSetOutputOptions {
help: void;
}
@ -36,12 +39,13 @@ export default class OrgsCmd extends Command {
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
...(isV14() ? cf.dataSetOutputFlags : {}),
};
public static authenticated = true;
public async run() {
this.parse<FlagsDef, {}>(OrgsCmd);
const { flags: options } = this.parse<FlagsDef, {}>(OrgsCmd);
const { getOwnOrganizations } = await import('../utils/sdk');
@ -49,8 +53,13 @@ export default class OrgsCmd extends Command {
const organizations = await getOwnOrganizations(getBalenaSdk());
// Display
console.log(
getVisuals().table.horizontal(organizations, ['name', 'handle']),
);
if (isV14()) {
await this.outputData(organizations, ['name', 'handle'], options);
} else {
// Old output implementation
console.log(
getVisuals().table.horizontal(organizations, ['name', 'handle']),
);
}
}
}

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
* Copyright 2016 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -22,8 +22,11 @@ import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
import type * as BalenaSdk from 'balena-sdk';
import jsyaml = require('js-yaml');
import { tryAsInteger } from '../../utils/validation';
import type { DataOutputOptions } from '../../framework';
interface FlagsDef {
import { isV14 } from '../../utils/version';
interface FlagsDef extends DataOutputOptions {
help: void;
composition?: boolean;
}
@ -49,7 +52,9 @@ export default class ReleaseCmd extends Command {
default: false,
char: 'c',
description: 'Return the release composition',
exclusive: ['json', 'fields'],
}),
...(isV14() ? cf.dataOutputFlags : {}),
};
public static args = [
@ -68,29 +73,27 @@ export default class ReleaseCmd extends Command {
ReleaseCmd,
);
const balena = getBalenaSdk();
if (options.composition) {
await this.showComposition(params.commitOrId, balena);
await this.showComposition(params.commitOrId);
} else {
await this.showReleaseInfo(params.commitOrId, balena);
await this.showReleaseInfo(params.commitOrId, options);
}
}
async showComposition(
commitOrId: string | number,
balena: BalenaSdk.BalenaSDK,
) {
const release = await balena.models.release.get(commitOrId, {
async showComposition(commitOrId: string | number) {
const release = await getBalenaSdk().models.release.get(commitOrId, {
$select: 'composition',
});
console.log(jsyaml.dump(release.composition));
if (isV14()) {
this.outputMessage(jsyaml.dump(release.composition));
} else {
// Old output implementation
console.log(jsyaml.dump(release.composition));
}
}
async showReleaseInfo(
commitOrId: string | number,
balena: BalenaSdk.BalenaSDK,
) {
async showReleaseInfo(commitOrId: string | number, options: FlagsDef) {
const fields: Array<keyof BalenaSdk.Release> = [
'id',
'commit',
@ -103,7 +106,7 @@ export default class ReleaseCmd extends Command {
'end_timestamp',
];
const release = await balena.models.release.get(commitOrId, {
const release = await getBalenaSdk().models.release.get(commitOrId, {
$select: fields,
$expand: {
release_tag: {
@ -116,13 +119,28 @@ export default class ReleaseCmd extends Command {
.release_tag!.map((t) => `${t.tag_key}=${t.value}`)
.join('\n');
const _ = await import('lodash');
const values = _.mapValues(
release,
(val) => val ?? 'N/a',
) as Dictionary<string>;
values['tags'] = tagStr;
if (isV14()) {
await this.outputData(
{
tags: tagStr,
...release,
},
fields,
{
displayNullValuesAs: 'N/a',
...options,
},
);
} else {
// Old output implementation
const _ = await import('lodash');
const values = _.mapValues(
release,
(val) => val ?? 'N/a',
) as Dictionary<string>;
values['tags'] = tagStr;
console.log(getVisuals().table.vertical(values, [...fields, 'tags']));
console.log(getVisuals().table.vertical(values, [...fields, 'tags']));
}
}
}

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
* Copyright 2016 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -21,8 +21,11 @@ import * as cf from '../utils/common-flags';
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
import { applicationNameNote } from '../utils/messages';
import type * as BalenaSdk from 'balena-sdk';
import type { DataSetOutputOptions } from '../framework';
interface FlagsDef {
import { isV14 } from '../utils/version';
interface FlagsDef extends DataSetOutputOptions {
help: void;
}
@ -43,6 +46,7 @@ export default class ReleasesCmd extends Command {
public static usage = 'releases <fleet>';
public static flags: flags.Input<FlagsDef> = {
...(isV14() ? cf.dataOutputFlags : {}),
help: cf.help,
};
@ -57,7 +61,9 @@ export default class ReleasesCmd extends Command {
public static authenticated = true;
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(ReleasesCmd);
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
ReleasesCmd,
);
const fields: Array<keyof BalenaSdk.Release> = [
'id',
@ -76,12 +82,20 @@ export default class ReleasesCmd extends Command {
{ $select: fields },
);
const _ = await import('lodash');
console.log(
getVisuals().table.horizontal(
releases.map((rel) => _.mapValues(rel, (val) => val ?? 'N/a')),
fields,
),
);
if (isV14()) {
await this.outputData(releases, fields, {
displayNullValuesAs: 'N/a',
...options,
});
} else {
// Old output implementation
const _ = await import('lodash');
console.log(
getVisuals().table.horizontal(
releases.map((rel) => _.mapValues(rel, (val) => val ?? 'N/a')),
fields,
),
);
}
}
}

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
* Copyright 2016 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -19,8 +19,11 @@ import { flags } from '@oclif/command';
import Command from '../command';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../utils/lazy';
import type { DataOutputOptions } from '../framework';
interface FlagsDef {
import { isV14 } from '../utils/version';
interface FlagsDef extends DataOutputOptions {
help: void;
}
@ -35,15 +38,27 @@ export default class SettingsCmd extends Command {
public static usage = 'settings';
public static flags: flags.Input<FlagsDef> = {
...(isV14() ? cf.dataOutputFlags : {}),
help: cf.help,
};
public async run() {
this.parse<FlagsDef, {}>(SettingsCmd);
const { flags: options } = this.parse<FlagsDef, {}>(SettingsCmd);
const settings = await getBalenaSdk().settings.getAll();
const prettyjson = await import('prettyjson');
console.log(prettyjson.render(settings));
if (isV14()) {
// Select all available fields for display
const fields = Object.keys(settings);
await this.outputData(settings, fields, {
noCapitalizeKeys: true,
...options,
});
} else {
// Old output implementation
const prettyjson = await import('prettyjson');
console.log(prettyjson.render(settings));
}
}
}

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
* Copyright 2016 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -21,8 +21,12 @@ import { ExpectedError } from '../errors';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
import { applicationIdInfo } from '../utils/messages';
import type { ApplicationTag, DeviceTag, ReleaseTag } from 'balena-sdk';
import type { DataSetOutputOptions } from '../framework';
interface FlagsDef {
import { isV14 } from '../utils/version';
interface FlagsDef extends DataSetOutputOptions {
fleet?: string;
device?: string;
release?: string;
@ -61,6 +65,7 @@ export default class TagsCmd extends Command {
...cf.release,
exclusive: ['fleet', 'device'],
},
...(isV14() ? cf.dataSetOutputFlags : {}),
help: cf.help,
};
@ -78,7 +83,7 @@ export default class TagsCmd extends Command {
const { tryAsInteger } = await import('../utils/validation');
let tags;
let tags: ApplicationTag[] | DeviceTag[] | ReleaseTag[] = [];
if (options.fleet) {
const { getFleetSlug } = await import('../utils/sdk');
@ -103,11 +108,17 @@ export default class TagsCmd extends Command {
tags = await balena.models.release.tags.getAllByRelease(releaseParam);
}
if (!tags || tags.length === 0) {
if (tags.length === 0 && !options.json) {
// TODO: Later change to output message
throw new ExpectedError('No tags found');
}
console.log(getVisuals().table.horizontal(tags, ['tag_key', 'value']));
if (isV14()) {
await this.outputData(tags, ['tag_key', 'value'], options);
} else {
// Old output implementation
console.log(getVisuals().table.horizontal(tags, ['tag_key', 'value']));
}
}
protected missingResourceMessage = stripIndent`

View File

@ -27,14 +27,7 @@ import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
// `@types/node` does not know about `options: { bigint?: boolean }`
type statT = (
fPath: string,
options: { bigint?: boolean },
) => fs.Stats | Promise<fs.Stats>;
// async stat does not work with pkg's internal `/snapshot` filesystem
const stat: statT = process.pkg ? fs.statSync : fs.promises.stat;
const stat = process.pkg ? fs.statSync : fs.promises.stat;
let fastBootStarted = false;

View File

@ -1,26 +1,35 @@
/*
Copyright 2020 Balena
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.
*/
/**
* @license
* Copyright 2020 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 { getCliUx, getChalk } from '../utils/lazy';
/**
* Used to extend FlagsDef for commands that output single-record data.
* Exposed to user in command options.
*/
export interface DataOutputOptions {
fields?: string;
json?: boolean;
}
/**
* Used to extend FlagsDef for commands that output multi-record data.
* Exposed to user in command options.
*/
export interface DataSetOutputOptions extends DataOutputOptions {
filter?: string;
'no-header'?: boolean;
@ -28,6 +37,14 @@ export interface DataSetOutputOptions extends DataOutputOptions {
sort?: string;
}
// Not exposed to user
export interface InternalOutputOptions {
displayNullValuesAs?: string;
hideNullOrUndefinedValues?: boolean;
titleField?: string;
noCapitalizeKeys?: boolean;
}
/**
* Output message to STDERR
*/
@ -49,7 +66,7 @@ export function outputMessage(msg: string) {
export async function outputData(
data: any[] | {},
fields: string[],
options: DataOutputOptions | DataSetOutputOptions,
options: (DataOutputOptions | DataSetOutputOptions) & InternalOutputOptions,
) {
if (Array.isArray(data)) {
await outputDataSet(data, fields, options as DataSetOutputOptions);
@ -68,7 +85,7 @@ export async function outputData(
async function outputDataSet(
data: any[],
fields: string[],
options: DataSetOutputOptions,
options: DataSetOutputOptions & InternalOutputOptions,
) {
// Oclif expects fields to be specified in the format used in table headers (though lowercase)
// By replacing underscores with spaces here, we can support both header format and actual field name
@ -77,6 +94,12 @@ async function outputDataSet(
options.filter = options.filter?.replace(/_/g, ' ');
options.sort = options.sort?.replace(/_/g, ' ');
if (!options.json) {
data = data.map((d) => {
return processNullValues(d, options);
});
}
getCliUx().table(
data,
// Convert fields array to column object keys
@ -97,7 +120,7 @@ async function outputDataSet(
}
/**
* Outputs a single data object (like `resin-cli-visuals table.vertical`),
* Outputs a single data object (similar to `resin-cli-visuals table.vertical`),
* but supporting a subset of options from `cli-ux table` (--json and --fields)
*
* @param data Array of data objects to output
@ -107,9 +130,9 @@ async function outputDataSet(
async function outputDataItem(
data: any,
fields: string[],
options: DataOutputOptions,
options: DataOutputOptions & InternalOutputOptions,
) {
const outData: typeof data = {};
let outData: typeof data = {};
// Convert comma separated list of fields in `options.fields` to array of correct format.
// Note, user may have specified the true field name (e.g. `some_field`),
@ -125,30 +148,83 @@ async function outputDataItem(
}
});
if (
(options.displayNullValuesAs || options.hideNullOrUndefinedValues) &&
!options.json
) {
outData = processNullValues(outData, options);
}
if (options.json) {
printLine(JSON.stringify(outData, undefined, 2));
} else {
const chalk = getChalk();
const { capitalize } = await import('lodash');
// Find longest key, so we can align results
const longestKeyLength = getLongestObjectKeyLength(outData);
if (options.titleField) {
printTitle(data[options.titleField as keyof any[]], options);
}
// Output one field per line
for (const [k, v] of Object.entries(outData)) {
for (let [k, v] of Object.entries(outData)) {
const shim = ' '.repeat(longestKeyLength - k.length);
const kDisplay = capitalize(k.replace(/_/g, ' '));
printLine(`${chalk.bold(kDisplay) + shim} : ${v}`);
let kDisplay = k.replace(/_/g, ' ');
// Start multiline values on the line below the field name
if (typeof v === 'string' && v.includes('\n')) {
v = `\n${v}`;
}
if (!options.noCapitalizeKeys) {
kDisplay = capitalize(kDisplay);
}
if (k !== options.titleField) {
printLine(` ${bold(kDisplay) + shim} : ${v}`);
}
}
}
}
function getLongestObjectKeyLength(o: any): number {
return Object.keys(o).length >= 1
? Object.keys(o).reduce((a, b) => {
return a.length > b.length ? a : b;
}).length
: 0;
/**
* Amend null/undefined values in data as per options:
* - options.displayNullValuesAs will replace the value with the specified string
* - options.hideNullOrUndefinedValues will remove the property from the data
*
* @param data The data object to process
* @param options Output options
*
* @returns a copy of the data with amended values.
*/
function processNullValues(data: any, options: InternalOutputOptions) {
const dataCopy = { ...data };
Object.entries(dataCopy).forEach(([k, v]) => {
if (v == null) {
if (options.displayNullValuesAs) {
dataCopy[k] = options.displayNullValuesAs;
} else if (options.hideNullOrUndefinedValues) {
delete dataCopy[k];
}
}
});
return dataCopy;
}
/**
* Print a title with underscore
*
* @param title The title string to print
* @param options Output options
*/
export function printTitle(
title: string,
options?: InternalOutputOptions & DataSetOutputOptions,
) {
if (!options?.['no-header']) {
printLine(` ${capitalize(bold(title))}`);
printLine(` ${bold('─'.repeat(title.length))}`);
}
}
function printLine(s: any) {
@ -156,3 +232,15 @@ function printLine(s: any) {
// but using this one explicitly for ease of testing
process.stdout.write(s + '\n');
}
function capitalize(s: string) {
return `${s[0].toUpperCase()}${s.slice(1)}`;
}
function bold(s: string) {
return getChalk().bold(s);
}
function getLongestObjectKeyLength(o: any): number {
return Math.max(0, ...Object.keys(o).map((k) => k.length));
}

View File

@ -53,6 +53,12 @@ export async function preparseArgs(argv: string[]): Promise<string[]> {
if (extractBooleanFlag(cmdSlice, '--debug')) {
process.env.DEBUG = '1';
}
// support global --v-next flag
if (extractBooleanFlag(cmdSlice, '--v-next')) {
const { version } = await import('../package.json');
const { inc } = await import('semver');
process.env.BALENA_CLI_VERSION_OVERRIDE = inc(version, 'major') || '';
}
unsupportedFlag = extractBooleanFlag(cmdSlice, '--unsupported');
}

View File

@ -207,35 +207,6 @@ export async function selectOrganization(organizations?: Organization[]) {
});
}
export async function awaitDevice(uuid: string) {
const balena = getBalenaSdk();
const deviceName = await balena.models.device.getName(uuid);
const visuals = getVisuals();
const spinner = new visuals.Spinner(
`Waiting for ${deviceName} to come online`,
);
const poll = async (): Promise<void> => {
const isOnline = await balena.models.device.isOnline(uuid);
if (isOnline) {
spinner.stop();
console.info(`The device **${deviceName}** is online!`);
return;
} else {
// Spinner implementation is smart enough to
// not start again if it was already started
spinner.start();
await delay(3000);
await poll();
}
};
console.info(`Waiting for ${deviceName} to connect to balena...`);
await poll();
return uuid;
}
export async function awaitDeviceOsUpdate(
uuid: string,
targetOsVersion: string,

View File

@ -212,7 +212,7 @@ function handleBuilderMetadata(obj: BuilderMessage, build: RemoteBuild) {
}
const value = match[1];
const amount = match[2] || 1;
const amount = Number(match[2]) || 1;
switch (value) {
case 'erase':

View File

@ -77,7 +77,7 @@ export async function exec(
.on('error', reject)
.on('close', resolve);
if (stdout) {
if (stdout && ps.stdout) {
ps.stdout.pipe(stdout);
}
});

View File

@ -98,7 +98,7 @@ async function spawnAndPipe(
resolve();
}
});
if (stderr) {
if (stderr && ps.stderr) {
ps.stderr.pipe(stderr);
}
});

365
npm-shrinkwrap.json generated
View File

@ -1,6 +1,6 @@
{
"name": "balena-cli",
"version": "13.1.8",
"version": "13.1.13",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -1321,14 +1321,6 @@
"tslint-no-unused-expression-chai": "^0.1.4",
"typescript": "^4.2.4",
"yargs": "^16.2.0"
},
"dependencies": {
"@types/node": {
"version": "12.20.37",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.37.tgz",
"integrity": "sha512-i1KGxqcvJaLQali+WuypQnXwcplhtNtjs66eNsZpp2P2FL/trJJxx/VWsM0YCL2iMoIJrbXje48lvIQAQ4p2ZA==",
"dev": true
}
}
},
"@balena/node-beaglebone-usbboot": {
@ -1567,6 +1559,202 @@
"tslib": "^2.0.0"
}
},
"@oclif/core": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/@oclif/core/-/core-1.0.10.tgz",
"integrity": "sha512-L+IcNU3NoYxwz5hmHfcUlOJ3dpgHRsIj1kAmI9CKEJHq5gBVKlP44Ot179Jke1jKRKX2g9N42izbmlh0SNpkkw==",
"requires": {
"@oclif/linewrap": "^1.0.0",
"chalk": "^4.1.2",
"clean-stack": "^3.0.1",
"cli-ux": "6.0.5",
"debug": "^4.3.3",
"fs-extra": "^9.1.0",
"get-package-type": "^0.1.0",
"globby": "^11.0.4",
"indent-string": "^4.0.0",
"is-wsl": "^2.2.0",
"lodash": "^4.17.21",
"semver": "^7.3.5",
"string-width": "^4.2.3",
"strip-ansi": "^6.0.1",
"tslib": "^2.3.1",
"widest-line": "^3.1.0",
"wrap-ansi": "^7.0.0"
},
"dependencies": {
"ansi-escapes": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
"integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
"requires": {
"type-fest": "^0.21.3"
}
},
"ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
},
"chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"clean-stack": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-3.0.1.tgz",
"integrity": "sha512-lR9wNiMRcVQjSB3a7xXGLuz4cr4wJuuXlaAEbRutGowQTmlp7R72/DOgN21e8jdwblMWl9UOJMJXarX94pzKdg==",
"requires": {
"escape-string-regexp": "4.0.0"
}
},
"cli-progress": {
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.9.1.tgz",
"integrity": "sha512-AXxiCe2a0Lm0VN+9L0jzmfQSkcZm5EYspfqXKaSIQKqIk+0hnkZ3/v1E9B39mkD6vYhKih3c/RPsJBSwq9O99Q==",
"requires": {
"colors": "^1.1.2",
"string-width": "^4.2.0"
}
},
"cli-ux": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/cli-ux/-/cli-ux-6.0.5.tgz",
"integrity": "sha512-q2pvzDiXMNISMqCBh0P2dkofQ/8OiWlEAjl6MDNk5oUZ6p54Fnk1rOaXxohYm+YkLX5YNUonGOrwkvuiwVreIg==",
"requires": {
"@oclif/core": "^1.0.8",
"@oclif/linewrap": "^1.0.0",
"@oclif/screen": "^1.0.4 ",
"ansi-escapes": "^4.3.0",
"ansi-styles": "^4.2.0",
"cardinal": "^2.1.1",
"chalk": "^4.1.0",
"clean-stack": "^3.0.0",
"cli-progress": "^3.9.1",
"extract-stack": "^2.0.0",
"fs-extra": "^8.1",
"hyperlinker": "^1.0.0",
"indent-string": "^4.0.0",
"is-wsl": "^2.2.0",
"js-yaml": "^3.13.1",
"lodash": "^4.17.21",
"natural-orderby": "^2.0.1",
"object-treeify": "^1.1.4",
"password-prompt": "^1.1.2",
"semver": "^7.3.2",
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"supports-color": "^8.1.0",
"supports-hyperlinks": "^2.1.0",
"tslib": "^2.0.0"
},
"dependencies": {
"fs-extra": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
"requires": {
"graceful-fs": "^4.2.0",
"jsonfile": "^4.0.0",
"universalify": "^0.1.0"
}
},
"supports-color": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
"requires": {
"has-flag": "^4.0.0"
}
}
}
},
"debug": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
"integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
"requires": {
"ms": "2.1.2"
}
},
"escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="
},
"globby": {
"version": "11.0.4",
"resolved": "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz",
"integrity": "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==",
"requires": {
"array-union": "^2.1.0",
"dir-glob": "^3.0.1",
"fast-glob": "^3.1.1",
"ignore": "^5.1.4",
"merge2": "^1.3.0",
"slash": "^3.0.0"
}
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
},
"js-yaml": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
"requires": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
}
},
"jsonfile": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
"integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
"requires": {
"graceful-fs": "^4.1.6"
}
},
"strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"requires": {
"ansi-regex": "^5.0.1"
}
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"requires": {
"has-flag": "^4.0.0"
}
},
"tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
},
"type-fest": {
"version": "0.21.3",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
"integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="
},
"universalify": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="
}
}
},
"@oclif/errors": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@oclif/errors/-/errors-1.3.3.tgz",
@ -2840,9 +3028,9 @@
}
},
"@types/node": {
"version": "10.17.60",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz",
"integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw=="
"version": "12.20.42",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.42.tgz",
"integrity": "sha512-aI3/oo5DzyiI5R/xAhxxRzfZlWlsbbqdgxfTPkqu/Zt+23GXiJvMCyPJT4+xKSXOnLqoL8jJYMLTwvK2M3a5hw=="
},
"@types/node-cleanup": {
"version": "2.1.2",
@ -3773,6 +3961,11 @@
"rimraf": "^3.0.2"
},
"dependencies": {
"@types/node": {
"version": "10.17.60",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz",
"integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw=="
},
"balena-sdk": {
"version": "15.59.2",
"resolved": "https://registry.npmjs.org/balena-sdk/-/balena-sdk-15.59.2.tgz",
@ -3818,12 +4011,12 @@
}
},
"balena-preload": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/balena-preload/-/balena-preload-11.0.0.tgz",
"integrity": "sha512-2LuPTw6LoVxuasGhn+cLyA5n7kjvwBtQmBsg08KNhcbEpWLkzrV5jhpNxlMPUuoC2xcMv5Rcg6FWbY87QV5zYQ==",
"version": "12.0.0",
"resolved": "https://registry.npmjs.org/balena-preload/-/balena-preload-12.0.0.tgz",
"integrity": "sha512-BD4ayIqqopJB0KFFjjlz0rIpcbbHojG8El8qOBLJHvidatgtgVs5xFWBoF5B7fgdJdjRsclA/AbUMZwovN7t3w==",
"requires": {
"archiver": "^3.1.1",
"balena-sdk": "^15.44.0",
"balena-sdk": "^16.0.0",
"bluebird": "^3.7.2",
"compare-versions": "^3.6.0",
"docker-progress": "^5.0.0",
@ -3854,32 +4047,6 @@
"zip-stream": "^2.1.2"
}
},
"balena-sdk": {
"version": "15.59.2",
"resolved": "https://registry.npmjs.org/balena-sdk/-/balena-sdk-15.59.2.tgz",
"integrity": "sha512-pMNwqqnqtets6PoYxRQhOHBnk7Say4gNhInAvS69UlEhraCR0Xau8rJk9G2IthWDA4tpR2In3u4SQJMIHYj84w==",
"requires": {
"@balena/es-version": "^1.0.0",
"@types/lodash": "^4.14.168",
"@types/memoizee": "^0.4.5",
"@types/node": "^10.17.55",
"abortcontroller-polyfill": "^1.7.1",
"balena-auth": "^4.1.0",
"balena-errors": "^4.7.1",
"balena-hup-action-utils": "~4.0.2",
"balena-pine": "^12.4.0",
"balena-register-device": "^7.1.0",
"balena-request": "^11.5.0",
"balena-semver": "^2.3.0",
"balena-settings-client": "^4.0.6",
"lodash": "^4.17.21",
"memoizee": "^0.4.15",
"moment": "^2.29.1",
"ndjson": "^2.0.0",
"semver": "^7.3.4",
"tslib": "^2.1.0"
}
},
"compress-commons": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-2.1.1.tgz",
@ -3951,11 +4118,6 @@
}
}
},
"tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
},
"zip-stream": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-2.1.3.tgz",
@ -4047,6 +4209,11 @@
"tslib": "^2.1.0"
},
"dependencies": {
"@types/node": {
"version": "10.17.60",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz",
"integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw=="
},
"balena-hup-action-utils": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/balena-hup-action-utils/-/balena-hup-action-utils-4.1.0.tgz",
@ -4113,6 +4280,13 @@
"@types/node": "^10.17.26",
"balena-errors": "^4.7.1",
"tslib": "^2.0.0"
},
"dependencies": {
"@types/node": {
"version": "10.17.60",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz",
"integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw=="
}
}
},
"balena-sync": {
@ -4141,6 +4315,11 @@
"typed-error": "^2.0.0"
},
"dependencies": {
"@types/node": {
"version": "10.17.60",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz",
"integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw=="
},
"balena-sdk": {
"version": "15.59.2",
"resolved": "https://registry.npmjs.org/balena-sdk/-/balena-sdk-15.59.2.tgz",
@ -4290,9 +4469,9 @@
"dev": true
},
"big-integer": {
"version": "1.6.50",
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.50.tgz",
"integrity": "sha512-+O2uoQWFRo8ysZNo/rjtri2jIwjr3XfeAgRjAUADRqGG+ZITvyn8J1kvXLTaKVr3hhGXk+f23tKfdzmklVM9vQ=="
"version": "1.6.51",
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz",
"integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg=="
},
"binary": {
"version": "0.3.0",
@ -4934,27 +5113,26 @@
}
},
"cli-ux": {
"version": "5.6.3",
"resolved": "https://registry.npmjs.org/cli-ux/-/cli-ux-5.6.3.tgz",
"integrity": "sha512-/oDU4v8BiDjX2OKcSunGH0iGDiEtj2rZaGyqNuv9IT4CgcSMyVWAMfn0+rEHaOc4n9ka78B0wo1+N1QX89f7mw==",
"version": "6.0.6",
"resolved": "https://registry.npmjs.org/cli-ux/-/cli-ux-6.0.6.tgz",
"integrity": "sha512-CvL4qmV78VhnbyHTswGjpDSQtU+oj3hT9DP9L6yMOwiTiNv0nMjMEV/8zou4CSqO6PtZ2A8qnlZDgAc07Js+aw==",
"requires": {
"@oclif/command": "^1.6.0",
"@oclif/errors": "^1.2.1",
"@oclif/core": "1.0.10",
"@oclif/linewrap": "^1.0.0",
"@oclif/screen": "^1.0.3",
"@oclif/screen": "^1.0.4 ",
"ansi-escapes": "^4.3.0",
"ansi-styles": "^4.2.0",
"cardinal": "^2.1.1",
"chalk": "^4.1.0",
"clean-stack": "^3.0.0",
"cli-progress": "^3.4.0",
"cli-progress": "^3.9.1",
"extract-stack": "^2.0.0",
"fs-extra": "^8.1",
"hyperlinker": "^1.0.0",
"indent-string": "^4.0.0",
"is-wsl": "^2.2.0",
"js-yaml": "^3.13.1",
"lodash": "^4.17.11",
"lodash": "^4.17.21",
"natural-orderby": "^2.0.1",
"object-treeify": "^1.1.4",
"password-prompt": "^1.1.2",
@ -4975,9 +5153,9 @@
}
},
"chalk": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
"integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==",
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
@ -4993,6 +5171,15 @@
}
}
},
"cli-progress": {
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.9.1.tgz",
"integrity": "sha512-AXxiCe2a0Lm0VN+9L0jzmfQSkcZm5EYspfqXKaSIQKqIk+0hnkZ3/v1E9B39mkD6vYhKih3c/RPsJBSwq9O99Q==",
"requires": {
"colors": "^1.1.2",
"string-width": "^4.2.0"
}
},
"fs-extra": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
@ -8134,6 +8321,11 @@
"has-symbols": "^1.0.1"
}
},
"get-package-type": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz",
"integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q=="
},
"get-port": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz",
@ -13522,11 +13714,11 @@
"integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg=="
},
"prettyjson": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prettyjson/-/prettyjson-1.2.1.tgz",
"integrity": "sha1-/P+rQdGcq0365eV15kJGYZsS0ok=",
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/prettyjson/-/prettyjson-1.2.5.tgz",
"integrity": "sha512-rksPWtoZb2ZpT5OVgtmy0KHVM+Dca3iVwWY9ifwhcexfjebtgjg3wmrUt9PvJ59XIYBcknQeYHD8IAnVlh9lAw==",
"requires": {
"colors": "^1.1.2",
"colors": "1.4.0",
"minimist": "^1.2.0"
}
},
@ -14736,6 +14928,11 @@
"@types/node": "*"
}
},
"@types/node": {
"version": "10.17.60",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz",
"integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw=="
},
"bl": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz",
@ -14931,11 +15128,35 @@
}
},
"resin-doodles": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/resin-doodles/-/resin-doodles-0.1.1.tgz",
"integrity": "sha1-0AmnndrHEhtFDHkUw2Jip4akBe4=",
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/resin-doodles/-/resin-doodles-0.2.0.tgz",
"integrity": "sha512-k6qfrsXoQC9Xj/iRfceMYeFJKvY94H0AzTmrHvneUgAuWQD6nBUDSRLPEqWFOQlEW/mGUYMFSsRYtYDAsUR3ow==",
"requires": {
"colors": "^1.1.2"
"chalk": "^4.1.2"
},
"dependencies": {
"chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"requires": {
"has-flag": "^4.0.0"
}
}
}
},
"resin-multibuild": {
@ -16858,9 +17079,9 @@
}
},
"tmp-promise": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.2.tgz",
"integrity": "sha512-OyCLAKU1HzBjL6Ev3gxUeraJNlbNingmi8IrHHEsYH8LTmEuhvYfqvhn2F/je+mjf4N58UmZ96OMEy1JanSCpA==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz",
"integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==",
"requires": {
"tmp": "^0.2.0"
}

View File

@ -1,6 +1,6 @@
{
"name": "balena-cli",
"version": "13.1.8",
"version": "13.1.13",
"description": "The official balena Command Line Interface",
"main": "./build/app.js",
"homepage": "https://github.com/balena-io/balena-cli",
@ -90,7 +90,7 @@
"author": "Balena Inc. (https://balena.io/)",
"license": "Apache-2.0",
"engines": {
"node": ">=12.0.0 <13.0.0",
"node": ">=12.8.0 <13.0.0",
"npm": "<7.0.0"
},
"husky": {
@ -147,7 +147,7 @@
"@types/ndjson": "^2.0.1",
"@types/net-keepalive": "^0.4.1",
"@types/nock": "^11.1.0",
"@types/node": "^10.17.60",
"@types/node": "^12.20.42",
"@types/node-cleanup": "^2.1.2",
"@types/parse-link-header": "^1.0.1",
"@types/prettyjson": "^0.0.30",
@ -206,7 +206,7 @@
"balena-errors": "^4.7.1",
"balena-image-fs": "^7.0.6",
"balena-image-manager": "^7.1.1",
"balena-preload": "^11.0.0",
"balena-preload": "^12.0.0",
"balena-release": "^3.2.0",
"balena-sdk": "^16.9.0",
"balena-semver": "^2.3.0",
@ -218,8 +218,8 @@
"chalk": "^3.0.0",
"chokidar": "^3.5.2",
"cli-truncate": "^2.1.0",
"cli-ux": "^5.5.1",
"color-hash": "^1.1.1",
"cli-ux": "^6.0.5",
"columnify": "^1.5.2",
"common-tags": "^1.7.2",
"denymount": "^2.3.0",
@ -257,14 +257,14 @@
"oclif": "^1.18.4",
"open": "^7.1.0",
"patch-package": "^6.4.7",
"prettyjson": "^1.1.3",
"prettyjson": "^1.2.5",
"progress-stream": "^2.0.0",
"reconfix": "^1.0.0-v0-1-0-fork-46760acff4d165f5238bfac5e464256ef1944476",
"request": "^2.88.2",
"resin-cli-form": "^2.0.2",
"resin-cli-visuals": "^1.8.0",
"resin-compose-parse": "^2.1.3",
"resin-doodles": "^0.1.1",
"resin-doodles": "^0.2.0",
"resin-multibuild": "^4.12.2",
"resin-stream-logger": "^0.1.2",
"rimraf": "^3.0.2",
@ -287,6 +287,6 @@
"windosu": "^0.3.0"
},
"versionist": {
"publishedAt": "2022-01-11T00:59:23.183Z"
"publishedAt": "2022-02-10T11:50:34.458Z"
}
}

View File

@ -21,6 +21,8 @@ import * as path from 'path';
import { apiResponsePath, BalenaAPIMock } from '../../nock/balena-api-mock';
import { cleanOutput, runCommand } from '../../helpers';
import { isV14 } from '../../../lib/utils/version';
describe('balena device', function () {
let api: BalenaAPIMock;
@ -57,9 +59,16 @@ describe('balena device', function () {
const lines = cleanOutput(out);
expect(lines).to.have.lengthOf(25);
expect(lines[0]).to.equal('== SPARKLING WOOD');
expect(lines[6].split(':')[1].trim()).to.equal('org/test app');
if (isV14()) {
expect(lines).to.have.lengthOf(26);
expect(lines[0]).to.equal('sparkling-wood');
expect(lines[2].split(':')[0].trim()).to.equal('Id');
expect(lines[2].split(':')[1].trim()).to.equal('1747415');
} else {
expect(lines).to.have.lengthOf(25);
expect(lines[0]).to.equal('== SPARKLING WOOD');
expect(lines[6].split(':')[1].trim()).to.equal('org/test app');
}
});
it.skip('correctly handles devices with missing fields', async () => {
@ -79,14 +88,20 @@ describe('balena device', function () {
const lines = cleanOutput(out);
expect(lines).to.have.lengthOf(14);
expect(lines[0]).to.equal('== SPARKLING WOOD');
expect(lines[6].split(':')[1].trim()).to.equal('org/test app');
if (isV14()) {
expect(lines).to.have.lengthOf(15);
expect(lines[0]).to.equal('sparkling-wood');
expect(lines[7].split(':')[1].trim()).to.equal('org/test app');
} else {
expect(lines).to.have.lengthOf(14);
expect(lines[0]).to.equal('== SPARKLING WOOD');
expect(lines[6].split(':')[1].trim()).to.equal('org/test app');
}
});
it('correctly handles devices with missing application', async () => {
// Devices with missing applications will have application name set to `N/a`.
// e.g. When user has a device associated with app that user is no longer a collaborator of.
it.skip('correctly handles devices with missing fleet', async () => {
// Devices with missing fleets will have fleet name set to `N/a`.
// e.g. When user has a device associated with fleet that user is no longer a collaborator of.
api.scope
.get(
/^\/v6\/device\?.+&\$expand=belongs_to__application\(\$select=app_name,slug\)/,
@ -103,8 +118,15 @@ describe('balena device', function () {
const lines = cleanOutput(out);
expect(lines).to.have.lengthOf(25);
expect(lines[0]).to.equal('== SPARKLING WOOD');
expect(lines[6].split(':')[1].trim()).to.equal('N/a');
if (isV14()) {
expect(lines).to.have.lengthOf(26);
expect(lines[0]).to.equal('sparkling-wood');
expect(lines[9].split(':')[0].trim()).to.equal('Fleet');
expect(lines[9].split(':')[1].trim()).to.equal('N/a');
} else {
expect(lines).to.have.lengthOf(25);
expect(lines[0]).to.equal('== SPARKLING WOOD');
expect(lines[6].split(':')[1].trim()).to.equal('N/a');
}
});
});

View File

@ -21,6 +21,8 @@ import * as path from 'path';
import { apiResponsePath, BalenaAPIMock } from '../../nock/balena-api-mock';
import { cleanOutput, runCommand } from '../../helpers';
import { isV14 } from '../../../lib/utils/version';
describe('balena devices', function () {
let api: BalenaAPIMock;
@ -48,15 +50,24 @@ describe('balena devices', function () {
const lines = cleanOutput(out);
expect(lines[0].replace(/ +/g, ' ')).to.equal(
'ID UUID DEVICE NAME DEVICE TYPE FLEET STATUS IS ONLINE SUPERVISOR VERSION OS VERSION DASHBOARD URL',
);
expect(lines).to.have.lengthOf.at.least(2);
expect(lines.some((l) => l.includes('org/test app'))).to.be.true;
// Devices with missing applications will have application name set to `N/a`.
// e.g. When user has a device associated with app that user is no longer a collaborator of.
expect(lines.some((l) => l.includes('N/a'))).to.be.true;
if (isV14()) {
expect(lines[0].replace(/ +/g, ' ')).to.equal(
' Id Uuid Device name Device type Fleet Status Is online Supervisor version Os version Dashboard url ',
);
expect(lines).to.have.lengthOf.at.least(3);
expect(lines.some((l) => l.includes('org/test app'))).to.be.true;
// Devices with missing applications will have application name set to `N/a`.
// e.g. When user has a device associated with app that user is no longer a collaborator of.
expect(lines.some((l) => l.includes('N/a'))).to.be.true;
} else {
expect(lines[0].replace(/ +/g, ' ')).to.equal(
'ID UUID DEVICE NAME DEVICE TYPE FLEET STATUS IS ONLINE SUPERVISOR VERSION OS VERSION DASHBOARD URL',
);
expect(lines).to.have.lengthOf.at.least(2);
expect(lines.some((l) => l.includes('org/test app'))).to.be.true;
// Devices with missing applications will have application name set to `N/a`.
// e.g. When user has a device associated with app that user is no longer a collaborator of.
expect(lines.some((l) => l.includes('N/a'))).to.be.true;
}
});
});

View File

@ -1,6 +1,6 @@
/**
* @license
* Copyright 2019-2021 Balena Ltd.
* 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.
@ -19,6 +19,7 @@ import { expect } from 'chai';
import { BalenaAPIMock } from '../../nock/balena-api-mock';
import { cleanOutput, runCommand } from '../../helpers';
import { isV14 } from '../../../lib/utils/version';
describe('balena devices supported', function () {
let api: BalenaAPIMock;
@ -50,7 +51,10 @@ describe('balena devices supported', function () {
const lines = cleanOutput(out, true);
expect(lines[0]).to.equal('SLUG ALIASES ARCH NAME');
expect(lines[0]).to.equal(
isV14() ? ' Slug Aliases Arch Name ' : 'SLUG ALIASES ARCH NAME',
);
expect(lines).to.have.lengthOf.at.least(2);
expect(lines).to.contain('intel-nuc nuc amd64 Intel NUC');
expect(lines).to.contain(

View File

@ -19,7 +19,9 @@ import { expect } from 'chai';
import { stripIndent } from '../../../build/utils/lazy';
import { BalenaAPIMock } from '../../nock/balena-api-mock';
import { runCommand } from '../../helpers';
import { runCommand, removeFirstNLines, trimLines } from '../../helpers';
import { isV14 } from '../../../lib/utils/version';
describe('balena envs', function () {
const appName = 'test';
@ -48,15 +50,30 @@ describe('balena envs', function () {
const { out, err } = await runCommand(`envs -f ${appName}`);
expect(out.join('')).to.equal(
stripIndent`
if (isV14()) {
let output = out.join('');
output = trimLines(removeFirstNLines(output, 2));
const expected =
stripIndent`
120110 svar1 svar1-value gh_user/testApp service1
120111 svar2 svar2-value gh_user/testApp service2
120101 var1 var1-val gh_user/testApp *
120102 var2 22 gh_user/testApp *
` + '\n';
expect(output).to.equal(expected);
} else {
expect(out.join('')).to.equal(
stripIndent`
ID NAME VALUE FLEET SERVICE
120110 svar1 svar1-value gh_user/testApp service1
120111 svar2 svar2-value gh_user/testApp service2
120101 var1 var1-val gh_user/testApp *
120102 var2 22 gh_user/testApp *
` + '\n',
);
);
}
expect(err.join('')).to.equal('');
});
@ -66,12 +83,24 @@ describe('balena envs', function () {
const { out, err } = await runCommand(`envs -f ${appName} --config`);
expect(out.join('')).to.equal(
stripIndent`
if (isV14()) {
let output = out.join('');
output = trimLines(removeFirstNLines(output, 2));
const expected =
stripIndent`
120300 RESIN_SUPERVISOR_NATIVE_LOGGER false gh_user/testApp
` + '\n';
expect(output).to.equal(expected);
} else {
expect(out.join('')).to.equal(
stripIndent`
ID NAME VALUE FLEET
120300 RESIN_SUPERVISOR_NATIVE_LOGGER false gh_user/testApp
` + '\n',
);
);
}
expect(err.join('')).to.equal('');
});
@ -82,15 +111,19 @@ describe('balena envs', function () {
const { out, err } = await runCommand(`envs -cjf ${appName}`);
expect(JSON.parse(out.join(''))).to.deep.equal([
{
fleet: 'gh_user/testApp',
id: 120300,
name: 'RESIN_SUPERVISOR_NATIVE_LOGGER',
value: 'false',
},
]);
expect(err.join('')).to.equal('');
if (isV14()) {
// TODO: Add tests once oclif json issue resolved.
} else {
expect(JSON.parse(out.join(''))).to.deep.equal([
{
fleet: 'gh_user/testApp',
id: 120300,
name: 'RESIN_SUPERVISOR_NATIVE_LOGGER',
value: 'false',
},
]);
expect(err.join('')).to.equal('');
}
});
it('should successfully list service variables for a test fleet (-s flag)', async () => {
@ -104,14 +137,28 @@ describe('balena envs', function () {
`envs -f ${appName} -s ${serviceName}`,
);
expect(out.join('')).to.equal(
stripIndent`
if (isV14()) {
let output = out.join('');
output = trimLines(removeFirstNLines(output, 2));
const expected =
stripIndent`
120111 svar2 svar2-value gh_user/testApp service2
120101 var1 var1-val gh_user/testApp *
120102 var2 22 gh_user/testApp *
` + '\n';
expect(output).to.equal(expected);
} else {
expect(out.join('')).to.equal(
stripIndent`
ID NAME VALUE FLEET SERVICE
120111 svar2 svar2-value gh_user/testApp service2
120101 var1 var1-val gh_user/testApp *
120102 var2 22 gh_user/testApp *
` + '\n',
);
);
}
expect(err.join('')).to.equal('');
});
@ -126,14 +173,28 @@ describe('balena envs', function () {
`envs -f ${appName} -s ${serviceName}`,
);
expect(out.join('')).to.equal(
stripIndent`
if (isV14()) {
let output = out.join('');
output = trimLines(removeFirstNLines(output, 2));
const expected =
stripIndent`
120110 svar1 svar1-value gh_user/testApp ${serviceName}
120101 var1 var1-val gh_user/testApp *
120102 var2 22 gh_user/testApp *
` + '\n';
expect(output).to.equal(expected);
} else {
expect(out.join('')).to.equal(
stripIndent`
ID NAME VALUE FLEET SERVICE
120110 svar1 svar1-value gh_user/testApp ${serviceName}
120101 var1 var1-val gh_user/testApp *
120102 var2 22 gh_user/testApp *
` + '\n',
);
);
}
expect(err.join('')).to.equal('');
});
@ -148,8 +209,27 @@ describe('balena envs', function () {
const uuid = shortUUID;
const result = await runCommand(`envs -d ${uuid}`);
let { out } = result;
let expected =
stripIndent`
if (isV14()) {
let output = out.join('');
output = trimLines(removeFirstNLines(output, 2));
const expected =
stripIndent`
120110 svar1 svar1-value org/test * service1
120111 svar2 svar2-value org/test * service2
120120 svar3 svar3-value org/test ${uuid} service1
120121 svar4 svar4-value org/test ${uuid} service2
120101 var1 var1-val org/test * *
120102 var2 22 org/test * *
120203 var3 var3-val org/test ${uuid} *
120204 var4 44 org/test ${uuid} *
` + '\n';
expect(output).to.equal(expected);
} else {
let expected =
stripIndent`
ID NAME VALUE FLEET DEVICE SERVICE
120110 svar1 svar1-value org/test * service1
120111 svar2 svar2-value org/test * service2
@ -161,10 +241,10 @@ describe('balena envs', function () {
120204 var4 44 org/test ${uuid} *
` + '\n';
out = out.map((l) => l.replace(/ +/g, ' '));
expected = expected.replace(/ +/g, ' ');
expect(out.join('')).to.equal(expected);
out = out.map((l) => l.replace(/ +/g, ' '));
expected = expected.replace(/ +/g, ' ');
expect(out.join('')).to.equal(expected);
}
});
it('should successfully list env variables for a test device (JSON output)', async () => {
@ -176,7 +256,11 @@ describe('balena envs', function () {
api.expectGetDeviceServiceVars();
const { out, err } = await runCommand(`envs -jd ${shortUUID}`);
const expected = `[
if (isV14()) {
// TODO: Add tests once oclif json issue resolved.
} else {
const expected = `[
{ "id": 120101, "fleet": "org/test", "deviceUUID": "*", "name": "var1", "value": "var1-val", "serviceName": "*" },
{ "id": 120102, "fleet": "org/test", "deviceUUID": "*", "name": "var2", "value": "22", "serviceName": "*" },
{ "id": 120110, "fleet": "org/test", "deviceUUID": "*", "name": "svar1", "value": "svar1-value", "serviceName": "service1" },
@ -187,7 +271,9 @@ describe('balena envs', function () {
{ "id": 120204, "fleet": "org/test", "deviceUUID": "${fullUUID}", "name": "var4", "value": "44", "serviceName": "*" }
]`;
expect(JSON.parse(out.join(''))).to.deep.equal(JSON.parse(expected));
expect(JSON.parse(out.join(''))).to.deep.equal(JSON.parse(expected));
}
expect(err.join('')).to.equal('');
});
@ -199,17 +285,30 @@ describe('balena envs', function () {
const result = await runCommand(`envs -d ${shortUUID} --config`);
let { out } = result;
let expected =
stripIndent`
if (isV14()) {
let output = out.join('');
output = trimLines(removeFirstNLines(output, 2));
const expected =
stripIndent`
120300 RESIN_SUPERVISOR_NATIVE_LOGGER false org/test *
120400 RESIN_SUPERVISOR_POLL_INTERVAL 900900 org/test ${shortUUID}
` + '\n';
expect(output).to.equal(expected);
} else {
let expected =
stripIndent`
ID NAME VALUE FLEET DEVICE
120300 RESIN_SUPERVISOR_NATIVE_LOGGER false org/test *
120400 RESIN_SUPERVISOR_POLL_INTERVAL 900900 org/test ${shortUUID}
` + '\n';
out = out.map((l) => l.replace(/ +/g, ' '));
expected = expected.replace(/ +/g, ' ');
out = out.map((l) => l.replace(/ +/g, ' '));
expected = expected.replace(/ +/g, ' ');
expect(out.join('')).to.equal(expected);
expect(out.join('')).to.equal(expected);
}
});
it('should successfully list service variables for a test device (-s flag)', async () => {
@ -225,8 +324,25 @@ describe('balena envs', function () {
const uuid = shortUUID;
const result = await runCommand(`envs -d ${uuid} -s ${serviceName}`);
let { out } = result;
let expected =
stripIndent`
if (isV14()) {
let output = out.join('');
output = trimLines(removeFirstNLines(output, 2));
const expected =
stripIndent`
120111 svar2 svar2-value org/test * service2
120121 svar4 svar4-value org/test ${uuid} service2
120101 var1 var1-val org/test * *
120102 var2 22 org/test * *
120203 var3 var3-val org/test ${uuid} *
120204 var4 44 org/test ${uuid} *
` + '\n';
expect(output).to.equal(expected);
} else {
let expected =
stripIndent`
ID NAME VALUE FLEET DEVICE SERVICE
120111 svar2 svar2-value org/test * service2
120121 svar4 svar4-value org/test ${uuid} service2
@ -236,10 +352,11 @@ describe('balena envs', function () {
120204 var4 44 org/test ${uuid} *
` + '\n';
out = out.map((l) => l.replace(/ +/g, ' '));
expected = expected.replace(/ +/g, ' ');
out = out.map((l) => l.replace(/ +/g, ' '));
expected = expected.replace(/ +/g, ' ');
expect(out.join('')).to.equal(expected);
expect(out.join('')).to.equal(expected);
}
});
it('should successfully list env and service variables for a test device (unknown fleet)', async () => {
@ -250,8 +367,23 @@ describe('balena envs', function () {
const uuid = shortUUID;
const result = await runCommand(`envs -d ${uuid}`);
let { out } = result;
let expected =
stripIndent`
if (isV14()) {
let output = out.join('');
output = trimLines(removeFirstNLines(output, 2));
const expected =
stripIndent`
120120 svar3 svar3-value N/A ${uuid} service1
120121 svar4 svar4-value N/A ${uuid} service2
120203 var3 var3-val N/A ${uuid} *
120204 var4 44 N/A ${uuid} *
` + '\n';
expect(output).to.equal(expected);
} else {
let expected =
stripIndent`
ID NAME VALUE FLEET DEVICE SERVICE
120120 svar3 svar3-value N/A ${uuid} service1
120121 svar4 svar4-value N/A ${uuid} service2
@ -259,10 +391,11 @@ describe('balena envs', function () {
120204 var4 44 N/A ${uuid} *
` + '\n';
out = out.map((l) => l.replace(/ +/g, ' '));
expected = expected.replace(/ +/g, ' ');
out = out.map((l) => l.replace(/ +/g, ' '));
expected = expected.replace(/ +/g, ' ');
expect(out.join('')).to.equal(expected);
expect(out.join('')).to.equal(expected);
}
});
it('should successfully list env and service vars for a test device (-s flags)', async () => {
@ -278,8 +411,24 @@ describe('balena envs', function () {
const uuid = shortUUID;
const result = await runCommand(`envs -d ${uuid} -s ${serviceName}`);
let { out } = result;
let expected =
stripIndent`
if (isV14()) {
let output = out.join('');
output = trimLines(removeFirstNLines(output, 2));
const expected =
stripIndent`
120110 svar1 svar1-value org/test * ${serviceName}
120120 svar3 svar3-value org/test ${uuid} ${serviceName}
120101 var1 var1-val org/test * *
120102 var2 22 org/test * *
120203 var3 var3-val org/test ${uuid} *
120204 var4 44 org/test ${uuid} *
` + '\n';
expect(output).to.equal(expected);
} else {
let expected =
stripIndent`
ID NAME VALUE FLEET DEVICE SERVICE
120110 svar1 svar1-value org/test * ${serviceName}
120120 svar3 svar3-value org/test ${uuid} ${serviceName}
@ -289,10 +438,11 @@ describe('balena envs', function () {
120204 var4 44 org/test ${uuid} *
` + '\n';
out = out.map((l) => l.replace(/ +/g, ' '));
expected = expected.replace(/ +/g, ' ');
out = out.map((l) => l.replace(/ +/g, ' '));
expected = expected.replace(/ +/g, ' ');
expect(out.join('')).to.equal(expected);
expect(out.join('')).to.equal(expected);
}
});
it('should successfully list env and service vars for a test device (-js flags)', async () => {
@ -308,7 +458,11 @@ describe('balena envs', function () {
const { out, err } = await runCommand(
`envs -d ${shortUUID} -js ${serviceName}`,
);
const expected = `[
if (isV14()) {
// TODO: Add tests once oclif json issue resolved.
} else {
const expected = `[
{ "id": 120101, "fleet": "org/test", "deviceUUID": "*", "name": "var1", "value": "var1-val", "serviceName": "*" },
{ "id": 120102, "fleet": "org/test", "deviceUUID": "*", "name": "var2", "value": "22", "serviceName": "*" },
{ "id": 120110, "fleet": "org/test", "deviceUUID": "*", "name": "svar1", "value": "svar1-value", "serviceName": "${serviceName}" },
@ -317,7 +471,8 @@ describe('balena envs', function () {
{ "id": 120204, "fleet": "org/test", "deviceUUID": "${fullUUID}", "name": "var4", "value": "44", "serviceName": "*" }
]`;
expect(JSON.parse(out.join(''))).to.deep.equal(JSON.parse(expected));
expect(err.join('')).to.equal('');
expect(JSON.parse(out.join(''))).to.deep.equal(JSON.parse(expected));
expect(err.join('')).to.equal('');
}
});
});

View File

@ -20,6 +20,8 @@ import { expect } from 'chai';
import { BalenaAPIMock } from '../nock/balena-api-mock';
import { cleanOutput, runCommand } from '../helpers';
import { isV14 } from '../../lib/utils/version';
describe('balena release', function () {
let api: BalenaAPIMock;
@ -34,7 +36,7 @@ describe('balena release', function () {
api.done();
});
it('should show release details', async () => {
it.skip('should show release details', async () => {
api.expectGetRelease();
const { out } = await runCommand('release 27fda508c');
const lines = cleanOutput(out);
@ -44,7 +46,7 @@ describe('balena release', function () {
expect(lines[1]).to.contain(' 90247b54de4fa7a0a3cbc85e73c68039');
});
it('should return release composition', async () => {
it.skip('should return release composition', async () => {
api.expectGetRelease();
const { out } = await runCommand('release 27fda508c --composition');
const lines = cleanOutput(out);
@ -61,8 +63,14 @@ describe('balena release', function () {
api.expectGetApplication();
const { out } = await runCommand('releases someapp');
const lines = cleanOutput(out);
expect(lines.length).to.be.equal(2);
expect(lines[1]).to.contain('142334');
expect(lines[1]).to.contain('90247b54de4fa7a0a3cbc85e73c68039');
if (isV14()) {
expect(lines.length).to.be.equal(3);
expect(lines[2]).to.contain('142334');
expect(lines[2]).to.contain('90247b54de4fa7a0a3cbc85e73c68039');
} else {
expect(lines.length).to.be.equal(2);
expect(lines[1]).to.contain('142334');
expect(lines[1]).to.contain('90247b54de4fa7a0a3cbc85e73c68039');
}
});
});

View File

@ -160,8 +160,9 @@ async function startMockSshServer(): Promise<[Server, number]> {
});
return new Promise<[Server, number]>((resolve, reject) => {
// port 0: let the OS allocation any available TCP port number
const listener = server.listen(0, '127.0.0.1', (err: Error) => {
// TODO: remove 'as any' below. According to @types/node v12.20.42, the
// callback type is `() => void`, but our code assumes `(err: Error) => void`
const listener = (server.listen as any)(0, '127.0.0.1', (err: Error) => {
// this callback is called for the 'listening' event
if (err) {
console.error(`Error starting mock ssh server:\n${err}`);

View File

@ -141,10 +141,10 @@ describe('DeprecationChecker', function () {
getStub.resolves(mockCache);
// Force isTTY to be false (undefined). It happens to be true when
// Force isTTY to be false. It happens to be true when
// the tests run on balenaCI on macOS and Linux.
const originalIsTTY = process.stderr.isTTY;
process.stderr.isTTY = undefined;
process.stderr.isTTY = false;
let result: TestOutput;
try {
result = await runCommand('version');

View File

@ -106,21 +106,6 @@ describe('outputDataSet', function () {
expect(splitHeader[1]).to.include('thing');
});
/*
it('should output fields in the order specified in `fields` param', async () => {
const fields = ['thing_color', 'id', 'name'];
const options = {};
await outputDataSet(dataSet, fields, options);
const headerLine = printLineSpy.firstCall.firstArg.toLowerCase();
// split header using the `it` column as delimiter
const splitHeader = headerLine.split('id');
expect(splitHeader[0]).to.include('thing');
expect(splitHeader[1]).to.include('name');
});
*/
it('should only output fields specified in `options.fields` if present', async () => {
const fields = ['name', 'id', 'thing_color', 'thing_shape'];
const options = {
@ -167,13 +152,41 @@ describe('outputDataSet', function () {
expect(printLineSpy.getCall(0).firstArg).to.include('red');
});
it(
'should output `null` values using the provided value, ' +
'if `options.displayNullValuesAs` is present',
async () => {
const fields = ['name', 'id', 'thing_color', 'thing_shape'];
const nullValue = 'N/a';
const options = {
'no-header': true,
displayNullValuesAs: nullValue,
};
const extendedDataSet = [
...dataSet,
{
name: 'item3',
id: 3,
thing_color: null,
thing_shape: 'round',
},
];
await outputDataSet(extendedDataSet, fields, options);
expect(printLineSpy.callCount).to.equal(3);
expect(printLineSpy.getCall(2).firstArg).to.include(nullValue);
},
);
it('should output data in json format, if `options.json` true', async () => {
const fields = ['name', 'thing_color', 'thing_shape'];
const options = {
json: true,
};
// TODO: I've run into an oclif cli-ux bug, where numbers are output as strings in json
// TODO: I've run into an oclif cli-ux bug, where all types (number. bool etc.) are output as strings in json
// (this can be seen by including 'id' in the fields list above).
// Issue opened: https://github.com/oclif/cli-ux/issues/309
// For now removing id for this test.

View File

@ -17,8 +17,8 @@
import * as _ from 'lodash';
import * as path from 'path';
import * as packageJSON from '../package.json';
import { getChalk } from '../lib/utils/lazy';
const balenaExe = process.platform === 'win32' ? 'balena.exe' : 'balena';
const standalonePath = path.resolve(__dirname, '..', 'build-bin', balenaExe);
@ -353,3 +353,65 @@ export async function switchSentry(
return sentryStatus;
}
}
/**
* Convert a string to an array of character codes
* @param text the text to convert.
* @returns an array of character codes representing the text.
*/
export function stringToCharCodes(text: string) {
return text.split('').map((c) => {
return c.charCodeAt(0);
});
}
/**
* Remove leaving and trailing whitespace from each lime of a string.
* @param text the text to process
* @returns a copy of the text with the lines trimmed.
*/
export function trimLines(text: string) {
let lines = text.split('\n');
lines = lines.map((l) => l.trim());
return lines.join('\n');
}
/**
* Pad each line with characters at beginning and end.
* @param text the text to pad.
* @param startPad the string to prepend each line with.
* @param endPad the string to append each line with.
* @returns a copy of the text with the specified padding.
*/
export function padLines(text: string, startPad: string, endPad: string = '') {
let lines = text.split('\n');
lines = lines.map((l) => {
return l === '' ? '' : `${startPad}${l}${endPad}`;
});
return lines.join('\n');
}
/**
* Format first nLines bold.
* @param text the text to format
* @param nLines number of liens to format (from top)
* @returns a copy of the text with the specified number of top lines formatted bold.
*/
export function boldFirstNLines(text: string, nLines: number) {
const chalk = getChalk();
let lines = text.split('\n');
lines = lines.map((l, i) => {
return i < nLines ? chalk.bold(l) : l;
});
return lines.join('\n');
}
/**
* Returns first nLines bold.
* @param text the text to format
* @param nLines number of liens to format (from top)
* @returns a copy of the text with the first nLines removed.
*/
export function removeFirstNLines(text: string, nLines: number) {
return text.split('\n').slice(nLines).join(`\n`);
}

View File

@ -113,7 +113,9 @@ async function createProxyServer(): Promise<[number, number]> {
let proxyPort = 0; // TCP port number, 0 means automatic allocation
await new Promise<void>((resolve, reject) => {
const listener = server.listen(0, '127.0.0.1', (err: Error) => {
// TODO: remove 'as any' below. According to @types/node v12.20.42, the
// callback type is `() => void`, but our code assumes `(err: Error) => void`
const listener = (server.listen as any)(0, '127.0.0.1', (err: Error) => {
if (err) {
console.error(`Error starting proxy server:\n${err}`);
reject(err);
@ -195,7 +197,9 @@ async function createInterceptorServer(): Promise<number> {
let interceptorPort = 0;
await new Promise<void>((resolve, reject) => {
const listener = server.listen(0, '127.0.0.1', (err: Error) => {
// TODO: remove 'as any' below. According to @types/node v12.20.42, the
// callback type is `() => void`, but our code assumes `(err: Error) => void`
const listener = (server.listen as any)(0, '127.0.0.1', (err: Error) => {
if (err) {
console.error(`Error starting interceptor server:\n${err}`);
reject(err);