Compare commits

..

1 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
188 changed files with 13449 additions and 47029 deletions

4
.gitattributes vendored
View File

@ -4,10 +4,6 @@
*.* -eol
*.sh text eol=lf
.dockerignore eol=lf
Dockerfile eol=lf
Dockerfile.* eol=lf
* text=auto eol=lf
# lf for the docs as it's auto-generated and will otherwise trigger an uncommited error on windows
docs/balena-cli.md text eol=lf

View File

@ -1,134 +0,0 @@
---
name: package and draft GitHub release
# https://github.com/product-os/flowzone/tree/master/.github/actions
inputs:
json:
description: 'JSON stringified object containing all the inputs from the calling workflow'
required: true
secrets:
description: 'JSON stringified object containing all the secrets from the calling workflow'
required: true
variables:
description: 'JSON stringified object containing all the variables from the calling workflow'
required: true
# --- custom environment
XCODE_APP_LOADER_EMAIL:
type: string
default: 'accounts+apple@balena.io'
NODE_VERSION:
type: string
default: '18.x'
VERBOSE:
type: string
default: 'true'
runs:
# https://docs.github.com/en/actions/creating-actions/creating-a-composite-action
using: 'composite'
steps:
- name: Download custom source artifact
uses: actions/download-artifact@v3
with:
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}-${{ runner.arch }}
path: ${{ runner.temp }}
- name: Extract custom source artifact
shell: pwsh
working-directory: .
run: tar -xf ${{ runner.temp }}/custom.tgz
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ inputs.NODE_VERSION }}
cache: npm
- name: Install additional tools
if: runner.os == 'Windows'
shell: bash
run: |
choco install yq
- name: Install additional tools
if: runner.os == 'macOS'
shell: bash
run: |
brew install coreutils
# https://www.electron.build/code-signing.html
# https://github.com/Apple-Actions/import-codesign-certs
- name: Import Apple code signing certificate
if: runner.os == 'macOS'
uses: apple-actions/import-codesign-certs@v1
with:
p12-file-base64: ${{ fromJSON(inputs.secrets).APPLE_SIGNING }}
p12-password: ${{ fromJSON(inputs.secrets).APPLE_SIGNING_PASSWORD }}
- name: Import Windows code signing certificate
if: runner.os == 'Windows'
shell: powershell
run: |
Set-Content -Path ${{ runner.temp }}/certificate.base64 -Value $env:WINDOWS_CERTIFICATE
certutil -decode ${{ runner.temp }}/certificate.base64 ${{ runner.temp }}/certificate.pfx
Remove-Item -path ${{ runner.temp }} -include certificate.base64
Import-PfxCertificate `
-FilePath ${{ runner.temp }}/certificate.pfx `
-CertStoreLocation Cert:\CurrentUser\My `
-Password (ConvertTo-SecureString -String $env:WINDOWS_CERTIFICATE_PASSWORD -Force -AsPlainText)
env:
WINDOWS_CERTIFICATE: ${{ fromJSON(inputs.secrets).WINDOWS_SIGNING }}
WINDOWS_CERTIFICATE_PASSWORD: ${{ fromJSON(inputs.secrets).WINDOWS_SIGNING_PASSWORD }}
# https://github.com/product-os/scripts/tree/master/shared
# https://github.com/product-os/balena-concourse/blob/master/pipelines/github-events/template.yml
- name: Package release
shell: bash
run: |
set -ea
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
runner_os="$(echo "${RUNNER_OS}" | tr '[:upper:]' '[:lower:]')"
runner_arch="$(echo "${RUNNER_ARCH}" | tr '[:upper:]' '[:lower:]')"
if [[ $runner_os =~ darwin|macos|osx ]]; then
CSC_KEY_PASSWORD=${{ fromJSON(inputs.secrets).APPLE_SIGNING_PASSWORD }}
CSC_KEYCHAIN=signing_temp
CSC_LINK=${{ fromJSON(inputs.secrets).APPLE_SIGNING }}
elif [[ $runner_os =~ windows|win ]]; then
CSC_KEY_PASSWORD=${{ fromJSON(inputs.secrets).WINDOWS_SIGNING_PASSWORD }}
CSC_LINK='${{ runner.temp }}\certificate.pfx'
# patches/all/oclif.patch
MSYSSHELLPATH="$(which bash)"
MSYSTEM=MSYS
# (signtool.exe) https://github.com/actions/runner-images/blob/main/images/win/Windows2019-Readme.md#installed-windows-sdks
PATH="/c/Program Files (x86)/Windows Kits/10/bin/${runner_arch}:${PATH}"
fi
npm run package
find dist -type f -maxdepth 1
env:
# https://github.blog/2020-08-03-github-actions-improvements-for-fork-and-pull-request-workflows/#improvements-for-public-repository-forks
# https://docs.github.com/en/actions/managing-workflow-runs/approving-workflow-runs-from-public-forks#about-workflow-runs-from-public-forks
CSC_FOR_PULL_REQUEST: true
# https://sectigo.com/resource-library/time-stamping-server
TIMESTAMP_SERVER: http://timestamp.sectigo.com
# Apple notarization (automation/build-bin.ts)
XCODE_APP_LOADER_EMAIL: ${{ inputs.XCODE_APP_LOADER_EMAIL }}
XCODE_APP_LOADER_PASSWORD: ${{ fromJSON(inputs.secrets).XCODE_APP_LOADER_PASSWORD }}
XCODE_APP_LOADER_TEAM_ID: ${{ inputs.XCODE_APP_LOADER_TEAM_ID }}
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: gh-release-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}
path: dist
retention-days: 1

View File

@ -1,59 +0,0 @@
---
name: test release
# https://github.com/product-os/flowzone/tree/master/.github/actions
inputs:
json:
description: "JSON stringified object containing all the inputs from the calling workflow"
required: true
secrets:
description: "JSON stringified object containing all the secrets from the calling workflow"
required: true
variables:
description: "JSON stringified object containing all the variables from the calling workflow"
required: true
# --- custom environment
NODE_VERSION:
type: string
default: '18.x'
VERBOSE:
type: string
default: "true"
runs:
# https://docs.github.com/en/actions/creating-actions/creating-a-composite-action
using: "composite"
steps:
# https://github.com/actions/setup-node#caching-global-packages-data
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ inputs.NODE_VERSION }}
cache: npm
- name: Test release
shell: bash
run: |
set -ea
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
if [[ -e package-lock.json ]] || [[ -e npm-shrinkwrap.json ]]; then
npm ci
else
npm i
fi
npm run build
npm run test
- name: Compress custom source
shell: pwsh
run: tar --exclude-vcs -acf ${{ runner.temp }}/custom.tgz .
- name: Upload custom artifact
uses: actions/upload-artifact@v3
with:
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}-${{ runner.arch }}
path: ${{ runner.temp }}/custom.tgz
retention-days: 1

View File

@ -1,29 +0,0 @@
name: Flowzone
on:
pull_request:
types: [opened, synchronize, closed]
branches: [main, master]
pull_request_target:
types: [opened, synchronize, closed]
branches: [main, master]
jobs:
flowzone:
name: Flowzone
uses: product-os/flowzone/.github/workflows/flowzone.yml@master
# prevent duplicate workflow executions for pull_request and pull_request_target
if: |
(
github.event.pull_request.head.repo.full_name == github.repository &&
github.event_name == 'pull_request'
) || (
github.event.pull_request.head.repo.full_name != github.repository &&
github.event_name == 'pull_request_target'
)
secrets: inherit
with:
custom_runs_on: '[["self-hosted","Linux","distro:focal","X64"],["self-hosted","Linux","distro:focal","ARM64"],["macos-12"],["windows-2019"]]'
repo_config: true
repo_description: "The official balena CLI tool."
github_prerelease: false

15
.resinci.yml Normal file
View File

@ -0,0 +1,15 @@
---
npm:
platforms:
- name: linux
os: ubuntu
architecture: x86_64
node_versions:
- "12"
- "14"
- name: linux
os: alpine
architecture: x86_64
node_versions:
- "12"
- "14"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -125,39 +125,6 @@ The README file is manually edited, but subsections are automatically extracted
The `INSTALL*.md` and `TROUBLESHOOTING.md` files are also manually edited.
## Patches folder
The `patches` folder contains patch files created with the
[patch-package](https://www.npmjs.com/package/patch-package) tool. Small code changes to
third-party modules can be made by directly editing Javascript files under the `node_modules`
folder and then running `patch-package` to create the patch files. The patch files are then
applied immediately after `npm install`, through the `postinstall` script defined in
`package.json`.
The subfolders of the `patches` folder are documented in the
[apply-patches.js](https://github.com/balena-io/balena-cli/blob/master/patches/apply-patches.js)
script.
To make changes to the patch files under the `patches` folder, **do not edit them directly,**
not even for a "single character change" because the hash values in the patch files also need
to be recomputed by `patch-packages`. Instead, edit the relevant files under `node_modules`
directly, and then run `patch-packages` with the `--patch-dir` option to specify the subfolder
where the patch should be saved. For example, edit `node_modules/exit-hook/index.js` and then
run:
```sh
$ npx patch-package --patch-dir patches/all exit-hook
```
That said, these kinds of patches should be avoided in favour of creating pull requests
upstream. Patch files create additional maintenance work over time as the patches need to be
updated when the dependencies are updated, and they prevent the compounding community benefit
that sharing fixes upstream have on open source projects like the balena CLI. The typical
scenario where these patches are used is when the upstream maintainers are unresponsive or
unwilling to merge the required fixes, the fixes are very small and specific to the balena CLI,
and creating a fork of the upstream repo is likely to be more long-term effort than maintaining
the patches.
## Windows
Besides the regular npm installation dependencies, the `npm run build:installer` script

View File

@ -78,8 +78,8 @@ 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 18.**
> **Versions 19 and later are not yet fully supported.**
> **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
@ -89,7 +89,7 @@ some development tools to be installed first, as follows.
$ sudo apt-get update && sudo apt-get -y install curl python3 git make g++
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
$ . ~/.bashrc
$ nvm install 18
$ nvm install 12
```
The `curl` command line above uses
@ -106,15 +106,15 @@ recommended.
```sh
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
$ . ~/.bashrc
$ nvm install 18
$ nvm install 12
```
#### **Windows** (not WSL)
Install:
* Node.js v12 from the [Nodejs.org releases page](https://nodejs.org/en/download/releases/).
* If you'd like the ability to switch between Node.js versions, install
- Node.js v18 from the [Nodejs.org releases page](https://nodejs.org/en/download/releases/).
[nvm-windows](https://github.com/coreybutler/nvm-windows#node-version-manager-nvm-for-windows)
instead.
* The [MSYS2 shell](https://www.msys2.org/), which provides `git`, `make`, `g++` and more:

View File

@ -17,7 +17,7 @@
import type { JsonVersions } from '../lib/commands/version';
import { run as oclifRun } from '@oclif/core';
import { run as oclifRun } from 'oclif';
import * as archiver from 'archiver';
import * as Bluebird from 'bluebird';
import { execFile } from 'child_process';
@ -30,7 +30,6 @@ import * as path from 'path';
import * as rimraf from 'rimraf';
import * as semver from 'semver';
import { promisify } from 'util';
import { notarize } from '@electron/notarize';
import { stripIndent } from '../build/utils/lazy';
import {
@ -46,6 +45,8 @@ const execFileAsync = promisify(execFile);
export const packageJSON = loadPackageJson();
export const version = 'v' + packageJSON.version;
const arch = process.arch;
const MSYS2_BASH =
process.env.MSYSSHELLPATH || 'C:\\msys64\\usr\\bin\\bash.exe';
function dPath(...paths: string[]) {
return path.join(ROOT, 'dist', ...paths);
@ -88,7 +89,7 @@ async function diffPkgOutput(pkgOut: string) {
'tests',
'test-data',
'pkg',
`expected-warnings-${process.platform}-${arch}.txt`,
`expected-warnings-${process.platform}.txt`,
);
const absSavedPath = path.join(ROOT, relSavedPath);
const ignoreStartsWith = [
@ -181,18 +182,9 @@ async function execPkg(...args: any[]) {
* to be directly executed from inside another binary executable.)
*/
async function buildPkg() {
// https://github.com/vercel/pkg#targets
let targets = `linux-${arch}`;
// TBC: not possible to build for macOS or Windows arm64 on x64 nodes
if (process.platform === 'darwin') {
targets = `macos-x64`;
}
if (process.platform === 'win32') {
targets = `win-x64`;
}
const args = [
'--targets',
targets,
'--target',
'host',
'--output',
'build-bin/balena',
'package.json',
@ -207,6 +199,7 @@ async function buildPkg() {
const paths: Array<[string, string[], string[]]> = [
// [platform, [source path], [destination path]]
['*', ['open', 'xdg-open'], ['xdg-open']],
['*', ['opn', 'xdg-open'], ['xdg-open-402']],
['darwin', ['denymount', 'bin', 'denymount'], ['denymount']],
];
await Promise.all(
@ -432,28 +425,20 @@ async function renameInstallerFiles() {
/**
* If the CSC_LINK and CSC_KEY_PASSWORD env vars are set, digitally sign the
* executable installer using Microsoft SignTool.exe (Sign Tool)
* https://learn.microsoft.com/en-us/dotnet/framework/tools/signtool-exe
* executable installer by running the balena-io/scripts/shared/sign-exe.sh
* script (which must be in the PATH) using a MSYS2 bash shell.
*/
async function signWindowsInstaller() {
if (process.env.CSC_LINK && process.env.CSC_KEY_PASSWORD) {
const exeName = renamedOclifInstallers[process.platform];
console.log(`Signing installer "${exeName}"`);
// trust ...
await execFileAsync('signtool.exe', [
'sign',
'-t',
process.env.TIMESTAMP_SERVER || 'http://timestamp.comodoca.com',
await execFileAsync(MSYS2_BASH, [
'sign-exe.sh',
'-f',
process.env.CSC_LINK,
'-p',
process.env.CSC_KEY_PASSWORD,
exeName,
'-d',
`balena-cli ${version}`,
exeName,
]);
// ... but verify
await execFileAsync('signtool.exe', ['verify', '-pa', '-v', exeName]);
} else {
console.log(
'Skipping installer signing step because CSC_* env vars are not set',
@ -465,20 +450,14 @@ async function signWindowsInstaller() {
* Wait for Apple Installer Notarization to continue
*/
async function notarizeMacInstaller(): Promise<void> {
const teamId = process.env.XCODE_APP_LOADER_TEAM_ID || '66H43P8FRG';
const appleId =
process.env.XCODE_APP_LOADER_EMAIL || 'accounts+apple@balena.io';
const appleIdPassword = process.env.XCODE_APP_LOADER_PASSWORD;
if (appleIdPassword && teamId) {
await notarize({
tool: 'notarytool',
teamId,
appPath: renamedOclifInstallers.darwin,
appleId,
appleIdPassword,
});
}
const appleId = 'accounts+apple@balena.io';
const { notarize } = await import('electron-notarize');
await notarize({
appBundleId: 'io.balena.etcher',
appPath: renamedOclifInstallers.darwin,
appleId,
appleIdPassword: '@keychain:CLI_PASSWORD',
});
}
/**
@ -492,10 +471,9 @@ export async function buildOclifInstaller() {
let packOpts = ['-r', ROOT];
if (process.platform === 'darwin') {
packOS = 'macos';
packOpts = packOpts.concat('--targets', 'darwin-x64');
} else if (process.platform === 'win32') {
packOS = 'win';
packOpts = packOpts.concat('--targets', 'win32-x64');
packOpts = packOpts.concat('-t', 'win32-x64');
}
if (packOS) {
console.log(`Building oclif installer for CLI ${version}`);
@ -513,11 +491,10 @@ export async function buildOclifInstaller() {
await signFilesForNotarization();
}
console.log('=======================================================');
console.log(`oclif ${packCmd} ${packOpts.join(' ')}`);
console.log(`oclif "${packCmd}" "${packOpts.join('" "')}"`);
console.log(`cwd="${process.cwd()}" ROOT="${ROOT}"`);
console.log('=======================================================');
const oclifPath = path.join(ROOT, 'node_modules', 'oclif');
await oclifRun([packCmd].concat(...packOpts), oclifPath);
await oclifRun([packCmd].concat(...packOpts));
await renameInstallerFiles();
// The Windows installer is explicitly signed here (oclif doesn't do it).
// The macOS installer is automatically signed by oclif (which runs the

View File

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Command as OclifCommandClass } from '@oclif/core';
import { Command as OclifCommandClass } from '@oclif/command';
type OclifCommand = typeof OclifCommandClass;

View File

@ -62,11 +62,12 @@ class FakeHelpCommand {
'$ balena help os download',
];
args = {
command: {
args = [
{
name: 'command',
description: 'command to show help for',
},
};
];
usage = 'help [command]';

View File

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Parser } from '@oclif/core';
import { flagUsages } from '@oclif/parser';
import * as ent from 'ent';
import * as _ from 'lodash';
@ -37,8 +37,8 @@ function renderOclifCommand(command: OclifCommand): string[] {
if (!_.isEmpty(command.args)) {
result.push('### Arguments');
for (const [name, arg] of Object.entries(command.args!)) {
result.push(`#### ${name.toUpperCase()}`, arg.description || '');
for (const arg of command.args!) {
result.push(`#### ${arg.name.toUpperCase()}`, arg.description || '');
}
}
@ -49,7 +49,7 @@ function renderOclifCommand(command: OclifCommand): string[] {
continue;
}
flag.name = name;
const flagUsage = Parser.flagUsages([flag])
const flagUsage = flagUsages([flag])
.map(([usage, _description]) => usage)
.join()
.trim();

View File

@ -15,12 +15,11 @@
* limitations under the License.
*/
// tslint:disable-next-line:import-blacklist
import { stripIndent } from 'common-tags';
import * as _ from 'lodash';
import { promises as fs } from 'fs';
import * as path from 'path';
import { simpleGit } from 'simple-git';
const stripIndent = require('common-tags/lib/stripIndent');
const _ = require('lodash');
const { promises: fs } = require('fs');
const path = require('path');
const simplegit = require('simple-git/promise');
const ROOT = path.normalize(path.join(__dirname, '..'));
@ -32,7 +31,7 @@ const ROOT = path.normalize(path.join(__dirname, '..'));
* using `touch`.
*/
async function checkBuildTimestamps() {
const git = simpleGit(ROOT);
const git = simplegit(ROOT);
const docFile = path.join(ROOT, 'docs', 'balena-cli.md');
const [docStat, gitStatus] = await Promise.all([
fs.stat(docFile),
@ -82,5 +81,4 @@ async function run() {
}
}
// tslint:disable-next-line:no-floating-promises
run();

View File

@ -8,21 +8,19 @@ _balena() {
local context state line curcontext="$curcontext"
# Valid top-level completions
main_commands=( build deploy envs fleets join keys leave login logout logs note orgs preload push releases scan settings ssh support tags tunnel version whoami api-key api-keys app block config device device devices env fleet fleet internal key key local os release release tag util )
main_commands=( build deploy envs fleets join keys leave login logout logs note orgs preload push releases scan settings ssh support tags tunnel version whoami api-key config device device devices env fleet fleet internal key key local os release release tag util )
# Sub-completions
api_key_cmds=( generate revoke )
app_cmds=( create )
block_cmds=( create )
api_key_cmds=( generate )
config_cmds=( generate inject read reconfigure write )
device_cmds=( deactivate identify init local-mode move os-update pin public-url purge reboot register rename restart rm shutdown track-fleet )
device_cmds=( deactivate identify init local-mode move os-update public-url purge reboot register rename restart rm shutdown )
devices_cmds=( supported )
env_cmds=( add rename rm )
fleet_cmds=( create pin purge rename restart rm track-latest )
fleet_cmds=( create purge rename restart rm )
internal_cmds=( osinit )
key_cmds=( add rm )
local_cmds=( configure flash )
os_cmds=( build-config configure download initialize versions )
release_cmds=( finalize invalidate validate )
release_cmds=( finalize )
tag_cmds=( rm set )
@ -45,12 +43,6 @@ _balena_sec_cmds() {
"api-key")
_describe -t api_key_cmds 'api-key_cmd' api_key_cmds "$@" && ret=0
;;
"app")
_describe -t app_cmds 'app_cmd' app_cmds "$@" && ret=0
;;
"block")
_describe -t block_cmds 'block_cmd' block_cmds "$@" && ret=0
;;
"config")
_describe -t config_cmds 'config_cmd' config_cmds "$@" && ret=0
;;

View File

@ -7,21 +7,19 @@ _balena_complete()
local cur prev
# Valid top-level completions
main_commands="build deploy envs fleets join keys leave login logout logs note orgs preload push releases scan settings ssh support tags tunnel version whoami api-key api-keys app block config device device devices env fleet fleet internal key key local os release release tag util"
main_commands="build deploy envs fleets join keys leave login logout logs note orgs preload push releases scan settings ssh support tags tunnel version whoami api-key config device device devices env fleet fleet internal key key local os release release tag util"
# Sub-completions
api_key_cmds="generate revoke"
app_cmds="create"
block_cmds="create"
api_key_cmds="generate"
config_cmds="generate inject read reconfigure write"
device_cmds="deactivate identify init local-mode move os-update pin public-url purge reboot register rename restart rm shutdown track-fleet"
device_cmds="deactivate identify init local-mode move os-update public-url purge reboot register rename restart rm shutdown"
devices_cmds="supported"
env_cmds="add rename rm"
fleet_cmds="create pin purge rename restart rm track-latest"
fleet_cmds="create purge rename restart rm"
internal_cmds="osinit"
key_cmds="add rm"
local_cmds="configure flash"
os_cmds="build-config configure download initialize versions"
release_cmds="finalize invalidate validate"
release_cmds="finalize"
tag_cmds="rm set"
@ -39,12 +37,6 @@ _balena_complete()
api-key)
COMPREPLY=( $(compgen -W "$api_key_cmds" -- $cur) )
;;
app)
COMPREPLY=( $(compgen -W "$app_cmds" -- $cur) )
;;
block)
COMPREPLY=( $(compgen -W "$block_cmds" -- $cur) )
;;
config)
COMPREPLY=( $(compgen -W "$config_cmds" -- $cur) )
;;

View File

@ -361,7 +361,7 @@ field to sort by (prepend '-' for descending order)
Display detailed information about a single fleet.
Fleets may be specified by fleet name or slug. Fleet slugs are
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
the recommended option, as they are unique and unambiguous. Slugs can be
listed with the `balena fleets` command. Note that slugs may change if the
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -369,26 +369,23 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public fleet, or with fleets
from other balena accounts that you may be invited to join under any role.
For this reason, fleet names are especially discouraged in scripts (e.g. CI
environments).
environments). Numeric fleet IDs are deprecated because they consist of an
implementation detail of the balena backend. We intend to remove support for
numeric IDs at some point in the future.
Examples:
$ balena fleet MyFleet
$ balena fleet myorg/myfleet
$ balena fleet myorg/myfleet --view
### Arguments
#### FLEET
fleet name or slug (preferred)
fleet name, slug (preferred), or numeric ID (deprecated)
### Options
#### --view
open fleet dashboard page
#### --fields FIELDS
only show provided fields (comma-separated)
@ -443,7 +440,7 @@ fleet device type (Check available types with `balena devices supported`)
Purge data from all devices belonging to a fleet.
This will clear the fleet's '/data' directory.
Fleets may be specified by fleet name or slug. Fleet slugs are
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
the recommended option, as they are unique and unambiguous. Slugs can be
listed with the `balena fleets` command. Note that slugs may change if the
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -451,7 +448,9 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public fleet, or with fleets
from other balena accounts that you may be invited to join under any role.
For this reason, fleet names are especially discouraged in scripts (e.g. CI
environments).
environments). Numeric fleet IDs are deprecated because they consist of an
implementation detail of the balena backend. We intend to remove support for
numeric IDs at some point in the future.
Examples:
@ -462,7 +461,7 @@ Examples:
#### FLEET
fleet name or slug (preferred)
fleet name, slug (preferred), or numeric ID (deprecated)
### Options
@ -473,7 +472,7 @@ Rename a fleet.
Note, if the `newName` parameter is omitted, it will be
prompted for interactively.
Fleets may be specified by fleet name or slug. Fleet slugs are
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
the recommended option, as they are unique and unambiguous. Slugs can be
listed with the `balena fleets` command. Note that slugs may change if the
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -481,7 +480,9 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public fleet, or with fleets
from other balena accounts that you may be invited to join under any role.
For this reason, fleet names are especially discouraged in scripts (e.g. CI
environments).
environments). Numeric fleet IDs are deprecated because they consist of an
implementation detail of the balena backend. We intend to remove support for
numeric IDs at some point in the future.
Examples:
@ -493,7 +494,7 @@ Examples:
#### FLEET
fleet name or slug (preferred)
fleet name, slug (preferred), or numeric ID (deprecated)
#### NEWNAME
@ -505,7 +506,7 @@ the new name for the fleet
Restart all devices belonging to a fleet.
Fleets may be specified by fleet name or slug. Fleet slugs are
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
the recommended option, as they are unique and unambiguous. Slugs can be
listed with the `balena fleets` command. Note that slugs may change if the
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -513,7 +514,9 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public fleet, or with fleets
from other balena accounts that you may be invited to join under any role.
For this reason, fleet names are especially discouraged in scripts (e.g. CI
environments).
environments). Numeric fleet IDs are deprecated because they consist of an
implementation detail of the balena backend. We intend to remove support for
numeric IDs at some point in the future.
Examples:
@ -524,7 +527,7 @@ Examples:
#### FLEET
fleet name or slug (preferred)
fleet name, slug (preferred), or numeric ID (deprecated)
### Options
@ -534,7 +537,7 @@ Permanently remove a fleet.
The --yes option may be used to avoid interactive confirmation.
Fleets may be specified by fleet name or slug. Fleet slugs are
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
the recommended option, as they are unique and unambiguous. Slugs can be
listed with the `balena fleets` command. Note that slugs may change if the
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -542,7 +545,9 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public fleet, or with fleets
from other balena accounts that you may be invited to join under any role.
For this reason, fleet names are especially discouraged in scripts (e.g. CI
environments).
environments). Numeric fleet IDs are deprecated because they consist of an
implementation detail of the balena backend. We intend to remove support for
numeric IDs at some point in the future.
Examples:
@ -554,7 +559,7 @@ Examples:
#### FLEET
fleet name or slug (preferred)
fleet name, slug (preferred), or numeric ID (deprecated)
### Options
@ -621,10 +626,6 @@ password
TCP port number of local HTTP login server (--web auth only)
#### -H, --hideExperimentalWarning
Hides warning for experimental features
## logout
Logout from your balena account.
@ -649,7 +650,7 @@ List all of your devices.
Devices can be filtered by fleet with the `--fleet` option.
Fleets may be specified by fleet name or slug. Fleet slugs are
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
the recommended option, as they are unique and unambiguous. Slugs can be
listed with the `balena fleets` command. Note that slugs may change if the
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -657,7 +658,9 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public fleet, or with fleets
from other balena accounts that you may be invited to join under any role.
For this reason, fleet names are especially discouraged in scripts (e.g. CI
environments).
environments). Numeric fleet IDs are deprecated because they consist of an
implementation detail of the balena backend. We intend to remove support for
numeric IDs at some point in the future.
The --json option is recommended when scripting the output of this command,
because field names are less likely to change in JSON format and because it
@ -675,11 +678,31 @@ Examples:
#### -f, --fleet FLEET
fleet name or slug (preferred)
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
@ -698,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;
@ -709,7 +752,6 @@ Show information about a single device.
Examples:
$ balena device 7cf02a6
$ balena device 7cf02a6 --view
### Arguments
@ -719,9 +761,13 @@ the device uuid
### Options
#### --view
#### --fields FIELDS
open device dashboard page
only show provided fields (comma-separated)
#### -j, --json
output in json format
## device deactivate &#60;uuid&#62;
@ -787,7 +833,7 @@ If the '--fleet' or '--drive' options are omitted, interactive menus will be
presented with values to choose from. If the '--os-version' option is omitted,
the latest released OS version for the fleet's default device type will be used.
Fleets may be specified by fleet name or slug. Fleet slugs are
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
the recommended option, as they are unique and unambiguous. Slugs can be
listed with the `balena fleets` command. Note that slugs may change if the
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -795,7 +841,9 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public fleet, or with fleets
from other balena accounts that you may be invited to join under any role.
For this reason, fleet names are especially discouraged in scripts (e.g. CI
environments).
environments). Numeric fleet IDs are deprecated because they consist of an
implementation detail of the balena backend. We intend to remove support for
numeric IDs at some point in the future.
Image configuration questions will be asked interactively unless a pre-configured
'config.json' file is provided with the '--config' option. The file can be
@ -805,14 +853,13 @@ Examples:
$ balena device init
$ balena device init -f myorg/myfleet
$ balena device init --fleet myFleet --os-version 2.101.7 --drive /dev/disk5 --config config.json --yes
$ balena device init --fleet myFleet --os-version 2.83.21+rev1.prod --drive /dev/disk5 --config config.json --yes
### Options
#### -f, --fleet FLEET
fleet name or slug (preferred)
fleet name, slug (preferred), or numeric ID (deprecated)
#### -y, --yes
@ -844,10 +891,6 @@ path to the config JSON file, see `balena os build-config`
custom key name assigned to generated provisioning api key
#### --provisioning-key-expiry-date PROVISIONING-KEY-EXPIRY-DATE
expiry date assigned to generated provisioning api key (format: YYYY-MM-DD)
## device local-mode &#60;uuid&#62;
Output current local mode status, or enable/disable local mode
@ -886,7 +929,7 @@ Move one or more devices to another fleet.
If --fleet is omitted, the fleet will be prompted for interactively.
Fleets may be specified by fleet name or slug. Fleet slugs are
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
the recommended option, as they are unique and unambiguous. Slugs can be
listed with the `balena fleets` command. Note that slugs may change if the
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -894,7 +937,9 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public fleet, or with fleets
from other balena accounts that you may be invited to join under any role.
For this reason, fleet names are especially discouraged in scripts (e.g. CI
environments).
environments). Numeric fleet IDs are deprecated because they consist of an
implementation detail of the balena backend. We intend to remove support for
numeric IDs at some point in the future.
Examples:
@ -913,7 +958,7 @@ comma-separated list (no blank spaces) of device UUIDs to be moved
#### -f, --fleet FLEET
fleet name or slug (preferred)
fleet name, slug (preferred), or numeric ID (deprecated)
## device os-update &#60;uuid&#62;
@ -927,7 +972,6 @@ Requires balenaCloud; will not work with openBalena or standalone balenaOS.
Examples:
$ balena device os-update 23c73a1
$ balena device os-update 23c73a1 --version 2.101.7
$ balena device os-update 23c73a1 --version 2.31.0+rev1.prod
### Arguments
@ -952,6 +996,9 @@ This command will output the current public URL for the
specified device. It can also enable or disable the URL,
or output the enabled status, using the respective options.
The old command style 'balena device public-url enable <uuid>'
is deprecated, but still supported.
Examples:
$ balena device public-url 23c73a1
@ -965,6 +1012,10 @@ Examples:
the uuid of the device to manage
#### LEGACYUUID
### Options
#### --enable
@ -1026,7 +1077,7 @@ Register a new device with a balena fleet.
If --uuid is not provided, a new UUID will be automatically assigned.
Fleets may be specified by fleet name or slug. Fleet slugs are
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
the recommended option, as they are unique and unambiguous. Slugs can be
listed with the `balena fleets` command. Note that slugs may change if the
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -1034,20 +1085,21 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public fleet, or with fleets
from other balena accounts that you may be invited to join under any role.
For this reason, fleet names are especially discouraged in scripts (e.g. CI
environments).
environments). Numeric fleet IDs are deprecated because they consist of an
implementation detail of the balena backend. We intend to remove support for
numeric IDs at some point in the future.
Examples:
$ balena device register MyFleet
$ balena device register MyFleet --uuid <uuid>
$ balena device register myorg/myfleet --uuid <uuid>
$ balena device register myorg/myfleet --uuid <uuid> --deviceType <deviceTypeSlug>
### Arguments
#### FLEET
fleet name or slug (preferred)
fleet name, slug (preferred), or numeric ID (deprecated)
### Options
@ -1055,10 +1107,6 @@ fleet name or slug (preferred)
custom uuid
#### --deviceType DEVICETYPE
device type slug (run 'balena devices supported' for possible values)
## device rename &#60;uuid&#62; [newName]
Rename a device.
@ -1184,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;
@ -1205,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
@ -1265,7 +1329,7 @@ name may be null in JSON output (or 'N/A' in tabular output) if the fleet that
the device belonged to is no longer accessible by the current user (for example,
in case the current user was removed from the fleet by the fleet's owner).
Fleets may be specified by fleet name or slug. Fleet slugs are
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
the recommended option, as they are unique and unambiguous. Slugs can be
listed with the `balena fleets` command. Note that slugs may change if the
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -1273,13 +1337,16 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public fleet, or with fleets
from other balena accounts that you may be invited to join under any role.
For this reason, fleet names are especially discouraged in scripts (e.g. CI
environments).
environments). Numeric fleet IDs are deprecated because they consist of an
implementation detail of the balena backend. We intend to remove support for
numeric IDs at some point in the future.
Examples:
$ balena envs --fleet myorg/myfleet
$ balena envs --fleet MyFleet --json
$ balena envs --fleet MyFleet --service MyService
$ balena envs --fleet MyFleet --service MyService
$ balena envs --fleet MyFleet --config
$ balena envs --device 7cf02a6
$ balena envs --device 7cf02a6 --json
@ -1290,7 +1357,7 @@ Examples:
#### -f, --fleet FLEET
fleet name or slug (preferred)
fleet name, slug (preferred), or numeric ID (deprecated)
#### -c, --config
@ -1300,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
@ -1400,7 +1487,7 @@ therefore the --service option cannot be used when the variable name starts
with a reserved prefix. When defining custom fleet variables, please avoid
these reserved prefixes.
Fleets may be specified by fleet name or slug. Fleet slugs are
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
the recommended option, as they are unique and unambiguous. Slugs can be
listed with the `balena fleets` command. Note that slugs may change if the
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -1408,7 +1495,9 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public fleet, or with fleets
from other balena accounts that you may be invited to join under any role.
For this reason, fleet names are especially discouraged in scripts (e.g. CI
environments).
environments). Numeric fleet IDs are deprecated because they consist of an
implementation detail of the balena backend. We intend to remove support for
numeric IDs at some point in the future.
Examples:
@ -1436,7 +1525,7 @@ variable value; if omitted, use value from this process' environment
#### -f, --fleet FLEET
fleet name or slug (preferred)
fleet name, slug (preferred), or numeric ID (deprecated)
#### -d, --device DEVICE
@ -1519,7 +1608,7 @@ select a service variable (may be used together with the --device option)
List all tags and their values for the specified fleet, device or release.
Fleets may be specified by fleet name or slug. Fleet slugs are
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
the recommended option, as they are unique and unambiguous. Slugs can be
listed with the `balena fleets` command. Note that slugs may change if the
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -1527,7 +1616,9 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public fleet, or with fleets
from other balena accounts that you may be invited to join under any role.
For this reason, fleet names are especially discouraged in scripts (e.g. CI
environments).
environments). Numeric fleet IDs are deprecated because they consist of an
implementation detail of the balena backend. We intend to remove support for
numeric IDs at some point in the future.
Examples:
@ -1541,7 +1632,7 @@ Examples:
#### -f, --fleet FLEET
fleet name or slug (preferred)
fleet name, slug (preferred), or numeric ID (deprecated)
#### -d, --device DEVICE
@ -1551,11 +1642,35 @@ 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.
Fleets may be specified by fleet name or slug. Fleet slugs are
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
the recommended option, as they are unique and unambiguous. Slugs can be
listed with the `balena fleets` command. Note that slugs may change if the
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -1563,7 +1678,9 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public fleet, or with fleets
from other balena accounts that you may be invited to join under any role.
For this reason, fleet names are especially discouraged in scripts (e.g. CI
environments).
environments). Numeric fleet IDs are deprecated because they consist of an
implementation detail of the balena backend. We intend to remove support for
numeric IDs at some point in the future.
Examples:
@ -1583,7 +1700,7 @@ the key string of the tag
#### -f, --fleet FLEET
fleet name or slug (preferred)
fleet name, slug (preferred), or numeric ID (deprecated)
#### -d, --device DEVICE
@ -1601,7 +1718,7 @@ You can optionally provide a value to be associated with the created
tag, as an extra argument after the tag key. If a value isn't
provided, a tag with an empty value is created.
Fleets may be specified by fleet name or slug. Fleet slugs are
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
the recommended option, as they are unique and unambiguous. Slugs can be
listed with the `balena fleets` command. Note that slugs may change if the
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -1609,7 +1726,9 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public fleet, or with fleets
from other balena accounts that you may be invited to join under any role.
For this reason, fleet names are especially discouraged in scripts (e.g. CI
environments).
environments). Numeric fleet IDs are deprecated because they consist of an
implementation detail of the balena backend. We intend to remove support for
numeric IDs at some point in the future.
Examples:
@ -1636,7 +1755,7 @@ the optional value associated with the tag
#### -f, --fleet FLEET
fleet name or slug (preferred)
fleet name, slug (preferred), or numeric ID (deprecated)
#### -d, --device DEVICE
@ -1715,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.
@ -1731,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.
@ -1921,7 +2072,7 @@ Examples:
#### FLEETORDEVICE
fleet name/slug, device uuid, or address of local device
fleet name/slug/id, device uuid, or address of local device
#### SERVICE
@ -1989,7 +2140,7 @@ Examples:
#### DEVICEORFLEET
device UUID or fleet name/slug
device UUID or fleet name/slug/ID
### Options
@ -2075,11 +2226,9 @@ Development images can be selected by appending `.dev` to the version.
Examples:
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version 2.101.7
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version 2022.7.0
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version ^2.90.0
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version 2.60.1+rev1
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version 2.60.1+rev1.dev
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version ^2.60.0
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version 2021.10.2.prod
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version latest
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version default
@ -2163,9 +2312,6 @@ confuse the balenaOS "development mode" with a device's "local mode", the latter
being a supervisor feature that allows the "balena push" command to push a user's
application directly to a device in the local network.
The '--secureBoot' option is used to configure a balenaOS installer image to opt-in
secure boot and disk encryption.
The --system-connection (-c) option is used to inject NetworkManager connection
profiles for additional network interfaces, such as cellular/GSM or additional
WiFi or ethernet connections. This option may be passed multiple times in case there
@ -2173,7 +2319,7 @@ are multiple files to inject. See connection profile examples and reference at:
https://www.balena.io/docs/reference/OS/network/2.x/
https://developer.gnome.org/NetworkManager/stable/ref-settings.html
Fleets may be specified by fleet name or slug. Fleet slugs are
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
the recommended option, as they are unique and unambiguous. Slugs can be
listed with the `balena fleets` command. Note that slugs may change if the
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -2181,7 +2327,9 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public fleet, or with fleets
from other balena accounts that you may be invited to join under any role.
For this reason, fleet names are especially discouraged in scripts (e.g. CI
environments).
environments). Numeric fleet IDs are deprecated because they consist of an
implementation detail of the balena backend. We intend to remove support for
numeric IDs at some point in the future.
Note: This command is currently not supported on Windows natively. Windows users
are advised to install the Windows Subsystem for Linux (WSL) with Ubuntu, and use
@ -2210,7 +2358,7 @@ ask advanced configuration questions (when in interactive mode)
#### -f, --fleet FLEET
fleet name or slug (preferred)
fleet name, slug (preferred), or numeric ID (deprecated)
#### --config CONFIG
@ -2236,10 +2384,6 @@ WiFi SSID (network name) (non-interactive configuration)
Configure balenaOS to operate in development mode
#### --secureBoot
Configure balenaOS installer to opt-in secure boot and disk encryption
#### -d, --device DEVICE
device UUID
@ -2264,16 +2408,10 @@ paths to local files to place into the 'system-connections' directory
custom key name assigned to generated provisioning api key
#### --provisioning-key-expiry-date PROVISIONING-KEY-EXPIRY-DATE
expiry date assigned to generated provisioning api key (format: YYYY-MM-DD)
## os initialize &#60;image&#62;
Initialize an os image for a device with a previously
configured operating system image and flash the
an external storage drive or the device's storage
medium depending on the device type.
configured operating system image.
Note: Initializing the device may ask for administrative permissions
@ -2324,16 +2462,13 @@ confuse the balenaOS "development mode" with a device's "local mode", the latter
being a supervisor feature that allows the "balena push" command to push a user's
application directly to a device in the local network.
The '--secureBoot' option is used to configure a balenaOS installer image to opt-in
secure boot and disk encryption.
To configure an image for a fleet of mixed device types, use the --fleet option
alongside the --deviceType option to specify the target device type.
To avoid interactive questions, specify a command line option for each question that
would otherwise be asked.
Fleets may be specified by fleet name or slug. Fleet slugs are
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
the recommended option, as they are unique and unambiguous. Slugs can be
listed with the `balena fleets` command. Note that slugs may change if the
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -2341,7 +2476,9 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public fleet, or with fleets
from other balena accounts that you may be invited to join under any role.
For this reason, fleet names are especially discouraged in scripts (e.g. CI
environments).
environments). Numeric fleet IDs are deprecated because they consist of an
implementation detail of the balena backend. We intend to remove support for
numeric IDs at some point in the future.
Examples:
@ -2350,7 +2487,6 @@ Examples:
$ balena config generate --device 7cf02a6 --version 2.12.7 --deviceApiKey <existingDeviceKey>
$ balena config generate --device 7cf02a6 --version 2.12.7 --output config.json
$ balena config generate --fleet myorg/fleet --version 2.12.7 --dev
$ balena config generate --fleet myorg/fleet --version 2.12.7 --secureBoot
$ balena config generate --fleet myorg/fleet --version 2.12.7 --deviceType fincm3
$ balena config generate --fleet myorg/fleet --version 2.12.7 --output config.json
$ balena config generate --fleet myorg/fleet --version 2.12.7 --network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 15
@ -2363,16 +2499,12 @@ a balenaOS version
#### -f, --fleet FLEET
fleet name or slug (preferred)
fleet name, slug (preferred), or numeric ID (deprecated)
#### --dev
Configure balenaOS to operate in development mode
#### --secureBoot
Configure balenaOS installer to opt-in secure boot and disk encryption
#### -d, --device DEVICE
device UUID
@ -2413,10 +2545,6 @@ supervisor cloud polling interval in minutes (e.g. for device variables)
custom key name assigned to generated provisioning api key
#### --provisioning-key-expiry-date PROVISIONING-KEY-EXPIRY-DATE
expiry date assigned to generated provisioning api key (format: YYYY-MM-DD)
## config inject &#60;file&#62;
Inject a 'config.json' file to a balenaOS image file or attached SD card or
@ -2543,7 +2671,7 @@ Check also the Preloading and Preregistering section of the balena CLI's advance
masterclass document:
https://www.balena.io/docs/learn/more/masterclasses/advanced-cli/#5-preloading-and-preregistering
Fleets may be specified by fleet name or slug. Fleet slugs are
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
the recommended option, as they are unique and unambiguous. Slugs can be
listed with the `balena fleets` command. Note that slugs may change if the
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -2551,7 +2679,9 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public fleet, or with fleets
from other balena accounts that you may be invited to join under any role.
For this reason, fleet names are especially discouraged in scripts (e.g. CI
environments).
environments). Numeric fleet IDs are deprecated because they consist of an
implementation detail of the balena backend. We intend to remove support for
numeric IDs at some point in the future.
Note that the this command requires Docker to be installed, as further detailed
in the balena CLI's installation instructions:
@ -2577,7 +2707,7 @@ the image file path
#### -f, --fleet FLEET
fleet name or slug (preferred)
fleet name, slug (preferred), or numeric ID (deprecated)
#### -c, --commit COMMIT
@ -2741,7 +2871,6 @@ Examples:
$ balena push myFleet
$ balena push myFleet --source <source directory>
$ balena push myFleet -s <source directory>
$ balena push myFleet --source <source directory> --note "this is the note for this release"
$ balena push myFleet --release-tag key1 "" key2 "value2 with spaces"
$ balena push myorg/myfleet
@ -2806,7 +2935,7 @@ used (usually $HOME/.balena/secrets.yml|.json)
Don't run a live session on this push. The filesystem will not be monitored,
and changes will not be synchronized to any running containers. Note that both
this flag and --detached are required to cause the process to end once the
this flag and --detached and required to cause the process to end once the
initial build has completed.
#### -d, --detached
@ -2858,10 +2987,6 @@ by the 'track latest' release policy but can be used through release pinning.
Draft releases can be marked as final through the API. Releases are created
as final by default unless this option is given.
#### --note NOTE
The notes for this release
# Settings
## settings
@ -2874,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;
@ -3041,7 +3174,7 @@ the type of device this build is for
#### -f, --fleet FLEET
fleet name or slug (preferred)
fleet name, slug (preferred), or numeric ID (deprecated)
#### -e, --emulated
@ -3051,6 +3184,10 @@ Use QEMU for ARM architecture emulation during the image build
Alternative Dockerfile name/path, relative to the source folder
#### --logs
No-op and deprecated since balena CLI v12.0.0. Build logs are now shown by default.
#### --nologs
Hide the image build log output (produce less verbose output)
@ -3223,7 +3360,6 @@ Examples:
$ balena deploy myFleet
$ balena deploy myorg/myfleet --build --source myBuildDir/
$ balena deploy myorg/myfleet --build --source myBuildDir/ --note "this is the note for this release"
$ balena deploy myorg/myfleet myRepo/myImage
$ balena deploy myFleet myRepo/myImage --release-tag key1 "" key2 "value2 with spaces"
@ -3231,7 +3367,7 @@ Examples:
#### FLEET
fleet name or slug (preferred)
fleet name, slug (preferred), or numeric ID (deprecated)
#### IMAGE
@ -3264,10 +3400,6 @@ by the 'track latest' release policy but can be used through release pinning.
Draft releases can be marked as final through the API. Releases are created
as final by default unless this option is given.
#### --note NOTE
The notes for this release
#### -e, --emulated
Use QEMU for ARM architecture emulation during the image build
@ -3276,6 +3408,10 @@ Use QEMU for ARM architecture emulation during the image build
Alternative Dockerfile name/path, relative to the source folder
#### --logs
No-op and deprecated since balena CLI v12.0.0. Build logs are now shown by default.
#### --nologs
Hide the image build log output (produce less verbose output)
@ -3369,7 +3505,7 @@ scan the local network for balenaOS devices and prompt you to select one
from an interactive picker. This may require administrator/root privileges.
Likewise, if the fleet option is not provided then a picker will be shown.
Fleets may be specified by fleet name or slug. Fleet slugs are
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
the recommended option, as they are unique and unambiguous. Slugs can be
listed with the `balena fleets` command. Note that slugs may change if the
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -3377,7 +3513,9 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public fleet, or with fleets
from other balena accounts that you may be invited to join under any role.
For this reason, fleet names are especially discouraged in scripts (e.g. CI
environments).
environments). Numeric fleet IDs are deprecated because they consist of an
implementation detail of the balena backend. We intend to remove support for
numeric IDs at some point in the future.
Examples:
@ -3398,7 +3536,7 @@ the IP or hostname of device
#### -f, --fleet FLEET
fleet name or slug (preferred)
fleet name, slug (preferred), or numeric ID (deprecated)
#### -i, --pollInterval POLLINTERVAL
@ -3454,7 +3592,7 @@ or hours, e.g. '12h', '2d'.
Both --device and --fleet flags accept multiple values, specified as
a comma-separated list (with no spaces).
Fleets may be specified by fleet name or slug. Fleet slugs are
Fleets may be specified by fleet name, slug, or numeric ID. Fleet slugs are
the recommended option, as they are unique and unambiguous. Slugs can be
listed with the `balena fleets` command. Note that slugs may change if the
fleet is renamed. Fleet names are not unique and may result in "Fleet is
@ -3462,7 +3600,9 @@ ambiguous" errors at any time (even if it "used to work in the past"), for
example if the name clashes with a newly created public fleet, or with fleets
from other balena accounts that you may be invited to join under any role.
For this reason, fleet names are especially discouraged in scripts (e.g. CI
environments).
environments). Numeric fleet IDs are deprecated because they consist of an
implementation detail of the balena backend. We intend to remove support for
numeric IDs at some point in the future.
Examples:

View File

@ -24,7 +24,6 @@ import {
} from './preparser';
import { CliSettings } from './utils/bootstrap';
import { onceAsync } from './utils/lazy';
import { run as mainRun } from '@oclif/core';
/**
* Sentry.io setup
@ -115,9 +114,10 @@ async function oclifRun(command: string[], options: AppOptions) {
}
const runPromise = (async function (shouldFlush: boolean) {
const { CustomMain } = await import('./utils/oclif-utils');
let isEEXIT = false;
try {
await mainRun(command, options.configPath);
await CustomMain.run(command);
} catch (error) {
// oclif sometimes exits with ExitError code EEXIT 0 (not an error),
// for example the `balena help` command.
@ -130,7 +130,7 @@ async function oclifRun(command: string[], options: AppOptions) {
}
}
if (shouldFlush) {
await import('@oclif/core/flush');
await import('@oclif/command/flush');
}
// TODO: figure out why we need to call fast-boot stop() here, in
// addition to calling it in the main `run()` function in this file.

View File

@ -15,7 +15,7 @@
* limitations under the License.
*/
import { Command } from '@oclif/core';
import Command from '@oclif/command';
import {
InsufficientPrivilegesError,
NotAvailableInOfflineModeError,
@ -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

@ -15,12 +15,20 @@
* limitations under the License.
*/
import { Args } from '@oclif/core';
import { flags } from '@oclif/command';
import Command from '../../command';
import { ExpectedError } from '../../errors';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
interface FlagsDef {
help: void;
}
interface ArgsDef {
name: string;
}
export default class GenerateCmd extends Command {
public static description = stripIndent`
Generate a new balenaCloud API key.
@ -33,23 +41,24 @@ export default class GenerateCmd extends Command {
`;
public static examples = ['$ balena api-key generate "Jenkins Key"'];
public static args = {
name: Args.string({
public static args = [
{
name: 'name',
description: 'the API key name',
required: true,
}),
};
},
];
public static usage = 'api-key generate <name>';
public static flags = {
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params } = await this.parse(GenerateCmd);
const { args: params } = this.parse<FlagsDef, ArgsDef>(GenerateCmd);
let key;
try {

View File

@ -1,71 +0,0 @@
/**
* @license
* Copyright 2016-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 { Args } from '@oclif/core';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
export default class RevokeCmd extends Command {
public static description = stripIndent`
Revoke balenaCloud API keys.
Revoke balenaCloud API keys with the given
comma-separated list of ids.
The given balenaCloud API keys will no longer be usable.
`;
public static examples = [
'$ balena api-key revoke 123',
'$ balena api-key revoke 123,124,456',
];
public static args = {
ids: Args.string({
description: 'the API key ids',
required: true,
}),
};
public static usage = 'api-key revoke <ids>';
public static flags = {
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params } = await this.parse(RevokeCmd);
try {
const apiKeyIds = params.ids.split(',');
if (apiKeyIds.filter((apiKeyId) => !apiKeyId.match(/^\d+$/)).length > 0) {
console.log('API key ids must be positive integers');
return;
}
await Promise.all(
apiKeyIds.map(
async (id) => await getBalenaSdk().models.apiKey.revoke(Number(id)),
),
);
console.log('Successfully revoked the given API keys');
} catch (e) {
throw e;
}
}
}

View File

@ -1,87 +0,0 @@
/**
* @license
* Copyright 2016-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 { Flags } from '@oclif/core';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
export default class ApiKeysCmd extends Command {
public static description = stripIndent`
Print a list of balenaCloud API keys.
Print a list of balenaCloud API keys.
Print a list of balenaCloud API keys for the current user or for a specific fleet with the \`--fleet\` option.
`;
public static examples = ['$ balena api-keys'];
public static usage = 'api-keys';
public static flags = {
help: cf.help,
user: Flags.boolean({
char: 'u',
description: 'show API keys for your user',
}),
fleet: cf.fleet,
};
public static authenticated = true;
public async run() {
const { flags: options } = await this.parse(ApiKeysCmd);
try {
const { getApplication } = await import('../../utils/sdk');
const actorId = options.fleet
? (
await getApplication(getBalenaSdk(), options.fleet, {
$select: 'actor',
})
).actor
: await getBalenaSdk().auth.getActorId();
const keys = await getBalenaSdk().pine.get({
resource: 'api_key',
options: {
$select: ['id', 'created_at', 'name', 'description', 'expiry_date'],
$filter: {
is_of__actor: actorId,
...(options.user
? {
name: {
$ne: null,
},
}
: {}),
},
$orderby: 'name asc',
},
});
const fields = ['id', 'name', 'created_at', 'description', 'expiry_date'];
const _ = await import('lodash');
console.log(
getVisuals().table.horizontal(
keys.map((key) => _.mapValues(key, (val) => val ?? 'N/a')),
fields,
),
);
} catch (e) {
throw e;
}
}
}

View File

@ -1,83 +0,0 @@
/**
* @license
* Copyright 2016-2021 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 { Flags, Args } from '@oclif/core';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { stripIndent } from '../../utils/lazy';
export default class AppCreateCmd extends Command {
public static description = stripIndent`
Create an app.
Create a new balena app.
You can specify the organization the app should belong to using
the \`--organization\` option. The organization's handle, not its name,
should be provided. Organization handles can be listed with the
\`balena orgs\` command.
The app's default device type is specified with the \`--type\` option.
The \`balena devices supported\` command can be used to list the available
device types.
Interactive dropdowns will be shown for selection if no device type or
organization is specified and there are multiple options to choose from.
If there is a single option to choose from, it will be chosen automatically.
This interactive behavior can be disabled by explicitly specifying a device
type and organization.
`;
public static examples = [
'$ balena app create MyApp',
'$ balena app create MyApp --organization mmyorg',
'$ balena app create MyApp -o myorg --type raspberry-pi',
];
public static args = {
name: Args.string({
description: 'app name',
required: true,
}),
};
public static usage = 'app create <name>';
public static flags = {
organization: Flags.string({
char: 'o',
description: 'handle of the organization the app should belong to',
}),
type: Flags.string({
char: 't',
description:
'app device type (Check available types with `balena devices supported`)',
}),
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params, flags: options } = await this.parse(AppCreateCmd);
await (
await import('../../utils/application-create')
).applicationCreateBase('app', options, params);
}
}

View File

@ -1,83 +0,0 @@
/**
* @license
* Copyright 2016-2021 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 { Flags, Args } from '@oclif/core';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { stripIndent } from '../../utils/lazy';
export default class BlockCreateCmd extends Command {
public static description = stripIndent`
Create an block.
Create a new balena block.
You can specify the organization the block should belong to using
the \`--organization\` option. The organization's handle, not its name,
should be provided. Organization handles can be listed with the
\`balena orgs\` command.
The block's default device type is specified with the \`--type\` option.
The \`balena devices supported\` command can be used to list the available
device types.
Interactive dropdowns will be shown for selection if no device type or
organization is specified and there are multiple options to choose from.
If there is a single option to choose from, it will be chosen automatically.
This interactive behavior can be disabled by explicitly specifying a device
type and organization.
`;
public static examples = [
'$ balena block create MyBlock',
'$ balena block create MyBlock --organization mmyorg',
'$ balena block create MyBlock -o myorg --type raspberry-pi',
];
public static args = {
name: Args.string({
description: 'block name',
required: true,
}),
};
public static usage = 'block create <name>';
public static flags = {
organization: Flags.string({
char: 'o',
description: 'handle of the organization the block should belong to',
}),
type: Flags.string({
char: 't',
description:
'block device type (Check available types with `balena devices supported`)',
}),
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params, flags: options } = await this.parse(BlockCreateCmd);
await (
await import('../../utils/application-create')
).applicationCreateBase('block', options, params);
}
}

View File

@ -15,12 +15,12 @@
* limitations under the License.
*/
import { Args, Flags } from '@oclif/core';
import { flags } from '@oclif/command';
import Command from '../command';
import { getBalenaSdk } from '../utils/lazy';
import * as cf from '../utils/common-flags';
import * as compose from '../utils/compose';
import type { ApplicationType, BalenaSDK } from 'balena-sdk';
import type { Application, ApplicationType, BalenaSDK } from 'balena-sdk';
import {
buildArgDeprecation,
dockerignoreHelp,
@ -31,9 +31,6 @@ import { buildProject, composeCliFlags } from '../utils/compose_ts';
import type { BuildOpts, DockerCliFlags } from '../utils/docker';
import { dockerCliFlags } from '../utils/docker';
// TODO: For this special one we can't use Interfaces.InferredFlags/InferredArgs
// because of the 'registry-secrets' type which is defined in the actual code
// as a path (string | undefined) but then the cli turns it into an object
interface FlagsDef extends ComposeCliFlags, DockerCliFlags {
arch?: string;
deviceType?: string;
@ -42,6 +39,10 @@ interface FlagsDef extends ComposeCliFlags, DockerCliFlags {
help: void;
}
interface ArgsDef {
source?: string;
}
export default class BuildCmd extends Command {
public static description = `\
Build a project locally.
@ -73,18 +74,21 @@ ${dockerignoreHelp}
'$ balena build --dockerHost my.docker.host --dockerPort 2376 --ca ca.pem --key key.pem --cert cert.pem -f myFleet',
];
public static args = {
source: Args.string({ description: 'path of project source directory' }),
};
public static args = [
{
name: 'source',
description: 'path of project source directory',
},
];
public static usage = 'build [source]';
public static flags = {
arch: Flags.string({
public static flags: flags.Input<FlagsDef> = {
arch: flags.string({
description: 'the architecture to build for',
char: 'A',
}),
deviceType: Flags.string({
deviceType: flags.string({
description: 'the type of device this build is for',
char: 'd',
}),
@ -93,13 +97,15 @@ ${dockerignoreHelp}
...dockerCliFlags,
// NOTE: Not supporting -h for help, because of clash with -h in DockerCliFlags
// Revisit this in future release.
help: Flags.help({}),
help: flags.help({}),
};
public static primary = true;
public async run() {
const { args: params, flags: options } = await this.parse(BuildCmd);
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
BuildCmd,
);
await Command.checkLoggedInIf(!!options.fleet);
@ -202,7 +208,7 @@ ${dockerignoreHelp}
* buildEmulated
* buildOpts: arguments to forward to docker build command
*
* @param {Dockerode} docker
* @param {DockerToolbelt} docker
* @param {Logger} logger
* @param {ComposeOpts} composeOpts
* @param opts
@ -212,9 +218,7 @@ ${dockerignoreHelp}
logger: import('../utils/logger'),
composeOpts: ComposeOpts,
opts: {
app?: {
application_type: [Pick<ApplicationType, 'supports_multicontainer'>];
};
app?: Application;
arch: string;
deviceType: string;
buildEmulated: boolean;
@ -230,7 +234,7 @@ ${dockerignoreHelp}
opts.buildOpts.t,
);
const appType = opts.app?.application_type?.[0];
const appType = (opts.app?.application_type as ApplicationType[])?.[0];
if (
appType != null &&
project.descriptors.length > 1 &&

View File

@ -15,17 +15,30 @@
* limitations under the License.
*/
import { Flags } from '@oclif/core';
import type { Interfaces } from '@oclif/core';
import { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, getCliForm, stripIndent } from '../../utils/lazy';
import {
applicationIdInfo,
devModeInfo,
secureBootInfo,
} from '../../utils/messages';
import type { BalenaSDK, PineDeferred } from 'balena-sdk';
import { applicationIdInfo, devModeInfo } from '../../utils/messages';
import type { PineDeferred } from 'balena-sdk';
interface FlagsDef {
version: string; // OS version
fleet?: string;
dev?: boolean; // balenaOS development variant
device?: string;
deviceApiKey?: string;
deviceType?: string;
'generate-device-api-key': boolean;
output?: string;
// Options for non-interactive configuration
network?: string;
wifiSsid?: string;
wifiKey?: string;
appUpdatePollInterval?: string;
'provisioning-key-name'?: string;
help: void;
}
export default class ConfigGenerateCmd extends Command {
public static description = stripIndent`
@ -37,8 +50,6 @@ export default class ConfigGenerateCmd extends Command {
${devModeInfo.split('\n').join('\n\t\t')}
${secureBootInfo.split('\n').join('\n\t\t')}
To configure an image for a fleet of mixed device types, use the --fleet option
alongside the --deviceType option to specify the target device type.
@ -54,7 +65,6 @@ export default class ConfigGenerateCmd extends Command {
'$ balena config generate --device 7cf02a6 --version 2.12.7 --deviceApiKey <existingDeviceKey>',
'$ balena config generate --device 7cf02a6 --version 2.12.7 --output config.json',
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --dev',
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --secureBoot',
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --deviceType fincm3',
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --output config.json',
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 15',
@ -62,95 +72,79 @@ export default class ConfigGenerateCmd extends Command {
public static usage = 'config generate';
public static flags = {
version: Flags.string({
public static flags: flags.Input<FlagsDef> = {
version: flags.string({
description: 'a balenaOS version',
required: true,
}),
fleet: { ...cf.fleet, exclusive: ['device'] },
dev: cf.dev,
secureBoot: cf.secureBoot,
device: {
...cf.device,
exclusive: [
'fleet',
'provisioning-key-name',
'provisioning-key-expiry-date',
],
exclusive: ['fleet', 'provisioning-key-name'],
},
deviceApiKey: Flags.string({
deviceApiKey: flags.string({
description:
'custom device key - note that this is only supported on balenaOS 2.0.3+',
char: 'k',
}),
deviceType: Flags.string({
deviceType: flags.string({
description:
"device type slug (run 'balena devices supported' for possible values)",
}),
'generate-device-api-key': Flags.boolean({
'generate-device-api-key': flags.boolean({
description: 'generate a fresh device key for the device',
}),
output: Flags.string({
output: flags.string({
description: 'path of output file',
char: 'o',
}),
// Options for non-interactive configuration
network: Flags.string({
network: flags.string({
description: 'the network type to use: ethernet or wifi',
options: ['ethernet', 'wifi'],
}),
wifiSsid: Flags.string({
wifiSsid: flags.string({
description:
'the wifi ssid to use (used only if --network is set to wifi)',
}),
wifiKey: Flags.string({
wifiKey: flags.string({
description:
'the wifi key to use (used only if --network is set to wifi)',
}),
appUpdatePollInterval: Flags.string({
appUpdatePollInterval: flags.string({
description:
'supervisor cloud polling interval in minutes (e.g. for device variables)',
}),
'provisioning-key-name': Flags.string({
'provisioning-key-name': flags.string({
description: 'custom key name assigned to generated provisioning api key',
exclusive: ['device'],
}),
'provisioning-key-expiry-date': Flags.string({
description:
'expiry date assigned to generated provisioning api key (format: YYYY-MM-DD)',
exclusive: ['device'],
}),
help: cf.help,
};
public static authenticated = true;
public async getApplication(balena: BalenaSDK, fleet: string) {
const { getApplication } = await import('../../utils/sdk');
return await getApplication(balena, fleet, {
$select: 'slug',
$expand: {
is_for__device_type: { $select: 'slug' },
},
});
}
public async run() {
const { flags: options } = await this.parse(ConfigGenerateCmd);
const { flags: options } = this.parse<FlagsDef, {}>(ConfigGenerateCmd);
const { getApplication } = await import('../../utils/sdk');
const balena = getBalenaSdk();
await this.validateOptions(options);
let resourceDeviceType: string;
let application: Awaited<ReturnType<typeof this.getApplication>> | null =
null;
let application: ApplicationWithDeviceType | null = null;
let device:
| (DeviceWithDeviceType & { belongs_to__application: PineDeferred })
| null = null;
if (options.device != null) {
const rawDevice = await balena.models.device.get(options.device, {
$expand: { is_of__device_type: { $select: 'slug' } },
});
const { tryAsInteger } = await import('../../utils/validation');
const rawDevice = await balena.models.device.get(
tryAsInteger(options.device),
{ $expand: { is_of__device_type: { $select: 'slug' } } },
);
if (!rawDevice.belongs_to__application) {
const { ExpectedError } = await import('../../errors');
throw new ExpectedError(stripIndent`
@ -163,40 +157,36 @@ export default class ConfigGenerateCmd extends Command {
resourceDeviceType = device.is_of__device_type[0].slug;
} else {
// Disambiguate application (if is a number, it could either be an ID or a numerical name)
application = await this.getApplication(balena, options.fleet!);
application = (await getApplication(balena, options.fleet!, {
$expand: {
is_for__device_type: { $select: 'slug' },
},
})) as ApplicationWithDeviceType;
resourceDeviceType = application.is_for__device_type[0].slug;
}
const deviceType = options.deviceType || resourceDeviceType;
const deviceManifest = await balena.models.device.getManifestBySlug(
deviceType,
);
// Check compatibility if application and deviceType provided
if (options.fleet && options.deviceType) {
const appDeviceManifest = await balena.models.device.getManifestBySlug(
resourceDeviceType,
);
const helpers = await import('../../utils/helpers');
if (
!(await helpers.areDeviceTypesCompatible(
resourceDeviceType,
deviceType,
))
!helpers.areDeviceTypesCompatible(appDeviceManifest, deviceManifest)
) {
const { ExpectedError } = await import('../../errors');
throw new ExpectedError(
throw new balena.errors.BalenaInvalidDeviceType(
`Device type ${options.deviceType} is incompatible with fleet ${options.fleet}`,
);
}
}
const deviceManifest =
await balena.models.config.getDeviceTypeManifestBySlug(deviceType);
const { validateSecureBootOptionAndWarn } = await import(
'../../utils/config'
);
await validateSecureBootOptionAndWarn(
options.secureBoot,
deviceType,
options.version,
);
// Prompt for values
// Pass params as an override: if there is any param with exactly the same name as a
// required option, that value is used (and the corresponding question is not asked)
@ -205,9 +195,7 @@ export default class ConfigGenerateCmd extends Command {
});
answers.version = options.version;
answers.developmentMode = options.dev;
answers.secureBoot = options.secureBoot;
answers.provisioningKeyName = options['provisioning-key-name'];
answers.provisioningKeyExpiryDate = options['provisioning-key-expiry-date'];
// Generate config
const { generateDeviceConfig, generateApplicationConfig } = await import(
@ -247,9 +235,7 @@ export default class ConfigGenerateCmd extends Command {
protected readonly deviceTypeNotAllowedMessage =
'The --deviceType option can only be used alongside the --fleet option';
protected async validateOptions(
options: Interfaces.InferredFlags<typeof ConfigGenerateCmd.flags>,
) {
protected async validateOptions(options: FlagsDef) {
const { ExpectedError } = await import('../../errors');
if (options.device == null && options.fleet == null) {

View File

@ -15,11 +15,21 @@
* limitations under the License.
*/
import { Args } from '@oclif/core';
import { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getVisuals, stripIndent } from '../../utils/lazy';
interface FlagsDef {
type?: string;
drive?: string;
help: void;
}
interface ArgsDef {
file: string;
}
export default class ConfigInjectCmd extends Command {
public static description = stripIndent`
Inject a config.json file to a balenaOS image or attached media.
@ -36,16 +46,18 @@ export default class ConfigInjectCmd extends Command {
'$ balena config inject my/config.json --drive /dev/disk2',
];
public static args = {
file: Args.string({
public static args = [
{
name: 'file',
description: 'the path to the config.json file to inject',
required: true,
}),
};
},
];
public static usage = 'config inject <file>';
public static flags = {
public static flags: flags.Input<FlagsDef> = {
...cf.deviceTypeIgnored,
drive: cf.driveOrImg,
help: cf.help,
};
@ -54,7 +66,9 @@ export default class ConfigInjectCmd extends Command {
public static offlineCompatible = true;
public async run() {
const { args: params, flags: options } = await this.parse(ConfigInjectCmd);
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
ConfigInjectCmd,
);
const { safeUmount } = await import('../../utils/umount');

View File

@ -15,10 +15,18 @@
* limitations under the License.
*/
import { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getVisuals, stripIndent } from '../../utils/lazy';
interface FlagsDef {
type?: string;
drive?: string;
help: void;
json: boolean;
}
export default class ConfigReadCmd extends Command {
public static description = stripIndent`
Read the config.json file of a balenaOS image or attached media.
@ -38,7 +46,8 @@ export default class ConfigReadCmd extends Command {
public static usage = 'config read';
public static flags = {
public static flags: flags.Input<FlagsDef> = {
...cf.deviceTypeIgnored,
drive: cf.driveOrImg,
help: cf.help,
json: cf.json,
@ -48,7 +57,7 @@ export default class ConfigReadCmd extends Command {
public static offlineCompatible = true;
public async run() {
const { flags: options } = await this.parse(ConfigReadCmd);
const { flags: options } = this.parse<FlagsDef, {}>(ConfigReadCmd);
const { safeUmount } = await import('../../utils/umount');

View File

@ -15,11 +15,19 @@
* limitations under the License.
*/
import { Flags } from '@oclif/core';
import { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getVisuals, stripIndent } from '../../utils/lazy';
interface FlagsDef {
type?: string;
drive?: string;
advanced: boolean;
help: void;
version?: string;
}
export default class ConfigReconfigureCmd extends Command {
public static description = stripIndent`
Interactively reconfigure a balenaOS image file or attached media.
@ -41,14 +49,15 @@ export default class ConfigReconfigureCmd extends Command {
public static usage = 'config reconfigure';
public static flags = {
public static flags: flags.Input<FlagsDef> = {
...cf.deviceTypeIgnored,
drive: cf.driveOrImg,
advanced: Flags.boolean({
advanced: flags.boolean({
description: 'show advanced commands',
char: 'v',
}),
help: cf.help,
version: Flags.string({
version: flags.string({
description: 'balenaOS version, for example "2.32.0" or "2.44.0+rev1"',
}),
};
@ -57,7 +66,7 @@ export default class ConfigReconfigureCmd extends Command {
public static root = true;
public async run() {
const { flags: options } = await this.parse(ConfigReconfigureCmd);
const { flags: options } = this.parse<FlagsDef, {}>(ConfigReconfigureCmd);
const { safeUmount } = await import('../../utils/umount');

View File

@ -15,11 +15,22 @@
* limitations under the License.
*/
import { Args } from '@oclif/core';
import { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getVisuals, stripIndent } from '../../utils/lazy';
interface FlagsDef {
type?: string;
drive?: string;
help: void;
}
interface ArgsDef {
key: string;
value: string;
}
export default class ConfigWriteCmd extends Command {
public static description = stripIndent`
Write a key-value pair to the config.json file of an OS image or attached media.
@ -37,20 +48,23 @@ export default class ConfigWriteCmd extends Command {
'$ balena config write --drive balena.img os.network.connectivity.interval 300',
];
public static args = {
key: Args.string({
public static args = [
{
name: 'key',
description: 'the key of the config parameter to write',
required: true,
}),
value: Args.string({
},
{
name: 'value',
description: 'the value of the config parameter to write',
required: true,
}),
};
},
];
public static usage = 'config write <key> <value>';
public static flags = {
public static flags: flags.Input<FlagsDef> = {
...cf.deviceTypeIgnored,
drive: cf.driveOrImg,
help: cf.help,
};
@ -59,7 +73,9 @@ export default class ConfigWriteCmd extends Command {
public static offlineCompatible = true;
public async run() {
const { args: params, flags: options } = await this.parse(ConfigWriteCmd);
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
ConfigWriteCmd,
);
const { denyMount, safeUmount } = await import('../../utils/umount');

View File

@ -15,8 +15,8 @@
* limitations under the License.
*/
import { Args, Flags } from '@oclif/core';
import type { ImageDescriptor } from '@balena/compose/dist/parse';
import { flags } from '@oclif/command';
import type { ImageDescriptor } from 'resin-compose-parse';
import Command from '../command';
import { ExpectedError } from '../errors';
@ -43,28 +43,31 @@ import {
parseReleaseTagKeysAndValues,
} from '../utils/compose_ts';
import { dockerCliFlags } from '../utils/docker';
import type { ApplicationType, DeviceType, Release } from 'balena-sdk';
import type {
Application,
ApplicationType,
DeviceType,
Release,
} from 'balena-sdk';
interface ApplicationWithArch {
id: number;
interface ApplicationWithArch extends Application {
arch: string;
is_for__device_type: [Pick<DeviceType, 'slug'>];
application_type: [Pick<ApplicationType, 'slug' | 'supports_multicontainer'>];
}
// TODO: For this special one we can't use Interfaces.InferredFlags/InferredArgs
// because of the 'registry-secrets' type which is defined in the actual code
// as a path (string | undefined) but then the cli turns it into an object
interface FlagsDef extends ComposeCliFlags, DockerCliFlags {
source?: string;
build: boolean;
nologupload: boolean;
'release-tag'?: string[];
draft: boolean;
note?: string;
help: void;
}
interface ArgsDef {
fleet: string;
image?: string;
}
export default class DeployCmd extends Command {
public static description = `\
Deploy a single image or a multicontainer project to a balena fleet.
@ -98,33 +101,35 @@ ${dockerignoreHelp}
public static examples = [
'$ balena deploy myFleet',
'$ balena deploy myorg/myfleet --build --source myBuildDir/',
'$ balena deploy myorg/myfleet --build --source myBuildDir/ --note "this is the note for this release"',
'$ balena deploy myorg/myfleet myRepo/myImage',
'$ balena deploy myFleet myRepo/myImage --release-tag key1 "" key2 "value2 with spaces"',
];
public static args = {
fleet: ca.fleetRequired,
image: Args.string({ description: 'the image to deploy' }),
};
public static args = [
ca.fleetRequired,
{
name: 'image',
description: 'the image to deploy',
},
];
public static usage = 'deploy <fleet> [image]';
public static flags = {
source: Flags.string({
public static flags: flags.Input<FlagsDef> = {
source: flags.string({
description:
'specify an alternate source directory; default is the working directory',
char: 's',
}),
build: Flags.boolean({
build: flags.boolean({
description: 'force a rebuild before deploy',
char: 'b',
}),
nologupload: Flags.boolean({
nologupload: flags.boolean({
description:
"don't upload build logs to the dashboard with image (if building)",
}),
'release-tag': Flags.string({
'release-tag': flags.string({
description: stripIndent`
Set release tags if the image deployment is successful. Multiple
arguments may be provided, alternating tag keys and values (see examples).
@ -132,7 +137,7 @@ ${dockerignoreHelp}
`,
multiple: true,
}),
draft: Flags.boolean({
draft: flags.boolean({
description: stripIndent`
Deploy the release as a draft. Draft releases are ignored
by the 'track latest' release policy but can be used through release pinning.
@ -140,12 +145,11 @@ ${dockerignoreHelp}
as final by default unless this option is given.`,
default: false,
}),
note: Flags.string({ description: 'The notes for this release' }),
...composeCliFlags,
...dockerCliFlags,
// NOTE: Not supporting -h for help, because of clash with -h in DockerCliFlags
// Revisit this in future release.
help: Flags.help({}),
help: flags.help({}),
};
public static authenticated = true;
@ -153,7 +157,9 @@ ${dockerignoreHelp}
public static primary = true;
public async run() {
const { args: params, flags: options } = await this.parse(DeployCmd);
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
DeployCmd,
);
(await import('events')).defaultMaxListeners = 1000;
@ -183,7 +189,7 @@ ${dockerignoreHelp}
);
if (image) {
(options as FlagsDef)['registry-secrets'] = await getRegistrySecrets(
options['registry-secrets'] = await getRegistrySecrets(
sdk,
options['registry-secrets'],
);
@ -196,7 +202,7 @@ ${dockerignoreHelp}
registrySecretsPath: options['registry-secrets'],
});
options.dockerfile = dockerfilePath;
(options as FlagsDef)['registry-secrets'] = registrySecrets;
options['registry-secrets'] = registrySecrets;
}
const helpers = await import('../utils/helpers');
@ -205,7 +211,7 @@ ${dockerignoreHelp}
const dockerUtils = await import('../utils/docker');
const [docker, buildOpts, composeOpts] = await Promise.all([
dockerUtils.getDocker(options),
dockerUtils.generateBuildOpts(options as FlagsDef),
dockerUtils.generateBuildOpts(options),
compose.generateOpts(options),
]);
@ -225,9 +231,6 @@ ${dockerignoreHelp}
releaseTagKeys,
releaseTagValues,
);
if (options.note) {
await sdk.models.release.setNote(release.id, options.note);
}
}
async deployProject(
@ -253,7 +256,7 @@ ${dockerignoreHelp}
'../utils/compose_ts'
);
const appType = opts.app.application_type[0];
const appType = (opts.app?.application_type as ApplicationType[])?.[0];
try {
const project = await loadProject(
@ -310,7 +313,7 @@ ${dockerignoreHelp}
projectName: project.name,
composition: compositionToBuild,
arch: opts.app.arch,
deviceType: opts.app.is_for__device_type[0].slug,
deviceType: (opts.app?.is_for__device_type as DeviceType[])?.[0].slug,
emulated: opts.buildEmulated,
buildOpts: opts.buildOpts,
inlineLogs: composeOpts.inlineLogs,
@ -331,7 +334,7 @@ ${dockerignoreHelp}
);
let release: Release | ComposeReleaseInfo['release'];
if (appType.slug === 'legacy-v1' || appType.slug === 'legacy-v2') {
if (appType?.is_legacy) {
const { deployLegacy } = require('../utils/deploy-legacy');
const msg = getChalk().yellow(
@ -339,9 +342,9 @@ ${dockerignoreHelp}
);
logger.logWarn(msg);
const [token, { username }, url, options] = await Promise.all([
const [token, username, url, options] = await Promise.all([
sdk.auth.getToken(),
sdk.auth.getUserInfo(),
sdk.auth.whoami(),
sdk.settings.get('balenaUrl'),
{
// opts.appName may be prefixed by 'owner/', unlike opts.app.app_name
@ -364,8 +367,8 @@ ${dockerignoreHelp}
$select: ['commit'],
});
} else {
const [{ id: userId }, auth, apiEndpoint] = await Promise.all([
sdk.auth.getUserInfo(),
const [userId, auth, apiEndpoint] = await Promise.all([
sdk.auth.getUserId(),
sdk.auth.getToken(),
sdk.settings.get('apiUrl'),
]);

View File

@ -15,11 +15,21 @@
* limitations under the License.
*/
import { Args } from '@oclif/core';
import { flags } from '@oclif/command';
import type { IArg } from '@oclif/parser/lib/args';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
interface FlagsDef {
yes: boolean;
help: void;
}
interface ArgsDef {
uuid: string;
}
export default class DeviceDeactivateCmd extends Command {
public static description = stripIndent`
Deactivate a device.
@ -34,16 +44,17 @@ export default class DeviceDeactivateCmd extends Command {
'$ balena device deactivate 7cf02a6 --yes',
];
public static args = {
uuid: Args.string({
public static args: Array<IArg<any>> = [
{
name: 'uuid',
description: 'the UUID of the device to be deactivated',
required: true,
}),
};
},
];
public static usage = 'device deactivate <uuid>';
public static flags = {
public static flags: flags.Input<FlagsDef> = {
yes: cf.yes,
help: cf.help,
};
@ -51,7 +62,7 @@ export default class DeviceDeactivateCmd extends Command {
public static authenticated = true;
public async run() {
const { args: params, flags: options } = await this.parse(
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
DeviceDeactivateCmd,
);

View File

@ -15,12 +15,22 @@
* limitations under the License.
*/
import { Args } from '@oclif/core';
import { flags } from '@oclif/command';
import type { IArg } from '@oclif/parser/lib/args';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { tryAsInteger } from '../../utils/validation';
import { ExpectedError } from '../../errors';
interface FlagsDef {
help: void;
}
interface ArgsDef {
uuid: string;
}
export default class DeviceIdentifyCmd extends Command {
public static description = stripIndent`
Identify a device.
@ -29,23 +39,25 @@ export default class DeviceIdentifyCmd extends Command {
`;
public static examples = ['$ balena device identify 23c73a1'];
public static args = {
uuid: Args.string({
public static args: Array<IArg<any>> = [
{
name: 'uuid',
description: 'the uuid of the device to identify',
parse: (dev) => tryAsInteger(dev),
required: true,
}),
};
},
];
public static usage = 'device identify <uuid>';
public static flags = {
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params } = await this.parse(DeviceIdentifyCmd);
const { args: params } = this.parse<FlagsDef, ArgsDef>(DeviceIdentifyCmd);
const balena = getBalenaSdk();

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.
@ -15,13 +15,17 @@
* limitations under the License.
*/
import { Flags, Args } from '@oclif/core';
import { flags } from '@oclif/command';
import { IArg } from '@oclif/parser/lib/args';
import Command from '../../command';
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;
@ -40,39 +44,45 @@ interface ExtendedDevice extends DeviceWithDeviceType {
undervoltage_detected?: boolean;
}
interface FlagsDef extends DataOutputOptions {
help: void;
}
interface ArgsDef {
uuid: string;
}
export default class DeviceCmd extends Command {
public static description = stripIndent`
Show info about a single device.
Show information about a single device.
`;
public static examples = [
'$ balena device 7cf02a6',
'$ balena device 7cf02a6 --view',
];
public static examples = ['$ balena device 7cf02a6'];
public static args = {
uuid: Args.string({
public static args: Array<IArg<any>> = [
{
name: 'uuid',
description: 'the device uuid',
parse: (dev) => tryAsInteger(dev),
required: true,
}),
};
},
];
public static usage = 'device <uuid>';
public static flags = {
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
view: Flags.boolean({
default: false,
description: 'open device dashboard page',
}),
...(isV14() ? cf.dataOutputFlags : {}),
};
public static authenticated = true;
public static primary = true;
public async run() {
const { args: params, flags: options } = await this.parse(DeviceCmd);
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
DeviceCmd,
);
const balena = getBalenaSdk();
@ -103,14 +113,6 @@ export default class DeviceCmd extends Command {
],
...expandForAppName,
})) as ExtendedDevice;
if (options.view) {
const open = await import('open');
const dashboardUrl = balena.models.device.getDashboardUrl(device.uuid);
await open(dashboardUrl, { wait: false });
return;
}
device.status = device.overall_status;
device.dashboard_url = balena.models.device.getDashboardUrl(device.uuid);
@ -166,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

@ -15,7 +15,7 @@
* limitations under the License.
*/
import { Flags } from '@oclif/core';
import { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
@ -31,7 +31,6 @@ interface FlagsDef {
config?: string;
help: void;
'provisioning-key-name'?: string;
'provisioning-key-expiry-date'?: string;
}
export default class DeviceInitCmd extends Command {
@ -70,20 +69,19 @@ export default class DeviceInitCmd extends Command {
public static examples = [
'$ balena device init',
'$ balena device init -f myorg/myfleet',
'$ balena device init --fleet myFleet --os-version 2.101.7 --drive /dev/disk5 --config config.json --yes',
'$ balena device init --fleet myFleet --os-version 2.83.21+rev1.prod --drive /dev/disk5 --config config.json --yes',
];
public static usage = 'device init';
public static flags = {
public static flags: flags.Input<FlagsDef> = {
fleet: cf.fleet,
yes: cf.yes,
advanced: Flags.boolean({
advanced: flags.boolean({
char: 'v',
description: 'show advanced configuration options',
}),
'os-version': Flags.string({
'os-version': flags.string({
description: stripIndent`
exact version number, or a valid semver range,
or 'latest' (includes pre-releases),
@ -93,23 +91,19 @@ export default class DeviceInitCmd extends Command {
`,
}),
drive: cf.drive,
config: Flags.string({
config: flags.string({
description: 'path to the config JSON file, see `balena os build-config`',
}),
'provisioning-key-name': Flags.string({
'provisioning-key-name': flags.string({
description: 'custom key name assigned to generated provisioning api key',
}),
'provisioning-key-expiry-date': Flags.string({
description:
'expiry date assigned to generated provisioning api key (format: YYYY-MM-DD)',
}),
help: cf.help,
};
public static authenticated = true;
public async run() {
const { flags: options } = await this.parse(DeviceInitCmd);
const { flags: options } = this.parse<FlagsDef, {}>(DeviceInitCmd);
// Imports
const { promisify } = await import('util');
@ -124,16 +118,20 @@ export default class DeviceInitCmd extends Command {
const balena = getBalenaSdk();
// Get application and
const application = options.fleet
? await getApplication(balena, options.fleet, {
$select: ['id', 'slug'],
$expand: {
is_for__device_type: {
$select: 'slug',
},
const application = (await getApplication(
balena,
options.fleet ||
(
await (await import('../../utils/patterns')).selectApplication()
).id,
{
$expand: {
is_for__device_type: {
$select: 'slug',
},
})
: await (await import('../../utils/patterns')).selectApplication();
},
},
)) as ApplicationWithDeviceType;
// Register new device
const deviceUuid = balena.models.device.generateUniqueKey();
@ -187,14 +185,6 @@ export default class DeviceInitCmd extends Command {
options['provisioning-key-name'],
);
}
if (options['provisioning-key-expiry-date']) {
configureCommand.push(
'--provisioning-key-expiry-date',
options['provisioning-key-expiry-date'],
);
}
await runCommand(configureCommand);
}

View File

@ -15,10 +15,23 @@
* limitations under the License.
*/
import { Flags, Args } from '@oclif/core';
import { flags } from '@oclif/command';
import type { IArg } from '@oclif/parser/lib/args';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { tryAsInteger } from '../../utils/validation';
interface FlagsDef {
enable: boolean;
disable: boolean;
status: boolean;
help?: void;
}
interface ArgsDef {
uuid: string | number;
}
export default class DeviceLocalModeCmd extends Command {
public static description = stripIndent`
@ -35,25 +48,27 @@ export default class DeviceLocalModeCmd extends Command {
'$ balena device local-mode 23c73a1 --status',
];
public static args = {
uuid: Args.string({
public static args: Array<IArg<any>> = [
{
name: 'uuid',
description: 'the uuid of the device to manage',
parse: (dev) => tryAsInteger(dev),
required: true,
}),
};
},
];
public static usage = 'device local-mode <uuid>';
public static flags = {
enable: Flags.boolean({
public static flags: flags.Input<FlagsDef> = {
enable: flags.boolean({
description: 'enable local mode',
exclusive: ['disable', 'status'],
}),
disable: Flags.boolean({
disable: flags.boolean({
description: 'disable local mode',
exclusive: ['enable', 'status'],
}),
status: Flags.boolean({
status: flags.boolean({
description: 'output boolean indicating local mode status',
exclusive: ['enable', 'disable'],
}),
@ -63,7 +78,7 @@ export default class DeviceLocalModeCmd extends Command {
public static authenticated = true;
public async run() {
const { args: params, flags: options } = await this.parse(
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
DeviceLocalModeCmd,
);

View File

@ -15,11 +15,12 @@
* limitations under the License.
*/
import { Args } from '@oclif/core';
import type { flags } from '@oclif/command';
import type { IArg } from '@oclif/parser/lib/args';
import type {
BalenaSDK,
Device,
PineOptions,
DeviceType,
PineTypedResult,
} from 'balena-sdk';
import Command from '../../command';
@ -28,6 +29,22 @@ import { ExpectedError } from '../../errors';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { applicationIdInfo } from '../../utils/messages';
type ExtendedDevice = PineTypedResult<
Device,
typeof import('../../utils/helpers').expandForAppNameAndCpuArch
> & {
application_name?: string;
};
interface FlagsDef {
fleet?: string;
help: void;
}
interface ArgsDef {
uuid: string;
}
export default class DeviceMoveCmd extends Command {
public static description = stripIndent`
Move one or more devices to another fleet.
@ -46,70 +63,68 @@ export default class DeviceMoveCmd extends Command {
'$ balena device move 7cf02a6 -f myorg/mynewfleet',
];
public static args = {
uuid: Args.string({
public static args: Array<IArg<any>> = [
{
name: 'uuid',
description:
'comma-separated list (no blank spaces) of device UUIDs to be moved',
required: true,
}),
};
},
];
public static usage = 'device move <uuid(s)>';
public static flags = {
public static flags: flags.Input<FlagsDef> = {
fleet: cf.fleet,
help: cf.help,
};
public static authenticated = true;
private async getDevices(balena: BalenaSDK, deviceUuids: string[]) {
const deviceOptions = {
$select: 'belongs_to__application',
$expand: {
is_of__device_type: {
$select: 'is_of__cpu_architecture',
$expand: {
is_of__cpu_architecture: {
$select: 'slug',
},
},
},
},
} satisfies PineOptions<Device>;
// TODO: Refacor once `device.get()` accepts an array of uuids`
const devices = await Promise.all(
deviceUuids.map(
(uuid) =>
balena.models.device.get(uuid, deviceOptions) as Promise<
PineTypedResult<Device, typeof deviceOptions>
>,
),
);
return devices;
}
public async run() {
const { args: params, flags: options } = await this.parse(DeviceMoveCmd);
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
DeviceMoveCmd,
);
const balena = getBalenaSdk();
// Split uuids string into array of uuids
const deviceUuids = params.uuid.split(',');
const { tryAsInteger } = await import('../../utils/validation');
const { expandForAppNameAndCpuArch } = await import('../../utils/helpers');
const devices = await this.getDevices(balena, deviceUuids);
// Parse ids string into array of correct types
const deviceIds: Array<string | number> = params.uuid
.split(',')
.map((id) => tryAsInteger(id));
// Disambiguate application
// Get devices
const devices = await Promise.all(
deviceIds.map(
(uuid) =>
balena.models.device.get(
uuid,
expandForAppNameAndCpuArch,
) as Promise<ExtendedDevice>,
),
);
// Map application name for each device
for (const device of devices) {
const belongsToApplication = device.belongs_to__application;
device.application_name = belongsToApplication?.[0]
? belongsToApplication[0].app_name
: 'N/a';
}
// Disambiguate application (if is a number, it could either be an ID or a numerical name)
const { getApplication } = await import('../../utils/sdk');
// Get destination application
const application = options.fleet
? await getApplication(balena, options.fleet, { $select: ['id', 'slug'] })
? await getApplication(balena, options.fleet)
: await this.interactivelySelectApplication(balena, devices);
// Move each device
for (const uuid of deviceUuids) {
for (const uuid of deviceIds) {
try {
await balena.models.device.move(uuid, application.id);
console.info(`Device ${uuid} was moved to fleet ${application.slug}`);
@ -122,8 +137,9 @@ export default class DeviceMoveCmd extends Command {
async interactivelySelectApplication(
balena: BalenaSDK,
devices: Awaited<ReturnType<typeof this.getDevices>>,
devices: ExtendedDevice[],
) {
const { getExpandedProp } = await import('../../utils/pine');
// deduplicate the slugs
const deviceCpuArchs = Array.from(
new Set(
@ -133,44 +149,46 @@ export default class DeviceMoveCmd extends Command {
),
);
const allCpuArches = await balena.pine.get({
resource: 'cpu_architecture',
options: {
$select: ['id', 'slug'],
const deviceTypeOptions = {
$select: 'slug',
$expand: {
is_of__cpu_architecture: {
$select: 'slug',
},
},
});
} as const;
const deviceTypes = (await balena.models.deviceType.getAllSupported(
deviceTypeOptions,
)) as Array<PineTypedResult<DeviceType, typeof deviceTypeOptions>>;
const compatibleCpuArchIds = allCpuArches
.filter((cpuArch) => {
return deviceCpuArchs.every((deviceCpuArch) =>
balena.models.os.isArchitectureCompatibleWith(
deviceCpuArch,
cpuArch.slug,
),
);
})
.map((deviceType) => deviceType.id);
const compatibleDeviceTypeSlugs = new Set(
deviceTypes
.filter((deviceType) => {
const deviceTypeArch = getExpandedProp(
deviceType.is_of__cpu_architecture,
'slug',
)!;
return deviceCpuArchs.every((deviceCpuArch) =>
balena.models.os.isArchitectureCompatibleWith(
deviceCpuArch,
deviceTypeArch,
),
);
})
.map((deviceType) => deviceType.slug),
);
const patterns = await import('../../utils/patterns');
try {
const application = await patterns.selectApplication(
{
is_for__device_type: {
$any: {
$alias: 'dt',
$expr: {
dt: {
is_of__cpu_architecture: { $in: compatibleCpuArchIds },
},
},
},
},
},
(app) =>
compatibleDeviceTypeSlugs.has(app.is_for__device_type[0].slug) &&
devices.some((device) => device.application_name !== app.app_name),
true,
);
return application;
} catch (err) {
if (!compatibleCpuArchIds.length) {
if (!compatibleDeviceTypeSlugs.size) {
throw new ExpectedError(
`${err.message}\nDo all devices have a compatible architecture?`,
);

View File

@ -15,13 +15,25 @@
* limitations under the License.
*/
import { Flags, Args } from '@oclif/core';
import { flags } from '@oclif/command';
import type { IArg } from '@oclif/parser/lib/args';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy';
import { tryAsInteger } from '../../utils/validation';
import type { Device } from 'balena-sdk';
import { ExpectedError } from '../../errors';
interface FlagsDef {
version?: string;
yes: boolean;
help: void;
}
interface ArgsDef {
uuid: string;
}
export default class DeviceOsUpdateCmd extends Command {
public static description = stripIndent`
Start a Host OS update for a device.
@ -35,21 +47,22 @@ export default class DeviceOsUpdateCmd extends Command {
`;
public static examples = [
'$ balena device os-update 23c73a1',
'$ balena device os-update 23c73a1 --version 2.101.7',
'$ balena device os-update 23c73a1 --version 2.31.0+rev1.prod',
];
public static args = {
uuid: Args.string({
public static args: Array<IArg<any>> = [
{
name: 'uuid',
description: 'the uuid of the device to update',
parse: (dev) => tryAsInteger(dev),
required: true,
}),
};
},
];
public static usage = 'device os-update <uuid>';
public static flags = {
version: Flags.string({
public static flags: flags.Input<FlagsDef> = {
version: flags.string({
description: 'a balenaOS version',
}),
yes: cf.yes,
@ -59,7 +72,7 @@ export default class DeviceOsUpdateCmd extends Command {
public static authenticated = true;
public async run() {
const { args: params, flags: options } = await this.parse(
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
DeviceOsUpdateCmd,
);

View File

@ -1,91 +0,0 @@
/**
* @license
* Copyright 2016-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 { Args } from '@oclif/core';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { getExpandedProp } from '../../utils/pine';
export default class DevicePinCmd extends Command {
public static description = stripIndent`
Pin a device to a release.
Pin a device to a release.
Note, if the commit is omitted, the currently pinned release will be printed, with instructions for how to see a list of releases
`;
public static examples = [
'$ balena device pin 7cf02a6',
'$ balena device pin 7cf02a6 91165e5',
];
public static args = {
uuid: Args.string({
description: 'the uuid of the device to pin to a release',
required: true,
}),
releaseToPinTo: Args.string({
description: 'the commit of the release for the device to get pinned to',
}),
};
public static usage = 'device pin <uuid> [releaseToPinTo]';
public static flags = {
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params } = await this.parse(DevicePinCmd);
const balena = getBalenaSdk();
const device = await balena.models.device.get(params.uuid, {
$expand: {
should_be_running__release: {
$select: 'commit',
},
belongs_to__application: {
$select: 'slug',
},
},
});
const pinnedRelease = getExpandedProp(
device.should_be_running__release,
'commit',
);
const appSlug = getExpandedProp(device.belongs_to__application, 'slug');
const releaseToPinTo = params.releaseToPinTo;
if (!releaseToPinTo) {
console.log(
`${
pinnedRelease
? `This device is currently pinned to ${pinnedRelease}.`
: 'This device is not currently pinned to any release.'
} \n\nTo see a list of all releases this device can be pinned to, run \`balena releases ${appSlug}\`.`,
);
} else {
await balena.models.device.pinToRelease(params.uuid, releaseToPinTo);
}
}
}

View File

@ -15,11 +15,26 @@
* limitations under the License.
*/
import { Flags, Args } from '@oclif/core';
import { flags } from '@oclif/command';
import type { IArg } from '@oclif/parser/lib/args';
import Command from '../../command';
import { ExpectedError } from '../../errors';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { tryAsInteger } from '../../utils/validation';
interface FlagsDef {
enable: boolean;
disable: boolean;
status: boolean;
help?: void;
}
interface ArgsDef {
uuid: string;
// Optional hidden arg to support old command format
legacyUuid?: string;
}
export default class DevicePublicUrlCmd extends Command {
public static description = stripIndent`
@ -28,6 +43,9 @@ export default class DevicePublicUrlCmd extends Command {
This command will output the current public URL for the
specified device. It can also enable or disable the URL,
or output the enabled status, using the respective options.
The old command style 'balena device public-url enable <uuid>'
is deprecated, but still supported.
`;
public static examples = [
@ -37,25 +55,33 @@ export default class DevicePublicUrlCmd extends Command {
'$ balena device public-url 23c73a1 --status',
];
public static args = {
uuid: Args.string({
public static args: Array<IArg<any>> = [
{
name: 'uuid',
description: 'the uuid of the device to manage',
parse: (dev) => tryAsInteger(dev),
required: true,
}),
};
},
{
// Optional hidden arg to support old command format
name: 'legacyUuid',
parse: (dev) => tryAsInteger(dev),
hidden: true,
},
];
public static usage = 'device public-url <uuid>';
public static flags = {
enable: Flags.boolean({
public static flags: flags.Input<FlagsDef> = {
enable: flags.boolean({
description: 'enable the public URL',
exclusive: ['disable', 'status'],
}),
disable: Flags.boolean({
disable: flags.boolean({
description: 'disable the public URL',
exclusive: ['enable', 'status'],
}),
status: Flags.boolean({
status: flags.boolean({
description: 'determine if public URL is enabled',
exclusive: ['enable', 'disable'],
}),
@ -65,10 +91,29 @@ export default class DevicePublicUrlCmd extends Command {
public static authenticated = true;
public async run() {
const { args: params, flags: options } = await this.parse(
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
DevicePublicUrlCmd,
);
// Legacy command format support.
// Previously this command used the following format
// (changed due to oclif technicalities):
// `balena device public-url enable|disable|status <uuid>`
if (params.legacyUuid) {
const action = params.uuid;
if (!['enable', 'disable', 'status'].includes(action)) {
throw new ExpectedError(
`Unexpected arguments: ${params.uuid} ${params.legacyUuid}`,
);
}
options.enable = action === 'enable';
options.disable = action === 'disable';
options.status = action === 'status';
params.uuid = params.legacyUuid;
delete params.legacyUuid;
}
const balena = getBalenaSdk();
if (options.enable) {

View File

@ -15,11 +15,20 @@
* limitations under the License.
*/
import { Args } from '@oclif/core';
import { flags } from '@oclif/command';
import type { IArg } from '@oclif/parser/lib/args';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, getCliUx, stripIndent } from '../../utils/lazy';
interface FlagsDef {
help: void;
}
interface ArgsDef {
uuid: string;
}
export default class DevicePurgeCmd extends Command {
public static description = stripIndent`
Purge data from a device.
@ -37,30 +46,34 @@ export default class DevicePurgeCmd extends Command {
public static usage = 'device purge <uuid>';
public static args = {
uuid: Args.string({
public static args: Array<IArg<any>> = [
{
name: 'uuid',
description: 'comma-separated list (no blank spaces) of device UUIDs',
required: true,
}),
};
},
];
public static flags = {
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params } = await this.parse(DevicePurgeCmd);
const { args: params } = this.parse<FlagsDef, ArgsDef>(DevicePurgeCmd);
const { tryAsInteger } = await import('../../utils/validation');
const balena = getBalenaSdk();
const ux = getCliUx();
const deviceUuids = params.uuid.split(',');
const deviceIds = params.uuid.split(',').map((id) => {
return tryAsInteger(id);
});
for (const uuid of deviceUuids) {
ux.action.start(`Purging data from device ${uuid}`);
await balena.models.device.purge(uuid);
for (const deviceId of deviceIds) {
ux.action.start(`Purging data from device ${deviceId}`);
await balena.models.device.purge(deviceId);
ux.action.stop();
}
}

View File

@ -15,10 +15,21 @@
* limitations under the License.
*/
import { Args } from '@oclif/core';
import { flags } from '@oclif/command';
import type { IArg } from '@oclif/parser/lib/args';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { tryAsInteger } from '../../utils/validation';
interface FlagsDef {
force: boolean;
help: void;
}
interface ArgsDef {
uuid: string;
}
export default class DeviceRebootCmd extends Command {
public static description = stripIndent`
@ -28,16 +39,18 @@ export default class DeviceRebootCmd extends Command {
`;
public static examples = ['$ balena device reboot 23c73a1'];
public static args = {
uuid: Args.string({
public static args: Array<IArg<any>> = [
{
name: 'uuid',
description: 'the uuid of the device to reboot',
parse: (dev) => tryAsInteger(dev),
required: true,
}),
};
},
];
public static usage = 'device reboot <uuid>';
public static flags = {
public static flags: flags.Input<FlagsDef> = {
force: cf.force,
help: cf.help,
};
@ -45,7 +58,9 @@ export default class DeviceRebootCmd extends Command {
public static authenticated = true;
public async run() {
const { args: params, flags: options } = await this.parse(DeviceRebootCmd);
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
DeviceRebootCmd,
);
const balena = getBalenaSdk();

View File

@ -15,13 +15,23 @@
* limitations under the License.
*/
import { Flags } from '@oclif/core';
import { flags } from '@oclif/command';
import type { IArg } from '@oclif/parser/lib/args';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import * as ca from '../../utils/common-args';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { applicationIdInfo } from '../../utils/messages';
interface FlagsDef {
uuid?: string;
help: void;
}
interface ArgsDef {
fleet: string;
}
export default class DeviceRegisterCmd extends Command {
public static description = stripIndent`
Register a new device.
@ -37,31 +47,24 @@ export default class DeviceRegisterCmd extends Command {
'$ balena device register MyFleet',
'$ balena device register MyFleet --uuid <uuid>',
'$ balena device register myorg/myfleet --uuid <uuid>',
'$ balena device register myorg/myfleet --uuid <uuid> --deviceType <deviceTypeSlug>',
];
public static args = {
fleet: ca.fleetRequired,
};
public static args: Array<IArg<any>> = [ca.fleetRequired];
public static usage = 'device register <fleet>';
public static flags = {
uuid: Flags.string({
public static flags: flags.Input<FlagsDef> = {
uuid: flags.string({
description: 'custom uuid',
char: 'u',
}),
deviceType: Flags.string({
description:
"device type slug (run 'balena devices supported' for possible values)",
}),
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params, flags: options } = await this.parse(
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
DeviceRegisterCmd,
);
@ -69,18 +72,12 @@ export default class DeviceRegisterCmd extends Command {
const balena = getBalenaSdk();
const application = await getApplication(balena, params.fleet, {
$select: ['id', 'slug'],
});
const application = await getApplication(balena, params.fleet);
const uuid = options.uuid ?? balena.models.device.generateUniqueKey();
console.info(`Registering to ${application.slug}: ${uuid}`);
const result = await balena.models.device.register(
application.id,
uuid,
options.deviceType,
);
const result = await balena.models.device.register(application.id, uuid);
return result && result.uuid;
}

View File

@ -15,10 +15,21 @@
* limitations under the License.
*/
import { Args } from '@oclif/core';
import { flags } from '@oclif/command';
import type { IArg } from '@oclif/parser/lib/args';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy';
import { tryAsInteger } from '../../utils/validation';
interface FlagsDef {
help: void;
}
interface ArgsDef {
uuid: string;
newName?: string;
}
export default class DeviceRenameCmd extends Command {
public static description = stripIndent`
@ -33,26 +44,29 @@ export default class DeviceRenameCmd extends Command {
'$ balena device rename 7cf02a6 MyPi',
];
public static args = {
uuid: Args.string({
public static args: Array<IArg<any>> = [
{
name: 'uuid',
description: 'the uuid of the device to rename',
parse: (dev) => tryAsInteger(dev),
required: true,
}),
newName: Args.string({
},
{
name: 'newName',
description: 'the new name for the device',
}),
};
},
];
public static usage = 'device rename <uuid> [newName]';
public static flags = {
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params } = await this.parse(DeviceRenameCmd);
const { args: params } = this.parse<FlagsDef, ArgsDef>(DeviceRenameCmd);
const balena = getBalenaSdk();

View File

@ -15,7 +15,8 @@
* limitations under the License.
*/
import { Flags, Args } from '@oclif/core';
import { flags } from '@oclif/command';
import type { IArg } from '@oclif/parser/lib/args';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, getCliUx, stripIndent } from '../../utils/lazy';
@ -25,6 +26,15 @@ import type {
CurrentServiceWithCommit,
} from 'balena-sdk';
interface FlagsDef {
help: void;
service?: string;
}
interface ArgsDef {
uuid: string;
}
export default class DeviceRestartCmd extends Command {
public static description = stripIndent`
Restart containers on a device.
@ -45,18 +55,19 @@ export default class DeviceRestartCmd extends Command {
'$ balena device restart 23c73a1 -s myService1,myService2',
];
public static args = {
uuid: Args.string({
public static args: Array<IArg<any>> = [
{
name: 'uuid',
description:
'comma-separated list (no blank spaces) of device UUIDs to restart',
required: true,
}),
};
},
];
public static usage = 'device restart <uuid>';
public static flags = {
service: Flags.string({
public static flags: flags.Input<FlagsDef> = {
service: flags.string({
description:
'comma-separated list (no blank spaces) of service names to restart',
char: 's',
@ -67,23 +78,28 @@ export default class DeviceRestartCmd extends Command {
public static authenticated = true;
public async run() {
const { args: params, flags: options } = await this.parse(DeviceRestartCmd);
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
DeviceRestartCmd,
);
const { tryAsInteger } = await import('../../utils/validation');
const balena = getBalenaSdk();
const ux = getCliUx();
const deviceUuids = params.uuid.split(',');
const deviceIds = params.uuid.split(',').map((id) => {
return tryAsInteger(id);
});
const serviceNames = options.service?.split(',');
// Iterate sequentially through deviceUuids.
// Iterate sequentially through deviceIds.
// We may later want to add a batching feature,
// so that n devices are processed in parallel
for (const uuid of deviceUuids) {
ux.action.start(`Restarting services on device ${uuid}`);
for (const deviceId of deviceIds) {
ux.action.start(`Restarting services on device ${deviceId}`);
if (serviceNames) {
await this.restartServices(balena, uuid, serviceNames);
await this.restartServices(balena, deviceId, serviceNames);
} else {
await this.restartAllServices(balena, uuid);
await this.restartAllServices(balena, deviceId);
}
ux.action.stop();
}
@ -91,7 +107,7 @@ export default class DeviceRestartCmd extends Command {
async restartServices(
balena: BalenaSDK,
deviceUuid: string,
deviceId: number | string,
serviceNames: string[],
) {
const { ExpectedError, instanceOf } = await import('../../errors');
@ -100,7 +116,7 @@ export default class DeviceRestartCmd extends Command {
// Get device
let device: DeviceWithServiceDetails<CurrentServiceWithCommit>;
try {
device = await balena.models.device.getWithServiceDetails(deviceUuid, {
device = await balena.models.device.getWithServiceDetails(deviceId, {
$expand: {
is_running__release: { $select: 'commit' },
},
@ -108,7 +124,7 @@ export default class DeviceRestartCmd extends Command {
} catch (e) {
const { BalenaDeviceNotFound } = await import('balena-errors');
if (instanceOf(e, BalenaDeviceNotFound)) {
throw new ExpectedError(`Device ${deviceUuid} not found.`);
throw new ExpectedError(`Device ${deviceId} not found.`);
} else {
throw e;
}
@ -120,7 +136,7 @@ export default class DeviceRestartCmd extends Command {
serviceNames.forEach((service) => {
if (!device.current_services[service]) {
throw new ExpectedError(
`Service ${service} not found on device ${deviceUuid}.`,
`Service ${service} not found on device ${deviceId}.`,
);
}
});
@ -139,7 +155,7 @@ export default class DeviceRestartCmd extends Command {
if (serviceContainer) {
restartPromises.push(
balena.models.device.restartService(
deviceUuid,
deviceId,
serviceContainer.image_id,
),
);
@ -150,32 +166,32 @@ export default class DeviceRestartCmd extends Command {
await Promise.all(restartPromises);
} catch (e) {
if (e.message.toLowerCase().includes('no online device')) {
throw new ExpectedError(`Device ${deviceUuid} is not online.`);
throw new ExpectedError(`Device ${deviceId} is not online.`);
} else {
throw e;
}
}
}
async restartAllServices(balena: BalenaSDK, deviceUuid: string) {
async restartAllServices(balena: BalenaSDK, deviceId: number | string) {
// Note: device.restartApplication throws `BalenaDeviceNotFound: Device not found` if device not online.
// Need to use device.get first to distinguish between non-existant and offline devices.
// Remove this workaround when SDK issue resolved: https://github.com/balena-io/balena-sdk/issues/649
const { instanceOf, ExpectedError } = await import('../../errors');
try {
const device = await balena.models.device.get(deviceUuid);
const device = await balena.models.device.get(deviceId);
if (!device.is_online) {
throw new ExpectedError(`Device ${deviceUuid} is not online.`);
throw new ExpectedError(`Device ${deviceId} is not online.`);
}
} catch (e) {
const { BalenaDeviceNotFound } = await import('balena-errors');
if (instanceOf(e, BalenaDeviceNotFound)) {
throw new ExpectedError(`Device ${deviceUuid} not found.`);
throw new ExpectedError(`Device ${deviceId} not found.`);
} else {
throw e;
}
}
await balena.models.device.restartApplication(deviceUuid);
await balena.models.device.restartApplication(deviceId);
}
}

View File

@ -15,10 +15,21 @@
* limitations under the License.
*/
import { Args } from '@oclif/core';
import { flags } from '@oclif/command';
import type { IArg } from '@oclif/parser/lib/args';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { tryAsInteger } from '../../utils/validation';
interface FlagsDef {
yes: boolean;
help: void;
}
interface ArgsDef {
uuid: string;
}
export default class DeviceRmCmd extends Command {
public static description = stripIndent`
@ -35,17 +46,18 @@ export default class DeviceRmCmd extends Command {
'$ balena device rm 7cf02a6 --yes',
];
public static args = {
uuid: Args.string({
public static args: Array<IArg<any>> = [
{
name: 'uuid',
description:
'comma-separated list (no blank spaces) of device UUIDs to be removed',
required: true,
}),
};
},
];
public static usage = 'device rm <uuid(s)>';
public static flags = {
public static flags: flags.Input<FlagsDef> = {
yes: cf.yes,
help: cf.help,
};
@ -53,7 +65,9 @@ export default class DeviceRmCmd extends Command {
public static authenticated = true;
public async run() {
const { args: params, flags: options } = await this.parse(DeviceRmCmd);
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
DeviceRmCmd,
);
const balena = getBalenaSdk();
const patterns = await import('../../utils/patterns');
@ -70,7 +84,7 @@ export default class DeviceRmCmd extends Command {
// Remove
for (const uuid of uuids) {
try {
await balena.models.device.remove(uuid);
await balena.models.device.remove(tryAsInteger(uuid));
} catch (err) {
console.info(`${err.message}, uuid: ${uuid}`);
process.exitCode = 1;

View File

@ -15,12 +15,23 @@
* limitations under the License.
*/
import { Args } from '@oclif/core';
import { flags } from '@oclif/command';
import type { IArg } from '@oclif/parser/lib/args';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { tryAsInteger } from '../../utils/validation';
import { ExpectedError } from '../../errors';
interface FlagsDef {
force: boolean;
help: void;
}
interface ArgsDef {
uuid: string;
}
export default class DeviceShutdownCmd extends Command {
public static description = stripIndent`
Shutdown a device.
@ -29,16 +40,18 @@ export default class DeviceShutdownCmd extends Command {
`;
public static examples = ['$ balena device shutdown 23c73a1'];
public static args = {
uuid: Args.string({
public static args: Array<IArg<any>> = [
{
name: 'uuid',
description: 'the uuid of the device to shutdown',
parse: (dev) => tryAsInteger(dev),
required: true,
}),
};
},
];
public static usage = 'device shutdown <uuid>';
public static flags = {
public static flags: flags.Input<FlagsDef> = {
force: cf.force,
help: cf.help,
};
@ -46,7 +59,7 @@ export default class DeviceShutdownCmd extends Command {
public static authenticated = true;
public async run() {
const { args: params, flags: options } = await this.parse(
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
DeviceShutdownCmd,
);

View File

@ -1,53 +0,0 @@
/**
* @license
* Copyright 2016-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 { Args } from '@oclif/core';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
export default class DeviceTrackFleetCmd extends Command {
public static description = stripIndent`
Make a device track the fleet's pinned release.
Make a device track the fleet's pinned release.
`;
public static examples = ['$ balena device track-fleet 7cf02a6'];
public static args = {
uuid: Args.string({
description: "the uuid of the device to make track the fleet's release",
required: true,
}),
};
public static usage = 'device track-fleet <uuid>';
public static flags = {
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params } = await this.parse(DeviceTrackFleetCmd);
const balena = getBalenaSdk();
await balena.models.device.trackApplicationRelease(params.uuid);
}
}

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.
@ -15,25 +15,28 @@
* limitations under the License.
*/
import { flags } from '@oclif/command';
import Command from '../../command';
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 type { Device, PineOptions } from 'balena-sdk';
import { isV14 } from '../../utils/version';
const devicesSelectFields = {
$select: [
'id',
'uuid',
'device_name',
'status',
'is_online',
'supervisor_version',
'os_version',
],
} satisfies PineOptions<Device>;
interface ExtendedDevice extends DeviceWithDeviceType {
dashboard_url?: string;
fleet?: string | null; // 'org/name' slug
device_type?: string | null;
}
interface FlagsDef extends DataSetOutputOptions {
fleet?: string;
help: void;
json?: boolean;
}
export default class DevicesCmd extends Command {
public static description = stripIndent`
@ -55,77 +58,94 @@ export default class DevicesCmd extends Command {
public static usage = 'devices';
public static flags = {
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() {
const { flags: options } = await this.parse(DevicesCmd);
const { flags: options } = this.parse<FlagsDef, {}>(DevicesCmd);
const balena = getBalenaSdk();
const devicesOptions = {
...devicesSelectFields,
...expandForAppName,
$orderby: { device_name: 'asc' },
} satisfies PineOptions<Device>;
const devices = (
await (async () => {
if (options.fleet != null) {
const { getApplication } = await import('../../utils/sdk');
const application = await getApplication(balena, options.fleet, {
$select: 'slug',
$expand: {
owns__device: devicesOptions,
},
});
return application.owns__device;
}
let devices;
return await balena.pine.get({
resource: 'device',
options: devicesOptions,
});
})()
).map((device) => ({
...device,
dashboard_url: balena.models.device.getDashboardUrl(device.uuid),
fleet: device.belongs_to__application?.[0]?.slug || null,
uuid: options.json ? device.uuid : device.uuid.slice(0, 7),
device_type: device.is_of__device_type?.[0]?.slug || null,
}));
const fields: Array<keyof (typeof devices)[number]> = [
'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));
if (options.fleet != null) {
const { getApplication } = await import('../../utils/sdk');
const application = await getApplication(balena, options.fleet);
devices = (await balena.models.device.getAllByApplication(
application.id,
expandForAppName,
)) as ExtendedDevice[];
} else {
const _ = await import('lodash');
console.log(
getVisuals().table.horizontal(
devices.map((dev) => _.mapValues(dev, (val) => val ?? 'N/a')),
fields,
),
);
devices = (await balena.models.device.getAll(
expandForAppName,
)) as ExtendedDevice[];
}
devices = devices.map(function (device) {
device.dashboard_url = balena.models.device.getDashboardUrl(device.uuid);
const belongsToApplication =
device.belongs_to__application as Application[];
device.fleet = belongsToApplication?.[0]?.slug || null;
device.uuid = options.json ? device.uuid : device.uuid.slice(0, 7);
device.device_type = device.is_of__device_type?.[0]?.slug || null;
return device;
});
if (isV14()) {
const outputFields = [
'id',
'uuid',
'device_name',
'device_type',
'fleet',
'status',
'is_online',
'supervisor_version',
'os_version',
'dashboard_url',
];
await this.outputData(devices, outputFields, {
...options,
displayNullValuesAs: 'N/a',
});
} else {
// 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.
@ -14,14 +14,20 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Flags } from '@oclif/core';
import type * as BalenaSdk from 'balena-sdk';
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';
import { isV14 } from '../../utils/version';
interface FlagsDef extends DataSetOutputOptions {
help: void;
json?: boolean;
}
export default class DevicesSupportedCmd extends Command {
public static description = stripIndent`
@ -45,56 +51,57 @@ export default class DevicesSupportedCmd extends Command {
new CommandHelp({ args: DevicesSupportedCmd.args }).defaultUsage()
).trim();
public static flags = {
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() {
const { flags: options } = await this.parse(DevicesSupportedCmd);
const pineOptions = {
$select: ['slug', 'name'],
$expand: {
is_of__cpu_architecture: { $select: 'slug' },
device_type_alias: {
$select: 'is_referenced_by__alias',
$orderby: { is_referenced_by__alias: 'asc' },
},
},
} satisfies BalenaSdk.PineOptions<BalenaSdk.DeviceType>;
const dts = (await getBalenaSdk().models.deviceType.getAllSupported(
pineOptions,
)) as Array<
BalenaSdk.PineTypedResult<BalenaSdk.DeviceType, typeof pineOptions>
>;
const { flags: options } = this.parse<FlagsDef, {}>(DevicesSupportedCmd);
const [dts, configDTs] = await Promise.all([
getBalenaSdk().models.deviceType.getAllSupported({
$expand: { is_of__cpu_architecture: { $select: 'slug' } },
$select: ['slug', 'name'],
}),
getBalenaSdk().models.config.getDeviceTypes(),
]);
const dtsBySlug = _.keyBy(dts, (dt) => dt.slug);
const configDTsBySlug = _.keyBy(configDTs, (dt) => dt.slug);
interface DT {
slug: string;
aliases: string[];
aliases: string[] | string;
arch: string;
name: string;
}
let deviceTypes = dts.map((dt): DT => {
const aliases = dt.device_type_alias
.map((dta) => dta.is_referenced_by__alias)
.filter((alias) => alias !== dt.slug);
return {
slug: dt.slug,
aliases: options.json ? aliases : [aliases.join(', ')],
arch: dt.is_of__cpu_architecture[0]?.slug || 'n/a',
let deviceTypes: DT[] = [];
for (const slug of Object.keys(dtsBySlug)) {
const configDT: Partial<typeof configDTs[0]> =
configDTsBySlug[slug] || {};
const aliases = (configDT.aliases || []).filter(
(alias) => alias !== slug,
);
const dt: Partial<typeof dts[0]> = dtsBySlug[slug] || {};
deviceTypes.push({
slug,
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

@ -15,7 +15,7 @@
* limitations under the License.
*/
import { Args } from '@oclif/core';
import { flags } from '@oclif/command';
import type * as BalenaSdk from 'balena-sdk';
import Command from '../../command';
import { ExpectedError } from '../../errors';
@ -78,21 +78,23 @@ export default class EnvAddCmd extends Command {
'$ balena env add EDITOR vim --device 7cf02a6,d6f1433 --service MyService,MyService2',
];
public static args = {
name: Args.string({
public static args = [
{
name: 'name',
required: true,
description: 'environment or config variable name',
}),
value: Args.string({
},
{
name: 'value',
required: false,
description:
"variable value; if omitted, use value from this process' environment",
}),
};
},
];
public static usage = 'env add <name> [value]';
public static flags = {
public static flags: flags.Input<FlagsDef> = {
fleet: { ...cf.fleet, exclusive: ['device'] },
device: { ...cf.device, exclusive: ['fleet'] },
help: cf.help,
@ -101,7 +103,9 @@ export default class EnvAddCmd extends Command {
};
public async run() {
const { args: params, flags: options } = await this.parse(EnvAddCmd);
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
EnvAddCmd,
);
const cmd = this;
if (!options.fleet && !options.device) {
@ -147,15 +151,16 @@ export default class EnvAddCmd extends Command {
const varType = isConfigVar ? 'configVar' : 'envVar';
if (options.fleet) {
for (const appSlug of await resolveFleetSlugs(balena, options.fleet)) {
const { getFleetSlug } = await import('../../utils/sdk');
for (const app of options.fleet.split(',')) {
try {
await balena.models.application[varType].set(
appSlug,
await getFleetSlug(balena, app),
params.name,
params.value,
);
} catch (err) {
console.error(`${err.message}, fleet: ${appSlug}`);
console.error(`${err.message}, fleet: ${app}`);
process.exitCode = 1;
}
}
@ -176,25 +181,6 @@ export default class EnvAddCmd extends Command {
}
}
// TODO: Stop accepting application names in the next major
// and just drop this in favor of doing the .split(',') directly.
async function resolveFleetSlugs(
balena: BalenaSdk.BalenaSDK,
fleetOption: string,
) {
const fleetSlugs: string[] = [];
const { getFleetSlug } = await import('../../utils/sdk');
for (const appNameOrSlug of fleetOption.split(',')) {
try {
fleetSlugs.push(await getFleetSlug(balena, appNameOrSlug));
} catch (err) {
console.error(`${err.message}, fleet: ${appNameOrSlug}`);
process.exitCode = 1;
}
}
return fleetSlugs;
}
/**
* Add service variables for a device or fleet.
*/
@ -204,17 +190,17 @@ async function setServiceVars(
options: FlagsDef,
) {
if (options.fleet) {
for (const appSlug of await resolveFleetSlugs(sdk, options.fleet)) {
for (const app of options.fleet.split(',')) {
for (const service of options.service!.split(',')) {
try {
const serviceId = await getServiceIdForApp(sdk, appSlug, service);
const serviceId = await getServiceIdForApp(sdk, app, service);
await sdk.models.service.var.set(
serviceId,
params.name,
params.value!,
);
} catch (err) {
console.error(`${err.message}, fleet: ${appSlug}`);
console.error(`${err.message}, fleet: ${app}`);
process.exitCode = 1;
}
}
@ -259,12 +245,11 @@ async function setServiceVars(
*/
async function getServiceIdForApp(
sdk: BalenaSdk.BalenaSDK,
appSlug: string,
appName: string,
serviceName: string,
): Promise<number> {
let serviceId: number | undefined;
const services = await sdk.models.service.getAllByApplication(appSlug, {
$select: 'id',
const services = await sdk.models.service.getAllByApplication(appName, {
$filter: { service_name: serviceName },
});
if (services.length > 0) {
@ -272,7 +257,7 @@ async function getServiceIdForApp(
}
if (serviceId === undefined) {
throw new ExpectedError(
`Cannot find service ${serviceName} for fleet ${appSlug}`,
`Cannot find service ${serviceName} for fleet ${appName}`,
);
}
return serviceId;

View File

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Args } from '@oclif/core';
import { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
@ -22,6 +22,20 @@ import * as ec from '../../utils/env-common';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { parseAsInteger } from '../../utils/validation';
type IArg<T> = import('@oclif/parser').args.IArg<T>;
interface FlagsDef {
config: boolean;
device: boolean;
service: boolean;
help: void;
}
interface ArgsDef {
id: number;
value: string;
}
export default class EnvRenameCmd extends Command {
public static description = stripIndent`
Change the value of a config or env var for a fleet, device or service.
@ -40,22 +54,24 @@ export default class EnvRenameCmd extends Command {
'$ balena env rename 678678 1 --device --config',
];
public static args = {
id: Args.integer({
public static args: Array<IArg<any>> = [
{
name: 'id',
required: true,
description: "variable's numeric database ID",
parse: async (input) => parseAsInteger(input, 'id'),
}),
value: Args.string({
parse: (input) => parseAsInteger(input, 'id'),
},
{
name: 'value',
required: true,
description:
"variable value; if omitted, use value from this process' environment",
}),
};
},
];
public static usage = 'env rename <id> <value>';
public static flags = {
public static flags: flags.Input<FlagsDef> = {
config: ec.booleanConfig,
device: ec.booleanDevice,
service: ec.booleanService,
@ -63,7 +79,9 @@ export default class EnvRenameCmd extends Command {
};
public async run() {
const { args: params, flags: opt } = await this.parse(EnvRenameCmd);
const { args: params, flags: opt } = this.parse<FlagsDef, ArgsDef>(
EnvRenameCmd,
);
await Command.checkLoggedIn();

View File

@ -15,13 +15,26 @@
* limitations under the License.
*/
import { Flags, Args } from '@oclif/core';
import { flags } from '@oclif/command';
import Command from '../../command';
import * as ec from '../../utils/env-common';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { parseAsInteger } from '../../utils/validation';
type IArg<T> = import('@oclif/parser').args.IArg<T>;
interface FlagsDef {
config: boolean;
device: boolean;
service: boolean;
yes: boolean;
}
interface ArgsDef {
id: number;
}
export default class EnvRmCmd extends Command {
public static description = stripIndent`
Remove a config or env var from a fleet, device or service.
@ -44,21 +57,22 @@ export default class EnvRmCmd extends Command {
'$ balena env rm 789789 --device --service --yes',
];
public static args = {
id: Args.integer({
public static args: Array<IArg<any>> = [
{
name: 'id',
required: true,
description: "variable's numeric database ID",
parse: async (input) => parseAsInteger(input, 'id'),
}),
};
parse: (input) => parseAsInteger(input, 'id'),
},
];
public static usage = 'env rm <id>';
public static flags = {
public static flags: flags.Input<FlagsDef> = {
config: ec.booleanConfig,
device: ec.booleanDevice,
service: ec.booleanService,
yes: Flags.boolean({
yes: flags.boolean({
char: 'y',
description:
'do not prompt for confirmation before deleting the variable',
@ -67,7 +81,9 @@ export default class EnvRmCmd extends Command {
};
public async run() {
const { args: params, flags: opt } = await this.parse(EnvRmCmd);
const { args: params, flags: opt } = this.parse<FlagsDef, ArgsDef>(
EnvRmCmd,
);
await Command.checkLoggedIn();

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.
@ -14,8 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Flags } from '@oclif/core';
import type { Interfaces } from '@oclif/core';
import { flags } from '@oclif/command';
import type * as SDK from 'balena-sdk';
import * as _ from 'lodash';
import Command from '../command';
@ -23,8 +22,18 @@ 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';
type FlagsDef = Interfaces.InferredFlags<typeof EnvsCmd.flags>;
import { isV14 } from '../utils/version';
interface FlagsDef extends DataSetOutputOptions {
fleet?: string;
config: boolean;
device?: string; // device UUID
json?: boolean;
help: void;
service?: string; // service name
}
interface EnvironmentVariableInfo extends SDK.EnvironmentVariableBase {
fleet?: string | null; // fleet slug
@ -87,6 +96,7 @@ export default class EnvsCmd extends Command {
'$ balena envs --fleet myorg/myfleet',
'$ balena envs --fleet MyFleet --json',
'$ balena envs --fleet MyFleet --service MyService',
'$ balena envs --fleet MyFleet --service MyService',
'$ balena envs --fleet MyFleet --config',
'$ balena envs --device 7cf02a6',
'$ balena envs --device 7cf02a6 --json',
@ -96,9 +106,9 @@ export default class EnvsCmd extends Command {
public static usage = 'envs';
public static flags = {
public static flags: flags.Input<FlagsDef> = {
fleet: { ...cf.fleet, exclusive: ['device'] },
config: Flags.boolean({
config: flags.boolean({
default: false,
char: 'c',
description: 'show configuration variables only',
@ -106,12 +116,12 @@ 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'] },
};
public async run() {
const { flags: options } = await this.parse(EnvsCmd);
const { flags: options } = this.parse<FlagsDef, {}>(EnvsCmd);
const variables: EnvironmentVariableInfo[] = [];
@ -174,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,
),
);
}
}
}
}
@ -202,7 +247,6 @@ async function validateServiceName(
fleetSlug: string,
) {
const services = await sdk.models.service.getAllByApplication(fleetSlug, {
$select: 'id',
$filter: { service_name: serviceName },
});
if (services.length === 0) {

View File

@ -15,11 +15,23 @@
* limitations under the License.
*/
import { Flags, Args } from '@oclif/core';
import { flags } from '@oclif/command';
import type { Application } from 'balena-sdk';
import Command from '../../command';
import { ExpectedError } from '../../errors';
import * as cf from '../../utils/common-flags';
import { stripIndent } from '../../utils/lazy';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
interface FlagsDef {
organization?: string;
type?: string; // application device type
help: void;
}
interface ArgsDef {
name: string;
}
export default class FleetCreateCmd extends Command {
public static description = stripIndent`
@ -49,21 +61,22 @@ export default class FleetCreateCmd extends Command {
'$ balena fleet create MyFleet -o myorg --type raspberry-pi',
];
public static args = {
name: Args.string({
public static args = [
{
name: 'name',
description: 'fleet name',
required: true,
}),
};
},
];
public static usage = 'fleet create <name>';
public static flags = {
organization: Flags.string({
public static flags: flags.Input<FlagsDef> = {
organization: flags.string({
char: 'o',
description: 'handle of the organization the fleet should belong to',
}),
type: Flags.string({
type: flags.string({
char: 't',
description:
'fleet device type (Check available types with `balena devices supported`)',
@ -74,10 +87,63 @@ export default class FleetCreateCmd extends Command {
public static authenticated = true;
public async run() {
const { args: params, flags: options } = await this.parse(FleetCreateCmd);
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
FleetCreateCmd,
);
await (
await import('../../utils/application-create')
).applicationCreateBase('fleet', options, params);
// Ascertain device type
const deviceType =
options.type ||
(await (await import('../../utils/patterns')).selectDeviceType());
// Ascertain organization
const organization =
options.organization?.toLowerCase() || (await this.getOrganization());
// Create application
let application: Application;
try {
application = await getBalenaSdk().models.application.create({
name: params.name,
deviceType,
organization,
});
} catch (err) {
if ((err.message || '').toLowerCase().includes('unique')) {
// BalenaRequestError: Request error: "organization" and "app_name" must be unique.
throw new ExpectedError(
`Error: fleet "${params.name}" already exists in organization "${organization}".`,
);
} else if ((err.message || '').toLowerCase().includes('unauthorized')) {
// BalenaRequestError: Request error: Unauthorized
throw new ExpectedError(
`Error: You are not authorized to create fleets in organization "${organization}".`,
);
}
throw err;
}
// Output
console.log(
`Fleet created: slug "${application.slug}", device type "${deviceType}"`,
);
}
async getOrganization() {
const { getOwnOrganizations } = await import('../../utils/sdk');
const organizations = await getOwnOrganizations(getBalenaSdk());
if (organizations.length === 0) {
// User is not a member of any organizations (should not happen).
throw new Error('This account is not a member of any organizations');
} else if (organizations.length === 1) {
// User is a member of only one organization - use this.
return organizations[0].handle;
} else {
// User is a member of multiple organizations -
const { selectOrganization } = await import('../../utils/patterns');
return selectOrganization(organizations);
}
}
}

View File

@ -15,13 +15,24 @@
* limitations under the License.
*/
import { Flags } from '@oclif/core';
import type { flags } from '@oclif/command';
import type { Release } from 'balena-sdk';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import * as ca from '../../utils/common-args';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
import { applicationIdInfo } from '../../utils/messages';
import { isV14 } from '../../utils/version';
import type { DataOutputOptions } from '../../framework';
interface FlagsDef extends DataOutputOptions {
help: void;
}
interface ArgsDef {
fleet: string;
}
export default class FleetCmd extends Command {
public static description = stripIndent`
@ -34,60 +45,59 @@ export default class FleetCmd extends Command {
public static examples = [
'$ balena fleet MyFleet',
'$ balena fleet myorg/myfleet',
'$ balena fleet myorg/myfleet --view',
];
public static args = {
fleet: ca.fleetRequired,
};
public static args = [ca.fleetRequired];
public static usage = 'fleet <fleet>';
public static flags = {
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
view: Flags.boolean({
default: false,
description: 'open fleet dashboard page',
}),
...cf.dataOutputFlags,
...(isV14() ? cf.dataOutputFlags : {}),
};
public static authenticated = true;
public static primary = true;
public async run() {
const { args: params, flags: options } = await this.parse(FleetCmd);
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
FleetCmd,
);
const { getApplication } = await import('../../utils/sdk');
const balena = getBalenaSdk();
const application = await getApplication(balena, params.fleet, {
const application = (await getApplication(getBalenaSdk(), params.fleet, {
$expand: {
is_for__device_type: { $select: 'slug' },
should_be_running__release: { $select: 'commit' },
},
});
if (options.view) {
const open = await import('open');
const dashboardUrl = balena.models.application.getDashboardUrl(
application.id,
);
await open(dashboardUrl, { wait: false });
return;
}
const outputApplication = {
...application,
device_type: application.is_for__device_type[0].slug,
commit: application.should_be_running__release[0]?.commit,
})) as ApplicationWithDeviceType & {
should_be_running__release: [Release?];
// For display purposes:
device_type: string;
commit?: string;
};
await this.outputData(
outputApplication,
['app_name', 'id', 'device_type', 'slug', 'commit'],
options,
);
application.device_type = application.is_for__device_type[0].slug;
application.commit = application.should_be_running__release[0]?.commit;
if (isV14()) {
await this.outputData(
application,
['app_name', 'id', 'device_type', 'slug', 'commit'],
options,
);
} else {
// Emulate table.vertical title output, but avoid uppercasing and inserting spaces
console.log(`== ${application.slug}`);
console.log(
getVisuals().table.vertical(application, [
'id',
'device_type',
'slug',
'commit',
]),
);
}
}
}

View File

@ -1,88 +0,0 @@
/**
* @license
* Copyright 2016-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 { Args } from '@oclif/core';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { getExpandedProp } from '../../utils/pine';
export default class FleetPinCmd extends Command {
public static description = stripIndent`
Pin a fleet to a release.
Pin a fleet to a release.
Note, if the commit is omitted, the currently pinned release will be printed, with instructions for how to see a list of releases
`;
public static examples = [
'$ balena fleet pin myfleet',
'$ balena fleet pin myorg/myfleet 91165e5',
];
public static args = {
slug: Args.string({
description: 'the slug of the fleet to pin to a release',
required: true,
}),
releaseToPinTo: Args.string({
description: 'the commit of the release for the fleet to get pinned to',
}),
};
public static usage = 'fleet pin <slug> [releaseToPinTo]';
public static flags = {
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params } = await this.parse(FleetPinCmd);
const balena = getBalenaSdk();
const fleet = await balena.models.application.get(params.slug, {
$expand: {
should_be_running__release: {
$select: 'commit',
},
},
});
const pinnedRelease = getExpandedProp(
fleet.should_be_running__release,
'commit',
);
const releaseToPinTo = params.releaseToPinTo;
const slug = params.slug;
if (!releaseToPinTo) {
console.log(
`${
pinnedRelease
? `This fleet is currently pinned to ${pinnedRelease}.`
: 'This fleet is not currently pinned to any release.'
} \n\nTo see a list of all releases this fleet can be pinned to, run \`balena releases ${slug}\`.`,
);
} else {
await balena.models.application.pinToRelease(slug, releaseToPinTo);
}
}
}

View File

@ -15,12 +15,22 @@
* limitations under the License.
*/
import type { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import * as ca from '../../utils/common-args';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { applicationIdInfo } from '../../utils/messages';
interface FlagsDef {
help: void;
}
interface ArgsDef {
fleet: string;
}
export default class FleetPurgeCmd extends Command {
public static description = stripIndent`
Purge data from a fleet.
@ -36,20 +46,18 @@ export default class FleetPurgeCmd extends Command {
'$ balena fleet purge myorg/myfleet',
];
public static args = {
fleet: ca.fleetRequired,
};
public static args = [ca.fleetRequired];
public static usage = 'fleet purge <fleet>';
public static flags = {
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params } = await this.parse(FleetPurgeCmd);
const { args: params } = this.parse<FlagsDef, ArgsDef>(FleetPurgeCmd);
const { getApplication } = await import('../../utils/sdk');
@ -57,9 +65,7 @@ export default class FleetPurgeCmd extends Command {
// balena.models.application.purge only accepts a numeric id
// so we must first fetch the app to get it's id,
const application = await getApplication(balena, params.fleet, {
$select: 'id',
});
const application = await getApplication(balena, params.fleet);
try {
await balena.models.application.purge(application.id);

View File

@ -15,7 +15,8 @@
* limitations under the License.
*/
import { Args } from '@oclif/core';
import type { flags } from '@oclif/command';
import type { ApplicationType } from 'balena-sdk';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
@ -23,6 +24,15 @@ import * as ca from '../../utils/common-args';
import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy';
import { applicationIdInfo } from '../../utils/messages';
interface FlagsDef {
help: void;
}
interface ArgsDef {
fleet: string;
newName?: string;
}
export default class FleetRenameCmd extends Command {
public static description = stripIndent`
Rename a fleet.
@ -41,23 +51,24 @@ export default class FleetRenameCmd extends Command {
'$ balena fleet rename myorg/oldname NewName',
];
public static args = {
fleet: ca.fleetRequired,
newName: Args.string({
public static args = [
ca.fleetRequired,
{
name: 'newName',
description: 'the new name for the fleet',
}),
};
},
];
public static usage = 'fleet rename <fleet> [newName]';
public static flags = {
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params } = await this.parse(FleetRenameCmd);
const { args: params } = this.parse<FlagsDef, ArgsDef>(FleetRenameCmd);
const { validateApplicationName } = await import('../../utils/validation');
const { ExpectedError } = await import('../../errors');
@ -67,10 +78,9 @@ export default class FleetRenameCmd extends Command {
// Disambiguate target application (if params.params is a number, it could either be an ID or a numerical name)
const { getApplication } = await import('../../utils/sdk');
const application = await getApplication(balena, params.fleet, {
$select: ['id', 'app_name', 'slug'],
$expand: {
application_type: {
$select: 'slug',
$select: ['is_legacy'],
},
},
});
@ -81,8 +91,8 @@ export default class FleetRenameCmd extends Command {
}
// Check app supports renaming
const appType = application.application_type[0];
if (appType.slug === 'legacy-v1' || appType.slug === 'legacy-v2') {
const appType = (application.application_type as ApplicationType[])?.[0];
if (appType.is_legacy) {
throw new ExpectedError(
`Fleet ${params.fleet} is of 'legacy' type, and cannot be renamed.`,
);
@ -123,9 +133,9 @@ export default class FleetRenameCmd extends Command {
}
// Get application again, to be sure of results
const renamedApplication = await getApplication(balena, application.id, {
$select: ['app_name', 'slug'],
});
const renamedApplication = await balena.models.application.get(
application.id,
);
// Output result
console.log(`Fleet renamed`);

View File

@ -15,12 +15,22 @@
* limitations under the License.
*/
import type { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import * as ca from '../../utils/common-args';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { applicationIdInfo } from '../../utils/messages';
interface FlagsDef {
help: void;
}
interface ArgsDef {
fleet: string;
}
export default class FleetRestartCmd extends Command {
public static description = stripIndent`
Restart a fleet.
@ -35,30 +45,26 @@ export default class FleetRestartCmd extends Command {
'$ balena fleet restart myorg/myfleet',
];
public static args = {
fleet: ca.fleetRequired,
};
public static args = [ca.fleetRequired];
public static usage = 'fleet restart <fleet>';
public static flags = {
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params } = await this.parse(FleetRestartCmd);
const { args: params } = this.parse<FlagsDef, ArgsDef>(FleetRestartCmd);
const { getApplication } = await import('../../utils/sdk');
const balena = getBalenaSdk();
// Disambiguate application
const application = await getApplication(balena, params.fleet, {
$select: 'slug',
});
// Disambiguate application (if is a number, it could either be an ID or a numerical name)
const application = await getApplication(balena, params.fleet);
await balena.models.application.restart(application.slug);
await balena.models.application.restart(application.id);
}
}

View File

@ -15,12 +15,23 @@
* limitations under the License.
*/
import type { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import * as ca from '../../utils/common-args';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { applicationIdInfo } from '../../utils/messages';
interface FlagsDef {
yes: boolean;
help: void;
}
interface ArgsDef {
fleet: string;
}
export default class FleetRmCmd extends Command {
public static description = stripIndent`
Remove a fleet.
@ -38,13 +49,11 @@ export default class FleetRmCmd extends Command {
'$ balena fleet rm myorg/myfleet',
];
public static args = {
fleet: ca.fleetRequired,
};
public static args = [ca.fleetRequired];
public static usage = 'fleet rm <fleet>';
public static flags = {
public static flags: flags.Input<FlagsDef> = {
yes: cf.yes,
help: cf.help,
};
@ -52,7 +61,9 @@ export default class FleetRmCmd extends Command {
public static authenticated = true;
public async run() {
const { args: params, flags: options } = await this.parse(FleetRmCmd);
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
FleetRmCmd,
);
const { confirm } = await import('../../utils/patterns');
const { getApplication } = await import('../../utils/sdk');
@ -65,11 +76,9 @@ export default class FleetRmCmd extends Command {
);
// Disambiguate application (if is a number, it could either be an ID or a numerical name)
const application = await getApplication(balena, params.fleet, {
$select: 'slug',
});
const application = await getApplication(balena, params.fleet);
// Remove
await balena.models.application.remove(application.slug);
await balena.models.application.remove(application.id);
}
}

View File

@ -1,56 +0,0 @@
/**
* @license
* Copyright 2016-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 { Args } from '@oclif/core';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
export default class FleetTrackLatestCmd extends Command {
public static description = stripIndent`
Make this fleet track the latest release.
Make this fleet track the latest release.
`;
public static examples = [
'$ balena fleet track-latest myorg/myfleet',
'$ balena fleet track-latest myfleet',
];
public static args = {
slug: Args.string({
description: 'the slug of the fleet to make track the latest release',
required: true,
}),
};
public static usage = 'fleet track-latest <slug>';
public static flags = {
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params } = await this.parse(FleetTrackLatestCmd);
const balena = getBalenaSdk();
await balena.models.application.trackLatestRelease(params.slug);
}
}

View File

@ -15,18 +15,25 @@
* limitations under the License.
*/
import type * as BalenaSdk from 'balena-sdk';
import { flags } from '@oclif/command';
import Command from '../command';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../utils/lazy';
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
import { isV14 } from '../utils/version';
import type { DataSetOutputOptions } from '../framework';
interface ExtendedApplication extends ApplicationWithDeviceTypeSlug {
interface ExtendedApplication extends ApplicationWithDeviceType {
device_count: number;
online_devices: number;
device_type?: string;
}
interface FlagsDef extends DataSetOutputOptions {
help: void;
verbose?: boolean;
}
export default class FleetsCmd extends Command {
public static description = stripIndent`
List all fleets.
@ -41,8 +48,8 @@ export default class FleetsCmd extends Command {
public static usage = 'fleets';
public static flags = {
...cf.dataSetOutputFlags,
public static flags: flags.Input<FlagsDef> = {
...(isV14() ? cf.dataSetOutputFlags : {}),
help: cf.help,
};
@ -50,24 +57,19 @@ export default class FleetsCmd extends Command {
public static primary = true;
public async run() {
const { flags: options } = await this.parse(FleetsCmd);
const { flags: options } = this.parse<FlagsDef, {}>(FleetsCmd);
const balena = getBalenaSdk();
const pineOptions = {
$select: ['id', 'app_name', 'slug'],
$expand: {
is_for__device_type: { $select: 'slug' },
owns__device: { $select: 'is_online' },
},
} satisfies BalenaSdk.PineOptions<BalenaSdk.Application>;
// Get applications
const applications =
(await balena.models.application.getAllDirectlyAccessible(
pineOptions,
)) as Array<
BalenaSdk.PineTypedResult<BalenaSdk.Application, typeof pineOptions>
> as ExtendedApplication[];
(await balena.models.application.getAllDirectlyAccessible({
$select: ['id', 'app_name', 'slug'],
$expand: {
is_for__device_type: { $select: 'slug' },
owns__device: { $select: 'is_online' },
},
})) as ExtendedApplication[];
// Add extended properties
applications.forEach((application) => {
@ -77,17 +79,30 @@ export default class FleetsCmd extends Command {
application.device_type = application.is_for__device_type[0].slug;
});
await this.outputData(
applications,
[
'id',
'app_name',
'slug',
'device_type',
'device_count',
'online_devices',
],
options,
);
if (isV14()) {
await this.outputData(
applications,
[
'id',
'app_name',
'slug',
'device_type',
'device_count',
'online_devices',
],
options,
);
} else {
console.log(
getVisuals().table.horizontal(applications, [
'id',
'app_name => NAME',
'slug',
'device_type',
'online_devices',
'device_count',
]),
);
}
}
}

View File

@ -15,7 +15,6 @@
* limitations under the License.
*/
import { Args } from '@oclif/core';
import Command from '../../command';
import { stripIndent } from '../../utils/lazy';
import { CommandHelp } from '../../utils/oclif-utils';
@ -28,6 +27,12 @@ import { CommandHelp } from '../../utils/oclif-utils';
// - https://github.com/balena-io/balena-cli/pull/1455#discussion_r334308357
// - https://github.com/balena-io/balena-cli/pull/1455#discussion_r334308526
interface ArgsDef {
image: string;
type: string;
config: string;
}
export default class OsinitCmd extends Command {
public static description = stripIndent`
Do actual init of the device with the preconfigured os image.
@ -36,17 +41,20 @@ export default class OsinitCmd extends Command {
Use \`balena os initialize <image>\` instead.
`;
public static args = {
image: Args.string({
public static args = [
{
name: 'image',
required: true,
}),
type: Args.string({
},
{
name: 'type',
required: true,
}),
config: Args.string({
},
{
name: 'config',
required: true,
}),
};
},
];
public static usage = (
'internal osinit ' +
@ -58,7 +66,7 @@ export default class OsinitCmd extends Command {
public static offlineCompatible = true;
public async run() {
const { args: params } = await this.parse(OsinitCmd);
const { args: params } = this.parse<{}, ArgsDef>(OsinitCmd);
const config = JSON.parse(params.config);

View File

@ -15,13 +15,23 @@
* limitations under the License.
*/
import { Args, Flags } from '@oclif/core';
import { flags } from '@oclif/command';
import Command from '../command';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../utils/lazy';
import { applicationIdInfo } from '../utils/messages';
import { parseAsLocalHostnameOrIp } from '../utils/validation';
interface FlagsDef {
fleet?: string;
pollInterval?: number;
help?: void;
}
interface ArgsDef {
deviceIpOrHostname?: string;
}
export default class JoinCmd extends Command {
public static description = stripIndent`
Move a local device to a fleet on another balena server.
@ -53,19 +63,20 @@ export default class JoinCmd extends Command {
'$ balena join 192.168.1.25 --fleet MyFleet',
];
public static args = {
deviceIpOrHostname: Args.string({
public static args = [
{
name: 'deviceIpOrHostname',
description: 'the IP or hostname of device',
parse: parseAsLocalHostnameOrIp,
}),
};
},
];
// Hardcoded to preserve camelcase
public static usage = 'join [deviceIpOrHostname]';
public static flags = {
public static flags: flags.Input<FlagsDef> = {
fleet: cf.fleet,
pollInterval: Flags.integer({
pollInterval: flags.integer({
description: 'the interval in minutes to check for updates',
char: 'i',
}),
@ -76,7 +87,9 @@ export default class JoinCmd extends Command {
public static primary = true;
public async run() {
const { args: params, flags: options } = await this.parse(JoinCmd);
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
JoinCmd,
);
const promote = await import('../utils/promote');
const sdk = getBalenaSdk();

View File

@ -15,12 +15,21 @@
* limitations under the License.
*/
import { Args } from '@oclif/core';
import { flags } from '@oclif/command';
import Command from '../../command';
import { ExpectedError } from '../../errors';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
interface FlagsDef {
help: void;
}
interface ArgsDef {
name: string;
path: string;
}
export default class KeyAddCmd extends Command {
public static description = stripIndent`
Add an SSH key to balenaCloud.
@ -51,19 +60,21 @@ export default class KeyAddCmd extends Command {
'$ balena key add Main %userprofile%.sshid_rsa.pub',
];
public static args = {
name: Args.string({
public static args = [
{
name: 'name',
description: 'the SSH key name',
required: true,
}),
path: Args.string({
},
{
name: `path`,
description: `the path to the public key file`,
}),
};
},
];
public static usage = 'key add <name> [path]';
public static flags = {
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
@ -72,7 +83,7 @@ export default class KeyAddCmd extends Command {
public static readStdin = true;
public async run() {
const { args: params } = await this.parse(KeyAddCmd);
const { args: params } = this.parse<FlagsDef, ArgsDef>(KeyAddCmd);
let key: string;
if (params.path != null) {

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.
@ -15,11 +15,24 @@
* limitations under the License.
*/
import { Args } from '@oclif/core';
import { flags } from '@oclif/command';
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 extends DataOutputOptions {
help: void;
}
interface ArgsDef {
id: number;
}
export default class KeyCmd extends Command {
public static description = stripIndent`
@ -30,38 +43,64 @@ export default class KeyCmd extends Command {
public static examples = ['$ balena key 17'];
public static args = {
id: Args.integer({
public static args: Array<IArg<any>> = [
{
name: 'id',
description: 'balenaCloud ID for the SSH key',
parse: async (x) => parseAsInteger(x, 'id'),
parse: (x) => parseAsInteger(x, 'id'),
required: true,
}),
};
},
];
public static usage = 'key <id>';
public static flags = {
public static flags: flags.Input<FlagsDef> = {
...(isV14() ? cf.dataOutputFlags : {}),
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params } = await this.parse(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

@ -15,12 +15,23 @@
* limitations under the License.
*/
import { Args } from '@oclif/core';
import { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { parseAsInteger } from '../../utils/validation';
type IArg<T> = import('@oclif/parser').args.IArg<T>;
interface FlagsDef {
yes: boolean;
help: void;
}
interface ArgsDef {
id: number;
}
export default class KeyRmCmd extends Command {
public static description = stripIndent`
Remove an SSH key from balenaCloud.
@ -32,17 +43,18 @@ export default class KeyRmCmd extends Command {
public static examples = ['$ balena key rm 17', '$ balena key rm 17 --yes'];
public static args = {
id: Args.integer({
public static args: Array<IArg<any>> = [
{
name: 'id',
description: 'balenaCloud ID for the SSH key',
parse: async (x) => parseAsInteger(x, 'id'),
parse: (x) => parseAsInteger(x, 'id'),
required: true,
}),
};
},
];
public static usage = 'key rm <id>';
public static flags = {
public static flags: flags.Input<FlagsDef> = {
yes: cf.yes,
help: cf.help,
};
@ -50,7 +62,9 @@ export default class KeyRmCmd extends Command {
public static authenticated = true;
public async run() {
const { args: params, flags: options } = await this.parse(KeyRmCmd);
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
KeyRmCmd,
);
const patterns = await import('../../utils/patterns');

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.
@ -15,9 +15,17 @@
* limitations under the License.
*/
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';
import { isV14 } from '../utils/version';
interface FlagsDef extends DataSetOutputOptions {
help: void;
}
export default class KeysCmd extends Command {
public static description = stripIndent`
@ -29,14 +37,15 @@ export default class KeysCmd extends Command {
public static usage = 'keys';
public static flags = {
public static flags: flags.Input<FlagsDef> = {
...(isV14() ? cf.dataSetOutputFlags : {}),
help: cf.help,
};
public static authenticated = true;
public async run() {
await this.parse(KeysCmd);
const { flags: options } = this.parse<FlagsDef, {}>(KeysCmd);
const keys = await getBalenaSdk().models.key.getAll();
@ -45,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

@ -15,12 +15,20 @@
* limitations under the License.
*/
import { Args } from '@oclif/core';
import { flags } from '@oclif/command';
import Command from '../command';
import * as cf from '../utils/common-flags';
import { stripIndent } from '../utils/lazy';
import { parseAsLocalHostnameOrIp } from '../utils/validation';
interface FlagsDef {
help?: void;
}
interface ArgsDef {
deviceIpOrHostname?: string;
}
export default class LeaveCmd extends Command {
public static description = stripIndent`
Remove a local device from its balena fleet.
@ -43,16 +51,17 @@ export default class LeaveCmd extends Command {
'$ balena leave 192.168.1.25',
];
public static args = {
deviceIpOrHostname: Args.string({
public static args = [
{
name: 'deviceIpOrHostname',
description: 'the device IP or hostname',
parse: parseAsLocalHostnameOrIp,
}),
};
},
];
public static usage = 'leave [deviceIpOrHostname]';
public static flags = {
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
@ -60,7 +69,7 @@ export default class LeaveCmd extends Command {
public static primary = true;
public async run() {
const { args: params } = await this.parse(LeaveCmd);
const { args: params } = this.parse<FlagsDef, ArgsDef>(LeaveCmd);
const promote = await import('../utils/promote');
const logger = await Command.getLogger();

View File

@ -15,12 +15,20 @@
* limitations under the License.
*/
import { Args } from '@oclif/core';
import { flags } from '@oclif/command';
import { promisify } from 'util';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { stripIndent } from '../../utils/lazy';
interface FlagsDef {
help: void;
}
interface ArgsDef {
target: string;
}
export default class LocalConfigureCmd extends Command {
public static description = stripIndent`
(Re)configure a balenaOS drive or image.
@ -33,16 +41,17 @@ export default class LocalConfigureCmd extends Command {
'$ balena local configure path/to/image.img',
];
public static args = {
target: Args.string({
public static args = [
{
name: 'target',
description: 'path of drive or image to configure',
required: true,
}),
};
},
];
public static usage = 'local configure <target>';
public static flags = {
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
@ -50,7 +59,7 @@ export default class LocalConfigureCmd extends Command {
public static offlineCompatible = true;
public async run() {
const { args: params } = await this.parse(LocalConfigureCmd);
const { args: params } = this.parse<FlagsDef, ArgsDef>(LocalConfigureCmd);
const reconfix = await import('reconfix');
const { denyMount, safeUmount } = await import('../../utils/umount');

View File

@ -15,13 +15,23 @@
* limitations under the License.
*/
import { Args } from '@oclif/core';
import { flags } from '@oclif/command';
import type { BlockDevice } from 'etcher-sdk/build/source-destination';
import Command from '../../command';
import { ExpectedError } from '../../errors';
import * as cf from '../../utils/common-flags';
import { getChalk, getVisuals, stripIndent } from '../../utils/lazy';
interface FlagsDef {
yes: boolean;
drive?: string;
help: void;
}
interface ArgsDef {
image: string;
}
export default class LocalFlashCmd extends Command {
public static description = stripIndent`
Flash an image to a drive.
@ -39,16 +49,17 @@ export default class LocalFlashCmd extends Command {
'$ balena local flash path/to/balenaos.img --drive /dev/disk2 --yes',
];
public static args = {
image: Args.string({
public static args = [
{
name: 'image',
description: 'path to OS image',
required: true,
}),
};
},
];
public static usage = 'local flash <image>';
public static flags = {
public static flags: flags.Input<FlagsDef> = {
drive: cf.drive,
yes: cf.yes,
help: cf.help,
@ -57,7 +68,9 @@ export default class LocalFlashCmd extends Command {
public static offlineCompatible = true;
public async run() {
const { args: params, flags: options } = await this.parse(LocalFlashCmd);
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
LocalFlashCmd,
);
if (process.platform === 'linux') {
const { promisify } = await import('util');

View File

@ -15,12 +15,11 @@
* limitations under the License.
*/
import { Flags, Args } from '@oclif/core';
import { flags } from '@oclif/command';
import Command from '../command';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, stripIndent, getCliForm } from '../utils/lazy';
import { ExpectedError } from '../errors';
import type { WhoamiResult } from 'balena-sdk';
interface FlagsDef {
token: boolean;
@ -31,7 +30,10 @@ interface FlagsDef {
password?: string;
port?: number;
help: void;
hideExperimentalWarning: boolean;
}
interface ArgsDef {
token?: string;
}
export default class LoginCmd extends Command {
@ -57,34 +59,37 @@ export default class LoginCmd extends Command {
'$ balena login --credentials --email johndoe@gmail.com --password secret',
];
public static args = {
token: Args.string({
public static args = [
{
// Capitano allowed -t to be type boolean|string, which oclif does not.
// So -t is now bool, and we check first arg for token content.
name: 'token',
hidden: true,
}),
};
},
];
public static usage = 'login';
public static flags = {
web: Flags.boolean({
public static flags: flags.Input<FlagsDef> = {
web: flags.boolean({
default: false,
char: 'w',
description: 'web-based login',
exclusive: ['token', 'credentials'],
}),
token: Flags.boolean({
token: flags.boolean({
default: false,
char: 't',
description: 'session token or API key',
exclusive: ['web', 'credentials'],
}),
credentials: Flags.boolean({
credentials: flags.boolean({
default: false,
char: 'c',
description: 'credential-based login',
exclusive: ['web', 'token'],
}),
email: Flags.string({
email: flags.string({
char: 'e',
description: 'email',
exclusive: ['user'],
@ -92,35 +97,32 @@ export default class LoginCmd extends Command {
}),
// Capitano version of this command had a second alias for email, 'u'.
// Using an oclif hidden flag to support the same behaviour.
user: Flags.string({
user: flags.string({
char: 'u',
hidden: true,
exclusive: ['email'],
dependsOn: ['credentials'],
}),
password: Flags.string({
password: flags.string({
char: 'p',
description: 'password',
dependsOn: ['credentials'],
}),
port: Flags.integer({
port: flags.integer({
char: 'P',
description:
'TCP port number of local HTTP login server (--web auth only)',
dependsOn: ['web'],
}),
hideExperimentalWarning: Flags.boolean({
char: 'H',
default: false,
description: 'Hides warning for experimental features',
}),
help: cf.help,
};
public static primary = true;
public async run() {
const { flags: options, args: params } = await this.parse(LoginCmd);
const { flags: options, args: params } = this.parse<FlagsDef, ArgsDef>(
LoginCmd,
);
const balena = getBalenaSdk();
const messages = await import('../utils/messages');
@ -135,24 +137,9 @@ export default class LoginCmd extends Command {
console.log(`\nLogging in to ${balenaUrl}`);
await this.doLogin(options, balenaUrl, params.token);
// We can safely assume this won't be undefined as doLogin will throw if this call fails
// We also don't need to worry too much about the amount of calls to whoami
// as these are cached by the SDK
const whoamiResult = (await balena.auth.whoami()) as WhoamiResult;
const username = await balena.auth.whoami();
if (whoamiResult.actorType !== 'user' && !options.hideExperimentalWarning) {
console.info(stripIndent`
----------------------------------------------------------------------------------------
You are logging in with a ${whoamiResult.actorType} key.
This is an experimental feature and many features of the CLI might not work as expected.
We sure hope you know what you are doing.
----------------------------------------------------------------------------------------
`);
}
console.info(
`Successfully logged in as: ${this.getLoggedInMessage(whoamiResult)}`,
);
console.info(`Successfully logged in as: ${username}`);
console.info(`\
Find out about the available commands by running:
@ -162,16 +149,6 @@ Find out about the available commands by running:
${messages.reachingOut}`);
}
private getLoggedInMessage(whoami: WhoamiResult): string {
if (whoami.actorType === 'user') {
return whoami.username;
}
const identifier =
whoami.actorType === 'device' ? whoami.uuid : whoami.slug;
return `${whoami.actorType} ${identifier}`;
}
async doLogin(
loginOptions: FlagsDef,
balenaUrl: string = 'balena-cloud.com',
@ -188,14 +165,7 @@ ${messages.reachingOut}`);
}
const balena = getBalenaSdk();
await balena.auth.loginWithToken(token!);
try {
if (!(await balena.auth.whoami())) {
throw new ExpectedError('Token authentication failed');
}
} catch (err) {
if (process.env.DEBUG) {
console.error(`Get user info failed with: ${err.message}`);
}
if (!(await balena.auth.whoami())) {
throw new ExpectedError('Token authentication failed');
}
return;

View File

@ -29,7 +29,7 @@ export default class LogoutCmd extends Command {
public static usage = 'logout';
public async run() {
await this.parse(LogoutCmd);
this.parse<{}, {}>(LogoutCmd);
await getBalenaSdk().auth.logout();
}
}

View File

@ -15,11 +15,24 @@
* limitations under the License.
*/
import { Flags, Args } from '@oclif/core';
import { flags } from '@oclif/command';
import Command from '../command';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../utils/lazy';
import { LogMessage } from 'balena-sdk';
import { IArg } from '@oclif/parser/lib/args';
interface FlagsDef {
'max-retry'?: number;
tail?: boolean;
service?: string[];
system?: boolean;
help: void;
}
interface ArgsDef {
device: string;
}
const MAX_RETRY = 1000;
@ -54,34 +67,35 @@ export default class LogsCmd extends Command {
'$ balena logs 23c73a1.local --system --service my-service',
];
public static args = {
device: Args.string({
public static args: Array<IArg<any>> = [
{
name: 'device',
description: 'device UUID, IP, or .local address',
required: true,
}),
};
},
];
public static usage = 'logs <device>';
public static flags = {
'max-retry': Flags.integer({
public static flags: flags.Input<FlagsDef> = {
'max-retry': flags.integer({
description: stripIndent`
Maximum number of reconnection attempts on "connection lost" errors
(use 0 to disable auto reconnection).`,
}),
tail: Flags.boolean({
tail: flags.boolean({
default: false,
description: 'continuously stream output',
char: 't',
}),
service: Flags.string({
service: flags.string({
description: stripIndent`
Reject logs not originating from this service.
This can be used in combination with --system or other --service flags.`,
char: 's',
multiple: true,
}),
system: Flags.boolean({
system: flags.boolean({
default: false,
description:
'Only show system logs. This can be used in combination with --service.',
@ -93,7 +107,9 @@ export default class LogsCmd extends Command {
public static primary = true;
public async run() {
const { args: params, flags: options } = await this.parse(LogsCmd);
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
LogsCmd,
);
const balena = getBalenaSdk();
const { serviceIdToName } = await import('../utils/cloud');

View File

@ -15,12 +15,22 @@
* limitations under the License.
*/
import { Flags, Args } from '@oclif/core';
import { flags } from '@oclif/command';
import Command from '../command';
import { ExpectedError } from '../errors';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../utils/lazy';
interface FlagsDef {
device?: string; // device UUID
dev?: string; // Alias for device.
help: void;
}
interface ArgsDef {
note: string;
}
export default class NoteCmd extends Command {
public static description = stripIndent`
Set a device note.
@ -36,17 +46,18 @@ export default class NoteCmd extends Command {
'$ cat note.txt | balena note --device 7cf02a6',
];
public static args = {
note: Args.string({
public static args = [
{
name: 'note',
description: 'note content',
}),
};
},
];
public static usage = 'note <|note>';
public static flags = {
public static flags: flags.Input<FlagsDef> = {
device: { exclusive: ['dev'], ...cf.device },
dev: Flags.string({
dev: flags.string({
exclusive: ['device'],
hidden: true,
}),
@ -58,7 +69,9 @@ export default class NoteCmd extends Command {
public static readStdin = true;
public async run() {
const { args: params, flags: options } = await this.parse(NoteCmd);
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
NoteCmd,
);
params.note = params.note || this.stdin;
@ -75,6 +88,6 @@ export default class NoteCmd extends Command {
const balena = getBalenaSdk();
return balena.models.device.setNote(options.device, params.note);
return balena.models.device.note(options.device!, params.note);
}
}

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.
@ -15,9 +15,17 @@
* limitations under the License.
*/
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';
import { isV14 } from '../utils/version';
interface FlagsDef extends DataSetOutputOptions {
help: void;
}
export default class OrgsCmd extends Command {
public static description = stripIndent`
@ -29,25 +37,29 @@ export default class OrgsCmd extends Command {
public static usage = 'orgs';
public static flags = {
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
...(isV14() ? cf.dataSetOutputFlags : {}),
};
public static authenticated = true;
public async run() {
await this.parse(OrgsCmd);
const { flags: options } = this.parse<FlagsDef, {}>(OrgsCmd);
const { getOwnOrganizations } = await import('../utils/sdk');
// Get organizations
const organizations = await getOwnOrganizations(getBalenaSdk(), {
$select: ['name', 'handle'],
});
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

@ -15,13 +15,24 @@
* limitations under the License.
*/
import { Flags, Args } from '@oclif/core';
import { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getCliForm, stripIndent } from '../../utils/lazy';
import * as _ from 'lodash';
import type { DeviceTypeJson } from 'balena-sdk';
interface FlagsDef {
advanced: boolean;
output: string;
help: void;
}
interface ArgsDef {
image: string;
'device-type': string;
}
export default class OsBuildConfigCmd extends Command {
public static description = stripIndent`
Prepare a configuration file for use by the 'os configure' command.
@ -35,25 +46,27 @@ export default class OsBuildConfigCmd extends Command {
'$ balena os configure ../path/rpi3.img --device 7cf02a6 --config rpi3-config.json',
];
public static args = {
image: Args.string({
public static args = [
{
name: 'image',
description: 'os image',
required: true,
}),
'device-type': Args.string({
},
{
name: 'device-type',
description: 'device type',
required: true,
}),
};
},
];
public static usage = 'os build-config <image> <device-type>';
public static flags = {
advanced: Flags.boolean({
public static flags: flags.Input<FlagsDef> = {
advanced: flags.boolean({
description: 'show advanced configuration options',
char: 'v',
}),
output: Flags.string({
output: flags.string({
description: 'path to output JSON file',
char: 'o',
required: true,
@ -64,7 +77,9 @@ export default class OsBuildConfigCmd extends Command {
public static authenticated = true;
public async run() {
const { args: params, flags: options } = await this.parse(OsBuildConfigCmd);
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
OsBuildConfigCmd,
);
const { writeFile } = (await import('fs')).promises;

View File

@ -15,8 +15,7 @@
* limitations under the License.
*/
import { Flags, Args } from '@oclif/core';
import type { Interfaces } from '@oclif/core';
import { flags } from '@oclif/command';
import type * as BalenaSdk from 'balena-sdk';
import { promisify } from 'util';
import * as _ from 'lodash';
@ -24,27 +23,41 @@ import Command from '../../command';
import { ExpectedError } from '../../errors';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy';
import {
applicationIdInfo,
devModeInfo,
secureBootInfo,
} from '../../utils/messages';
import { applicationIdInfo, devModeInfo } from '../../utils/messages';
const CONNECTIONS_FOLDER = '/system-connections';
type FlagsDef = Interfaces.InferredFlags<typeof OsConfigureCmd.flags>;
interface FlagsDef {
advanced?: boolean;
fleet?: string;
config?: string;
'config-app-update-poll-interval'?: number;
'config-network'?: string;
'config-wifi-key'?: string;
'config-wifi-ssid'?: string;
dev?: boolean; // balenaOS development variant
device?: string; // device UUID
'device-type'?: string;
help?: void;
version?: string;
'system-connection': string[];
'initial-device-name'?: string;
'provisioning-key-name'?: string;
}
interface ArgsDef {
image: string;
}
interface Answers {
appUpdatePollInterval: number; // in minutes
developmentMode?: boolean; // balenaOS development variant
secureBoot?: boolean;
deviceType: string; // e.g. "raspberrypi3"
network: 'ethernet' | 'wifi';
version: string; // e.g. "2.32.0+rev1"
wifiSsid?: string;
wifiKey?: string;
provisioningKeyName?: string;
provisioningKeyExpiryDate?: string;
}
export default class OsConfigureCmd extends Command {
@ -65,8 +78,6 @@ export default class OsConfigureCmd extends Command {
${devModeInfo.split('\n').join('\n\t\t')}
${secureBootInfo.split('\n').join('\n\t\t')}
The --system-connection (-c) option is used to inject NetworkManager connection
profiles for additional network interfaces, such as cellular/GSM or additional
WiFi or ethernet connections. This option may be passed multiple times in case there
@ -90,85 +101,75 @@ export default class OsConfigureCmd extends Command {
'$ balena os configure ../path/rpi3.img -f MyFinFleet --device-type raspberrypi3 --config myWifiConfig.json',
];
public static args = {
image: Args.string({
public static args = [
{
name: 'image',
required: true,
description: 'path to a balenaOS image file, e.g. "rpi3.img"',
}),
};
},
];
public static usage = 'os configure <image>';
public static flags = {
advanced: Flags.boolean({
public static flags: flags.Input<FlagsDef> = {
advanced: flags.boolean({
char: 'v',
description:
'ask advanced configuration questions (when in interactive mode)',
}),
fleet: { ...cf.fleet, exclusive: ['device'] },
config: Flags.string({
config: flags.string({
description:
'path to a pre-generated config.json file to be injected in the OS image',
exclusive: ['provisioning-key-name', 'provisioning-key-expiry-date'],
exclusive: ['provisioning-key-name'],
}),
'config-app-update-poll-interval': Flags.integer({
'config-app-update-poll-interval': flags.integer({
description:
'supervisor cloud polling interval in minutes (e.g. for variable updates)',
}),
'config-network': Flags.string({
'config-network': flags.string({
description: 'device network type (non-interactive configuration)',
options: ['ethernet', 'wifi'],
}),
'config-wifi-key': Flags.string({
'config-wifi-key': flags.string({
description: 'WiFi key (password) (non-interactive configuration)',
}),
'config-wifi-ssid': Flags.string({
'config-wifi-ssid': flags.string({
description: 'WiFi SSID (network name) (non-interactive configuration)',
}),
dev: cf.dev,
secureBoot: cf.secureBoot,
device: {
...cf.device,
exclusive: [
'fleet',
'provisioning-key-name',
'provisioning-key-expiry-date',
],
},
'device-type': Flags.string({
device: { ...cf.device, exclusive: ['fleet', 'provisioning-key-name'] },
'device-type': flags.string({
description:
'device type slug (e.g. "raspberrypi3") to override the fleet device type',
}),
'initial-device-name': Flags.string({
'initial-device-name': flags.string({
description:
'This option will set the device name when the device provisions',
}),
version: Flags.string({
version: flags.string({
description: 'balenaOS version, for example "2.32.0" or "2.44.0+rev1"',
}),
'system-connection': Flags.string({
'system-connection': flags.string({
multiple: true,
char: 'c',
required: false,
description:
"paths to local files to place into the 'system-connections' directory",
}),
'provisioning-key-name': Flags.string({
'provisioning-key-name': flags.string({
description: 'custom key name assigned to generated provisioning api key',
exclusive: ['config', 'device'],
}),
'provisioning-key-expiry-date': Flags.string({
description:
'expiry date assigned to generated provisioning api key (format: YYYY-MM-DD)',
exclusive: ['config', 'device'],
}),
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params, flags: options } = await this.parse(OsConfigureCmd);
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
OsConfigureCmd,
);
await validateOptions(options);
@ -180,7 +181,7 @@ export default class OsConfigureCmd extends Command {
const helpers = await import('../../utils/helpers');
const { getApplication } = await import('../../utils/sdk');
let app: ApplicationWithDeviceTypeSlug | undefined;
let app: ApplicationWithDeviceType | undefined;
let device;
let deviceTypeSlug: string;
@ -199,8 +200,8 @@ export default class OsConfigureCmd extends Command {
$expand: {
is_for__device_type: { $select: 'slug' },
},
})) as ApplicationWithDeviceTypeSlug;
await checkDeviceTypeCompatibility(options, app);
})) as ApplicationWithDeviceType;
await checkDeviceTypeCompatibility(balena, options, app);
deviceTypeSlug =
options['device-type'] || app.is_for__device_type[0].slug;
}
@ -223,15 +224,6 @@ export default class OsConfigureCmd extends Command {
const { validateDevOptionAndWarn } = await import('../../utils/config');
await validateDevOptionAndWarn(options.dev, osVersion);
const { validateSecureBootOptionAndWarn } = await import(
'../../utils/config'
);
await validateSecureBootOptionAndWarn(
options.secureBoot,
deviceTypeSlug,
osVersion,
);
const answers: Answers = await askQuestionsForDeviceType(
deviceTypeManifest,
options,
@ -242,9 +234,7 @@ export default class OsConfigureCmd extends Command {
}
answers.version = osVersion;
answers.developmentMode = options.dev;
answers.secureBoot = options.secureBoot;
answers.provisioningKeyName = options['provisioning-key-name'];
answers.provisioningKeyExpiryDate = options['provisioning-key-expiry-date'];
if (_.isEmpty(configJson)) {
if (device) {
@ -356,19 +346,17 @@ async function getOsVersionFromImage(
* @param app Balena SDK Application model object
*/
async function checkDeviceTypeCompatibility(
sdk: BalenaSdk.BalenaSDK,
options: FlagsDef,
app: {
is_for__device_type: [Pick<BalenaSdk.DeviceType, 'slug'>];
},
app: ApplicationWithDeviceType,
) {
if (options['device-type']) {
const [appDeviceType, optionDeviceType] = await Promise.all([
sdk.models.device.getManifestBySlug(app.is_for__device_type[0].slug),
sdk.models.device.getManifestBySlug(options['device-type']),
]);
const helpers = await import('../../utils/helpers');
if (
!(await helpers.areDeviceTypesCompatible(
app.is_for__device_type[0].slug,
options['device-type'],
))
) {
if (!helpers.areDeviceTypesCompatible(appDeviceType, optionDeviceType)) {
throw new ExpectedError(
`Device type ${options['device-type']} is incompatible with fleet ${options.fleet}`,
);

View File

@ -15,11 +15,21 @@
* limitations under the License.
*/
import { Flags, Args } from '@oclif/core';
import { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { stripIndent } from '../../utils/lazy';
interface FlagsDef {
output: string;
version?: string;
help: void;
}
interface ArgsDef {
type: string;
}
export default class OsDownloadCmd extends Command {
public static description = stripIndent`
Download an unconfigured OS image.
@ -43,11 +53,9 @@ export default class OsDownloadCmd extends Command {
`;
public static examples = [
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img',
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version 2.101.7',
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version 2022.7.0',
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version ^2.90.0',
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version 2.60.1+rev1',
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version 2.60.1+rev1.dev',
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version ^2.60.0',
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version 2021.10.2.prod',
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version latest',
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version default',
@ -55,22 +63,23 @@ export default class OsDownloadCmd extends Command {
'$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version menu-esr',
];
public static args = {
type: Args.string({
public static args = [
{
name: 'type',
description: 'the device type',
required: true,
}),
};
},
];
public static usage = 'os download <type>';
public static flags = {
output: Flags.string({
public static flags: flags.Input<FlagsDef> = {
output: flags.string({
description: 'output path',
char: 'o',
required: true,
}),
version: Flags.string({
version: flags.string({
description: stripIndent`
version number (ESR or non-ESR versions),
or semver range (non-ESR versions only),
@ -85,7 +94,9 @@ export default class OsDownloadCmd extends Command {
};
public async run() {
const { args: params, flags: options } = await this.parse(OsDownloadCmd);
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
OsDownloadCmd,
);
// balenaOS ESR versions require user authentication
if (options.version) {

View File

@ -15,11 +15,22 @@
* limitations under the License.
*/
import { Args } from '@oclif/core';
import { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getCliForm, stripIndent } from '../../utils/lazy';
interface FlagsDef {
type: string;
drive?: string;
yes: boolean;
help: void;
}
interface ArgsDef {
image: string;
}
const INIT_WARNING_MESSAGE = `
Note: Initializing the device may ask for administrative permissions
@ -31,9 +42,7 @@ export default class OsInitializeCmd extends Command {
Initialize an os image for a device.
Initialize an os image for a device with a previously
configured operating system image and flash the
an external storage drive or the device's storage
medium depending on the device type.
configured operating system image.
${INIT_WARNING_MESSAGE}
`;
@ -41,16 +50,17 @@ export default class OsInitializeCmd extends Command {
'$ balena os initialize ../path/rpi.img --type raspberry-pi',
];
public static args = {
image: Args.string({
public static args = [
{
name: 'image',
description: 'path to OS image',
required: true,
}),
};
},
];
public static usage = 'os initialize <image>';
public static flags = {
public static flags: flags.Input<FlagsDef> = {
type: cf.deviceType,
drive: cf.drive,
yes: cf.yes,
@ -60,7 +70,9 @@ export default class OsInitializeCmd extends Command {
public static authenticated = true;
public async run() {
const { args: params, flags: options } = await this.parse(OsInitializeCmd);
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
OsInitializeCmd,
);
const { getManifest, sudo } = await import('../../utils/helpers');

View File

@ -15,11 +15,20 @@
* limitations under the License.
*/
import { Flags, Args } from '@oclif/core';
import { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { stripIndent } from '../../utils/lazy';
interface FlagsDef {
esr?: boolean;
help: void;
}
interface ArgsDef {
type: string;
}
export default class OsVersionsCmd extends Command {
public static description = stripIndent`
Show available balenaOS versions for the given device type.
@ -33,25 +42,28 @@ export default class OsVersionsCmd extends Command {
public static examples = ['$ balena os versions raspberrypi3'];
public static args = {
type: Args.string({
public static args = [
{
name: 'type',
description: 'device type',
required: true,
}),
};
},
];
public static usage = 'os versions <type>';
public static flags = {
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
esr: Flags.boolean({
esr: flags.boolean({
description: 'select balenaOS ESR versions',
default: false,
}),
};
public async run() {
const { args: params, flags: options } = await this.parse(OsVersionsCmd);
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
OsVersionsCmd,
);
const { formatOsVersion, getOsVersions } = await import(
'../../utils/cloud'

View File

@ -25,21 +25,30 @@ import {
stripIndent,
} from '../utils/lazy';
import { applicationIdInfo } from '../utils/messages';
import type { DockerConnectionCliFlags } from '../utils/docker';
import { dockerConnectionCliFlags } from '../utils/docker';
import { parseAsInteger } from '../utils/validation';
import { Flags, Args } from '@oclif/core';
import { flags } from '@oclif/command';
import * as _ from 'lodash';
import type {
Application,
BalenaSDK,
PineExpand,
PineOptions,
PineTypedResult,
Release,
} from 'balena-sdk';
import type { Application, BalenaSDK, PineExpand, Release } from 'balena-sdk';
import type { Preloader } from 'balena-preload';
interface FlagsDef extends DockerConnectionCliFlags {
fleet?: string;
commit?: string;
'splash-image'?: string;
'dont-check-arch': boolean;
'pin-device-to-release': boolean;
'additional-space'?: number;
'add-certificate'?: string[];
help: void;
}
interface ArgsDef {
image: string;
}
export default class PreloadCmd extends Command {
public static description = stripIndent`
Preload a release on a disk image (or Edison zip archive).
@ -73,18 +82,19 @@ export default class PreloadCmd extends Command {
'$ balena preload balena.img',
];
public static args = {
image: Args.string({
public static args = [
{
name: 'image',
description: 'the image file path',
required: true,
}),
};
},
];
public static usage = 'preload <image>';
public static flags = {
public static flags: flags.Input<FlagsDef> = {
fleet: cf.fleet,
commit: Flags.string({
commit: flags.string({
description: `\
The commit hash of the release to preload. Use "current" to specify the current
release (ignored if no appId is given). The current release is usually also the
@ -95,27 +105,27 @@ https://github.com/balena-io-examples/staged-releases\
`,
char: 'c',
}),
'splash-image': Flags.string({
'splash-image': flags.string({
description: 'path to a png image to replace the splash screen',
char: 's',
}),
'dont-check-arch': Flags.boolean({
'dont-check-arch': flags.boolean({
default: false,
description:
'disable architecture compatibility check between image and fleet',
}),
'pin-device-to-release': Flags.boolean({
allowNo: true,
'pin-device-to-release': flags.boolean({
default: false,
description:
'pin the preloaded device to the preloaded release on provision',
char: 'p',
}),
'additional-space': Flags.integer({
'additional-space': flags.integer({
description:
'expand the image by this amount of bytes instead of automatically estimating the required amount',
parse: async (x) => parseAsInteger(x, 'additional-space'),
parse: (x) => parseAsInteger(x, 'additional-space'),
}),
'add-certificate': Flags.string({
'add-certificate': flags.string({
description: `\
Add the given certificate (in PEM format) to /etc/ssl/certs in the preloading container.
The file name must end with '.crt' and must not be already contained in the preloader's
@ -127,14 +137,14 @@ Can be repeated to add multiple certificates.\
...dockerConnectionCliFlags,
// Redefining --dockerPort here (defined already in dockerConnectionCliFlags)
// without -p alias, to avoid clash with -p alias of pin-device-to-release
dockerPort: Flags.integer({
dockerPort: flags.integer({
description:
'Docker daemon TCP port number (hint: 2375 for balena devices)',
parse: async (p) => parseAsInteger(p, 'dockerPort'),
parse: (p) => parseAsInteger(p, 'dockerPort'),
}),
// Not supporting -h for help, because of clash with -h in DockerCliFlags
// Revisit this in future release.
help: Flags.help({}),
help: flags.help({}),
};
public static authenticated = true;
@ -142,7 +152,9 @@ Can be repeated to add multiple certificates.\
public static primary = true;
public async run() {
const { args: params, flags: options } = await this.parse(PreloadCmd);
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
PreloadCmd,
);
const balena = getBalenaSdk();
const balenaPreload = await import('balena-preload');
@ -175,7 +187,7 @@ Can be repeated to add multiple certificates.\
: undefined;
const progressBars: {
[key: string]: InstanceType<ReturnType<typeof getVisuals>['Progress']>;
[key: string]: ReturnType<typeof getVisuals>['Progress'];
} = {};
const progressHandler = function (event: {
@ -189,7 +201,7 @@ Can be repeated to add multiple certificates.\
};
const spinners: {
[key: string]: InstanceType<ReturnType<typeof getVisuals>['Spinner']>;
[key: string]: ReturnType<typeof getVisuals>['Spinner'];
} = {};
const spinnerHandler = function (event: { name: string; action: string }) {
@ -211,7 +223,7 @@ Can be repeated to add multiple certificates.\
const splashImage = options['splash-image'];
const additionalSpace = options['additional-space'];
const dontCheckArch = options['dont-check-arch'] || false;
const pinDevice = options['pin-device-to-release'];
const pinDevice = options['pin-device-to-release'] || false;
if (dontCheckArch && !fleetSlug) {
throw new ExpectedError(
@ -230,7 +242,7 @@ Can be repeated to add multiple certificates.\
const dockerUtils = await import('../utils/docker');
const docker = await dockerUtils.getDocker(options);
const preloader = new balenaPreload.Preloader(
undefined,
null,
docker,
fleetSlug,
commit,
@ -238,7 +250,7 @@ Can be repeated to add multiple certificates.\
splashImage,
undefined, // TODO: Currently always undefined, investigate approach in ssh command.
dontCheckArch,
pinDevice ?? false,
pinDevice,
certificates,
additionalSpace,
);
@ -276,7 +288,7 @@ Can be repeated to add multiple certificates.\
preloader.on('error', reject);
resolve(
this.prepareAndPreload(preloader, balena, {
slug: fleetSlug,
appId: fleetSlug,
commit,
pinDevice,
}),
@ -296,7 +308,7 @@ Can be repeated to add multiple certificates.\
}
}
readonly applicationExpandOptions = {
readonly applicationExpandOptions: PineExpand<Application> = {
owns__release: {
$select: ['id', 'commit', 'end_timestamp', 'composition'],
$expand: {
@ -317,7 +329,7 @@ Can be repeated to add multiple certificates.\
should_be_running__release: {
$select: 'commit',
},
} satisfies PineExpand<Application>;
};
isCurrentCommit(commit: string) {
return commit === 'latest' || commit === 'current';
@ -331,7 +343,7 @@ Can be repeated to add multiple certificates.\
} catch {
throw new Error(`Device type "${deviceTypeSlug}" not found in API query`);
}
const options = {
return (await balena.models.application.getAllDirectlyAccessible({
$select: ['id', 'slug', 'should_track_latest_release'],
$expand: this.applicationExpandOptions,
$filter: {
@ -376,10 +388,11 @@ Can be repeated to add multiple certificates.\
},
},
$orderby: 'slug asc',
} satisfies PineOptions<Application>;
return (await balena.models.application.getAllDirectlyAccessible(
options,
)) as Array<PineTypedResult<Application, typeof options>>;
})) as Array<
ApplicationWithDeviceType & {
should_be_running__release: [Release?];
}
>;
}
async selectApplication(deviceTypeSlug: string) {
@ -429,16 +442,16 @@ Can be repeated to add multiple certificates.\
}
async offerToDisableAutomaticUpdates(
application: Pick<Application, 'id' | 'should_track_latest_release'>,
application: Application,
commit: string,
pinDevice: boolean | undefined,
pinDevice: boolean,
) {
const balena = getBalenaSdk();
if (
this.isCurrentCommit(commit) ||
!application.should_track_latest_release ||
pinDevice != null
pinDevice
) {
return;
}
@ -457,9 +470,8 @@ through the web dashboard or programatically through the balena API / SDK.
Documentation about release policies and pinning can be found at:
https://www.balena.io/docs/learn/deploy/release-strategy/release-policy/
Alternatively, the --pin-device-to-release or --no-pin-device-to-release flags may be used
to avoid this interactive confirmation and pin only the preloaded device to the selected release
or keep it unpinned respectively.
Alternatively, the --pin-device-to-release flag may be used to pin only the
preloaded device to the selected release.
Would you like to disable automatic updates for this fleet now?\
`;
@ -479,28 +491,28 @@ Would you like to disable automatic updates for this fleet now?\
});
}
async getAppWithReleases(balenaSdk: BalenaSDK, slug: string) {
async getAppWithReleases(balenaSdk: BalenaSDK, appId: string) {
const { getApplication } = await import('../utils/sdk');
return await getApplication(balenaSdk, slug, {
return (await getApplication(balenaSdk, appId, {
$expand: this.applicationExpandOptions,
});
})) as Application & { should_be_running__release: [Release?] };
}
async prepareAndPreload(
preloader: Preloader,
balenaSdk: BalenaSDK,
options: {
slug?: string;
appId?: string;
commit?: string;
pinDevice?: boolean;
pinDevice: boolean;
},
) {
await preloader.prepare();
const application = options.slug
? await this.getAppWithReleases(balenaSdk, options.slug)
: await this.selectApplication(preloader.config!.deviceType);
const application = options.appId
? await this.getAppWithReleases(balenaSdk, options.appId)
: await this.selectApplication(preloader.config.deviceType);
let commit: string; // commit hash or the strings 'latest' or 'current'
@ -511,7 +523,7 @@ Would you like to disable automatic updates for this fleet now?\
if (this.isCurrentCommit(options.commit)) {
if (!appCommit) {
throw new Error(
`Unexpected empty commit hash for fleet slug "${application.slug}"`,
`Unexpected empty commit hash for fleet ID "${application.id}"`,
);
}
// handle `--commit current` (and its `--commit latest` synonym)

View File

@ -15,15 +15,14 @@
* limitations under the License.
*/
import { Flags, Args } from '@oclif/core';
import type { Interfaces } from '@oclif/core';
import { flags } from '@oclif/command';
import Command from '../command';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../utils/lazy';
import { dockerignoreHelp, registrySecretsHelp } from '../utils/messages';
import type { BalenaSDK } from 'balena-sdk';
import { ExpectedError, instanceOf } from '../errors';
import { RegistrySecrets } from '@balena/compose/dist/multibuild';
import { RegistrySecrets } from 'resin-multibuild';
import { lowercaseIfSlug } from '../utils/normalization';
import {
applyReleaseTagKeysAndValues,
@ -35,7 +34,29 @@ enum BuildTarget {
Device,
}
type FlagsDef = Interfaces.InferredFlags<typeof PushCmd.flags>;
interface ArgsDef {
fleetOrDevice: string;
}
interface FlagsDef {
source: string;
emulated: boolean;
dockerfile?: string; // DeviceDeployOptions.dockerfilePath (alternative Dockerfile)
nocache: boolean;
pull: boolean;
'noparent-check': boolean;
'registry-secrets'?: string;
nolive: boolean;
detached: boolean;
service?: string[];
system: boolean;
env?: string[];
'noconvert-eol': boolean;
'multi-dockerignore': boolean;
'release-tag'?: string[];
draft: boolean;
help: void;
}
export default class PushCmd extends Command {
public static description = stripIndent`
@ -76,7 +97,6 @@ export default class PushCmd extends Command {
'$ balena push myFleet',
'$ balena push myFleet --source <source directory>',
'$ balena push myFleet -s <source directory>',
'$ balena push myFleet --source <source directory> --note "this is the note for this release"',
'$ balena push myFleet --release-tag key1 "" key2 "value2 with spaces"',
'$ balena push myorg/myfleet',
'',
@ -90,26 +110,27 @@ export default class PushCmd extends Command {
'$ balena push 23c73a1.local --system --service my-service',
];
public static args = {
fleetOrDevice: Args.string({
public static args = [
{
name: 'fleetOrDevice',
description:
'fleet name or slug, or local device IP address or ".local" hostname',
required: true,
parse: lowercaseIfSlug,
}),
};
},
];
public static usage = 'push <fleetOrDevice>';
public static flags = {
source: Flags.string({
public static flags: flags.Input<FlagsDef> = {
source: flags.string({
description: stripIndent`
Source directory to be sent to balenaCloud or balenaOS device
(default: current working dir)`,
char: 's',
default: '.',
}),
emulated: Flags.boolean({
emulated: flags.boolean({
description: stripIndent`
Don't use the faster, native balenaCloud ARM builders; force slower QEMU ARM
emulation on Intel x86-64 builders. This flag is sometimes used to investigate
@ -117,11 +138,11 @@ export default class PushCmd extends Command {
char: 'e',
default: false,
}),
dockerfile: Flags.string({
dockerfile: flags.string({
description:
'Alternative Dockerfile name/path, relative to the source folder',
}),
nocache: Flags.boolean({
nocache: flags.boolean({
description: stripIndent`
Don't use cached layers of previously built images for this project. This
ensures that the latest base image and packages are pulled. Note that build
@ -132,18 +153,18 @@ export default class PushCmd extends Command {
char: 'c',
default: false,
}),
pull: Flags.boolean({
pull: flags.boolean({
description: stripIndent`
When pushing to a local device, force the base images to be pulled again.
Currently this option is ignored when pushing to the balenaCloud builders.`,
default: false,
}),
'noparent-check': Flags.boolean({
'noparent-check': flags.boolean({
description: stripIndent`
Disable project validation check of 'docker-compose.yml' file in parent folder`,
default: false,
}),
'registry-secrets': Flags.string({
'registry-secrets': flags.string({
description: stripIndent`
Path to a local YAML or JSON file containing Docker registry passwords used
to pull base images. Note that if registry-secrets are not provided on the
@ -151,15 +172,15 @@ export default class PushCmd extends Command {
used (usually $HOME/.balena/secrets.yml|.json)`,
char: 'R',
}),
nolive: Flags.boolean({
nolive: flags.boolean({
description: stripIndent`
Don't run a live session on this push. The filesystem will not be monitored,
and changes will not be synchronized to any running containers. Note that both
this flag and --detached are required to cause the process to end once the
this flag and --detached and required to cause the process to end once the
initial build has completed.`,
default: false,
}),
detached: Flags.boolean({
detached: flags.boolean({
description: stripIndent`
When pushing to the cloud, this option will cause the build to start, then
return execution back to the shell, with the status and release ID (if
@ -168,20 +189,20 @@ export default class PushCmd extends Command {
char: 'd',
default: false,
}),
service: Flags.string({
service: flags.string({
description: stripIndent`
Reject logs not originating from this service.
This can be used in combination with --system and other --service flags.
Only valid when pushing to a local mode device.`,
multiple: true,
}),
system: Flags.boolean({
system: flags.boolean({
description: stripIndent`
Only show system logs. This can be used in combination with --service.
Only valid when pushing to a local mode device.`,
default: false,
}),
env: Flags.string({
env: flags.string({
description: stripIndent`
When performing a push to device, run the built containers with environment
variables provided with this argument. Environment variables can be applied
@ -193,17 +214,17 @@ export default class PushCmd extends Command {
`,
multiple: true,
}),
'noconvert-eol': Flags.boolean({
'noconvert-eol': flags.boolean({
description: `Don't convert line endings from CRLF (Windows format) to LF (Unix format).`,
default: false,
}),
'multi-dockerignore': Flags.boolean({
'multi-dockerignore': flags.boolean({
description:
'Have each service use its own .dockerignore file. See "balena help push".',
char: 'm',
default: false,
}),
'release-tag': Flags.string({
'release-tag': flags.string({
description: stripIndent`
Set release tags if the image build is successful (balenaCloud only). Multiple
arguments may be provided, alternating tag keys and values (see examples).
@ -212,7 +233,7 @@ export default class PushCmd extends Command {
multiple: true,
exclusive: ['detached'],
}),
draft: Flags.boolean({
draft: flags.boolean({
description: stripIndent`
Instruct the builder to create the release as a draft. Draft releases are ignored
by the 'track latest' release policy but can be used through release pinning.
@ -220,14 +241,15 @@ export default class PushCmd extends Command {
as final by default unless this option is given.`,
default: false,
}),
note: Flags.string({ description: 'The notes for this release' }),
help: cf.help,
};
public static primary = true;
public async run() {
const { args: params, flags: options } = await this.parse(PushCmd);
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
PushCmd,
);
const logger = await Command.getLogger();
logger.logDebug(`Using build source directory: ${options.source} `);
@ -303,7 +325,7 @@ export default class PushCmd extends Command {
]);
const application = await getApplication(sdk, appNameOrSlug, {
$select: 'slug',
$select: ['app_name', 'slug'],
});
const opts = {
@ -332,9 +354,6 @@ export default class PushCmd extends Command {
releaseTagKeys,
releaseTagValues,
);
if (options.note) {
await sdk.models.release.setNote(releaseId, options.note);
}
} else if (releaseTagKeys.length > 0) {
throw new Error(stripIndent`
A release ID could not be parsed out of the builder's output.

View File

@ -15,10 +15,19 @@
* limitations under the License.
*/
import { commitOrIdArg } from '.';
import { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { tryAsInteger } from '../../utils/validation';
interface FlagsDef {
help: void;
}
interface ArgsDef {
commitOrId: string | number;
}
export default class ReleaseFinalizeCmd extends Command {
public static description = stripIndent`
@ -42,21 +51,23 @@ export default class ReleaseFinalizeCmd extends Command {
public static usage = 'release finalize <commitOrId>';
public static flags = {
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static args = {
commitOrId: commitOrIdArg({
public static args = [
{
name: 'commitOrId',
description: 'the commit or ID of the release to finalize',
required: true,
}),
};
parse: (commitOrId: string) => tryAsInteger(commitOrId),
},
];
public static authenticated = true;
public async run() {
const { args: params } = await this.parse(ReleaseFinalizeCmd);
const { args: params } = this.parse<FlagsDef, ArgsDef>(ReleaseFinalizeCmd);
const balena = getBalenaSdk();

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.
@ -15,17 +15,25 @@
* limitations under the License.
*/
import { Flags, Args } from '@oclif/core';
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 * as BalenaSdk from 'balena-sdk';
import jsyaml = require('js-yaml');
import { tryAsInteger } from '../../utils/validation';
import type { DataOutputOptions } from '../../framework';
export const commitOrIdArg = Args.custom({
parse: async (commitOrId: string) => tryAsInteger(commitOrId),
});
import { isV14 } from '../../utils/version';
interface FlagsDef extends DataOutputOptions {
help: void;
composition?: boolean;
}
interface ArgsDef {
commitOrId: string | number;
}
export default class ReleaseCmd extends Command {
public static description = stripIndent`
@ -38,50 +46,54 @@ export default class ReleaseCmd extends Command {
public static usage = 'release <commitOrId>';
public static flags = {
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
composition: Flags.boolean({
composition: flags.boolean({
default: false,
char: 'c',
description: 'Return the release composition',
exclusive: ['json', 'fields'],
}),
...(isV14() ? cf.dataOutputFlags : {}),
};
public static args = {
commitOrId: commitOrIdArg({
public static args = [
{
name: 'commitOrId',
description: 'the commit or ID of the release to get information',
required: true,
}),
};
parse: (commitOrId: string) => tryAsInteger(commitOrId),
},
];
public static authenticated = true;
public async run() {
const { args: params, flags: options } = await this.parse(ReleaseCmd);
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
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',
@ -94,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: {
@ -107,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,70 +0,0 @@
/**
* @license
* Copyright 2016-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 { commitOrIdArg } from '.';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
export default class ReleaseInvalidateCmd extends Command {
public static description = stripIndent`
Invalidate a release.
Invalidate a release.
Invalid releases are not automatically deployed to devices tracking the latest
release. For an invalid release to be deployed to a device, the device should be
explicity pinned to that release.
`;
public static examples = [
'$ balena release invalidate a777f7345fe3d655c1c981aa642e5555',
'$ balena release invalidate 1234567',
];
public static usage = 'release invalidate <commitOrId>';
public static flags = {
help: cf.help,
};
public static args = {
commitOrId: commitOrIdArg({
description: 'the commit or ID of the release to invalidate',
required: true,
}),
};
public static authenticated = true;
public async run() {
const { args: params } = await this.parse(ReleaseInvalidateCmd);
const balena = getBalenaSdk();
const release = await balena.models.release.get(params.commitOrId, {
$select: ['id', 'is_invalidated'],
});
if (release.is_invalidated) {
console.log(`Release ${params.commitOrId} is already invalidated!`);
return;
}
await balena.models.release.setIsInvalidated(release.id, true);
console.log(`Release ${params.commitOrId} invalidated`);
}
}

View File

@ -1,69 +0,0 @@
/**
* @license
* Copyright 2016-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 { commitOrIdArg } from '.';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
export default class ReleaseValidateCmd extends Command {
public static description = stripIndent`
Validate a release.
Validate a release.
Valid releases are automatically deployed to devices tracking the latest
release if they are finalized.
`;
public static examples = [
'$ balena release validate a777f7345fe3d655c1c981aa642e5555',
'$ balena release validate 1234567',
];
public static usage = 'release validate <commitOrId>';
public static flags = {
help: cf.help,
};
public static args = {
commitOrId: commitOrIdArg({
description: 'the commit or ID of the release to validate',
required: true,
}),
};
public static authenticated = true;
public async run() {
const { args: params } = await this.parse(ReleaseValidateCmd);
const balena = getBalenaSdk();
const release = await balena.models.release.get(params.commitOrId, {
$select: ['id', 'is_invalidated'],
});
if (!release.is_invalidated) {
console.log(`Release ${params.commitOrId} is already validated!`);
return;
}
await balena.models.release.setIsInvalidated(release.id, false);
console.log(`Release ${params.commitOrId} validated`);
}
}

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.
@ -15,12 +15,23 @@
* limitations under the License.
*/
import { Args } from '@oclif/core';
import { flags } from '@oclif/command';
import Command from '../command';
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';
import { isV14 } from '../utils/version';
interface FlagsDef extends DataSetOutputOptions {
help: void;
}
interface ArgsDef {
fleet: string;
}
export default class ReleasesCmd extends Command {
public static description = stripIndent`
@ -34,21 +45,25 @@ export default class ReleasesCmd extends Command {
public static usage = 'releases <fleet>';
public static flags = {
public static flags: flags.Input<FlagsDef> = {
...(isV14() ? cf.dataOutputFlags : {}),
help: cf.help,
};
public static args = {
fleet: Args.string({
public static args = [
{
name: 'fleet',
description: 'fleet name or slug (preferred)',
required: true,
}),
};
},
];
public static authenticated = true;
public async run() {
const { args: params } = await this.parse(ReleasesCmd);
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
ReleasesCmd,
);
const fields: Array<keyof BalenaSdk.Release> = [
'id',
@ -67,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

@ -15,11 +15,19 @@
* limitations under the License.
*/
import { Flags } from '@oclif/core';
import { flags } from '@oclif/command';
import type { LocalBalenaOsDevice } from 'balena-sync';
import Command from '../command';
import * as cf from '../utils/common-flags';
import { getCliUx, stripIndent } from '../utils/lazy';
interface FlagsDef {
json?: boolean;
verbose: boolean;
timeout?: number;
help: void;
}
export default class ScanCmd extends Command {
public static description = stripIndent`
Scan for balenaOS devices on your local network.
@ -40,18 +48,18 @@ export default class ScanCmd extends Command {
public static usage = 'scan';
public static flags = {
verbose: Flags.boolean({
public static flags: flags.Input<FlagsDef> = {
verbose: flags.boolean({
default: false,
char: 'v',
description: 'display full info',
}),
timeout: Flags.integer({
timeout: flags.integer({
char: 't',
description: 'scan timeout in seconds',
}),
help: cf.help,
json: Flags.boolean({
json: flags.boolean({
default: false,
char: 'j',
description: 'produce JSON output instead of tabular output',
@ -64,14 +72,14 @@ export default class ScanCmd extends Command {
public async run() {
const _ = await import('lodash');
const { discoverLocalBalenaOsDevices } = await import('../utils/discover');
const { discover } = await import('balena-sync');
const prettyjson = await import('prettyjson');
const dockerUtils = await import('../utils/docker');
const dockerPort = 2375;
const dockerTimeout = 2000;
const { flags: options } = await this.parse(ScanCmd);
const { flags: options } = this.parse<FlagsDef, {}>(ScanCmd);
const discoverTimeout =
options.timeout != null ? options.timeout * 1000 : undefined;
@ -80,7 +88,8 @@ export default class ScanCmd extends Command {
const ux = getCliUx();
ux.action.start('Scanning for local balenaOS devices');
const localDevices = await discoverLocalBalenaOsDevices(discoverTimeout);
const localDevices: LocalBalenaOsDevice[] =
await discover.discoverLocalBalenaOsDevices(discoverTimeout);
const engineReachableDevices: boolean[] = await Promise.all(
localDevices.map(async ({ address }: { address: string }) => {
const docker = await dockerUtils.createClient({
@ -97,7 +106,7 @@ export default class ScanCmd extends Command {
}),
);
const developmentDevices = localDevices.filter(
const developmentDevices: LocalBalenaOsDevice[] = localDevices.filter(
(_localDevice, index) => engineReachableDevices[index],
);
@ -107,15 +116,18 @@ export default class ScanCmd extends Command {
_.isEqual,
);
const productionDevicesInfo = productionDevices.map((device) => {
return {
host: device.host,
address: device.address,
osVariant: 'production',
dockerInfo: undefined,
dockerVersion: undefined,
};
});
const productionDevicesInfo = _.map(
productionDevices,
(device: LocalBalenaOsDevice) => {
return {
host: device.host,
address: device.address,
osVariant: 'production',
dockerInfo: undefined,
dockerVersion: undefined,
};
},
);
// Query devices for info
const devicesInfo = await Promise.all(

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.
@ -15,9 +15,17 @@
* limitations under the License.
*/
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';
import { isV14 } from '../utils/version';
interface FlagsDef extends DataOutputOptions {
help: void;
}
export default class SettingsCmd extends Command {
public static description = stripIndent`
@ -29,16 +37,28 @@ export default class SettingsCmd extends Command {
public static usage = 'settings';
public static flags = {
public static flags: flags.Input<FlagsDef> = {
...(isV14() ? cf.dataOutputFlags : {}),
help: cf.help,
};
public async run() {
await this.parse(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

@ -15,11 +15,25 @@
* limitations under the License.
*/
import { Flags, Args } from '@oclif/core';
import { flags } from '@oclif/command';
import Command from '../command';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../utils/lazy';
import { parseAsInteger, validateLocalHostnameOrIp } from '../utils/validation';
import * as BalenaSdk from 'balena-sdk';
interface FlagsDef {
port?: number;
tty: boolean;
verbose: boolean;
noproxy: boolean;
help: void;
}
interface ArgsDef {
fleetOrDevice: string;
service?: string;
}
export default class SshCmd extends Command {
public static description = stripIndent`
@ -60,39 +74,42 @@ export default class SshCmd extends Command {
'$ echo "uptime; exit;" | balena ssh 192.168.0.1 myService',
];
public static args = {
fleetOrDevice: Args.string({
description: 'fleet name/slug, device uuid, or address of local device',
public static args = [
{
name: 'fleetOrDevice',
description:
'fleet name/slug/id, device uuid, or address of local device',
required: true,
}),
service: Args.string({
},
{
name: 'service',
description: 'service name, if connecting to a container',
required: false,
}),
};
},
];
public static usage = 'ssh <fleetOrDevice> [service]';
public static flags = {
port: Flags.integer({
public static flags: flags.Input<FlagsDef> = {
port: flags.integer({
description: stripIndent`
SSH server port number (default 22222) if the target is an IP address or .local
hostname. Otherwise, port number for the balenaCloud gateway (default 22).`,
char: 'p',
parse: async (p) => parseAsInteger(p, 'port'),
parse: (p) => parseAsInteger(p, 'port'),
}),
tty: Flags.boolean({
tty: flags.boolean({
default: false,
description:
'force pseudo-terminal allocation (bypass TTY autodetection for stdin)',
char: 't',
}),
verbose: Flags.boolean({
verbose: flags.boolean({
default: false,
description: 'increase verbosity',
char: 'v',
}),
noproxy: Flags.boolean({
noproxy: flags.boolean({
default: false,
description: 'bypass global proxy configuration for the ssh connection',
}),
@ -103,14 +120,16 @@ export default class SshCmd extends Command {
public static offlineCompatible = true;
public async run() {
const { args: params, flags: options } = await this.parse(SshCmd);
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
SshCmd,
);
// Local connection
if (validateLocalHostnameOrIp(params.fleetOrDevice)) {
const { performLocalDeviceSSH } = await import('../utils/device/ssh');
return await performLocalDeviceSSH({
hostname: params.fleetOrDevice,
port: options.port || 'local',
address: params.fleetOrDevice,
port: options.port,
forceTTY: options.tty,
verbose: options.verbose,
service: params.service,
@ -133,11 +152,17 @@ export default class SshCmd extends Command {
params.fleetOrDevice,
);
const device = await sdk.models.device.get(deviceUuid, {
$select: ['id', 'supervisor_version', 'is_online'],
});
const deviceId = device.id;
const supervisorVersion = device.supervisor_version;
const { which } = await import('../utils/which');
const [whichProxytunnel, { username }, proxyUrl] = await Promise.all([
const [whichProxytunnel, username, proxyUrl] = await Promise.all([
useProxy ? which('proxytunnel', false) : undefined,
sdk.auth.getUserInfo(),
sdk.auth.whoami(),
// note that `proxyUrl` refers to the balenaCloud "resin-proxy"
// service, currently "balena-devices.com", rather than some
// local proxy server URL
@ -184,15 +209,19 @@ export default class SshCmd extends Command {
// that we know exists and is accessible
let containerId: string | undefined;
if (params.service != null) {
const { getContainerIdForService } = await import('../utils/device/ssh');
containerId = await getContainerIdForService({
containerId = await this.getContainerId(
sdk,
deviceUuid,
hostname: `ssh.${proxyUrl}`,
port: options.port || 'cloud',
proxyCommand,
service: params.service,
username,
});
params.service,
{
port: options.port,
proxyCommand,
proxyUrl: proxyUrl || '',
username: username!,
},
supervisorVersion,
deviceId,
);
}
let accessCommand: string;
@ -201,14 +230,158 @@ export default class SshCmd extends Command {
} else {
accessCommand = `host ${deviceUuid}`;
}
const { runRemoteCommand } = await import('../utils/ssh');
await runRemoteCommand({
cmd: accessCommand,
hostname: `ssh.${proxyUrl}`,
port: options.port || 'cloud',
proxyCommand,
username,
const command = this.generateVpnSshCommand({
uuid: deviceUuid,
command: accessCommand,
verbose: options.verbose,
port: options.port,
proxyCommand,
proxyUrl: proxyUrl || '',
username: username!,
});
const { spawnSshAndThrowOnError } = await import('../utils/ssh');
return spawnSshAndThrowOnError(command);
}
async getContainerId(
sdk: BalenaSdk.BalenaSDK,
uuid: string,
serviceName: string,
sshOpts: {
port?: number;
proxyCommand?: string[];
proxyUrl: string;
username: string;
},
version?: string,
id?: number,
): Promise<string> {
const semver = await import('balena-semver');
if (version == null || id == null) {
const device = await sdk.models.device.get(uuid, {
$select: ['id', 'supervisor_version'],
});
version = device.supervisor_version;
id = device.id;
}
let containerId: string | undefined;
if (semver.gte(version, '8.6.0')) {
const apiUrl = await sdk.settings.get('apiUrl');
// TODO: Move this into the SDKs device model
const request = await sdk.request.send({
method: 'POST',
url: '/supervisor/v2/containerId',
baseUrl: apiUrl,
body: {
method: 'GET',
deviceId: id,
},
});
if (request.status !== 200) {
throw new Error(
`There was an error connecting to device ${uuid}, HTTP response code: ${request.status}.`,
);
}
const body = request.body;
if (body.status !== 'success') {
throw new Error(
`There was an error communicating with device ${uuid}.\n\tError: ${body.message}`,
);
}
containerId = body.services[serviceName];
} else {
console.error(stripIndent`
Using legacy method to detect container ID. This will be slow.
To speed up this process, please update your device to an OS
which has a supervisor version of at least v8.6.0.
`);
// We need to execute a balena ps command on the device,
// and parse the output, looking for a specific
// container
const childProcess = await import('child_process');
const { escapeRegExp } = await import('lodash');
const { which } = await import('../utils/which');
const { deviceContainerEngineBinary } = await import(
'../utils/device/ssh'
);
const sshBinary = await which('ssh');
const sshArgs = this.generateVpnSshCommand({
uuid,
verbose: false,
port: sshOpts.port,
command: `host ${uuid} "${deviceContainerEngineBinary}" ps --format "{{.ID}} {{.Names}}"`,
proxyCommand: sshOpts.proxyCommand,
proxyUrl: sshOpts.proxyUrl,
username: sshOpts.username,
});
if (process.env.DEBUG) {
console.error(`[debug] [${sshBinary}, ${sshArgs.join(', ')}]`);
}
const subProcess = childProcess.spawn(sshBinary, sshArgs, {
stdio: [null, 'pipe', null],
});
const containers = await new Promise<string>((resolve, reject) => {
const output: string[] = [];
subProcess.stdout.on('data', (chunk) => output.push(chunk.toString()));
subProcess.on('close', (code: number) => {
if (code !== 0) {
reject(
new Error(
`Non-zero error code when looking for service container: ${code}`,
),
);
} else {
resolve(output.join(''));
}
});
});
const lines = containers.split('\n');
const regex = new RegExp(`\\/?${escapeRegExp(serviceName)}_\\d+_\\d+`);
for (const container of lines) {
const [cId, name] = container.split(' ');
if (regex.test(name)) {
containerId = cId;
break;
}
}
}
if (containerId == null) {
throw new Error(
`Could not find a service ${serviceName} on device ${uuid}.`,
);
}
return containerId;
}
generateVpnSshCommand(opts: {
uuid: string;
command: string;
verbose: boolean;
port?: number;
username: string;
proxyUrl: string;
proxyCommand?: string[];
}) {
return [
...(opts.verbose ? ['-vvv'] : []),
'-t',
...['-o', 'LogLevel=ERROR'],
...['-o', 'StrictHostKeyChecking=no'],
...['-o', 'UserKnownHostsFile=/dev/null'],
...(opts.proxyCommand && opts.proxyCommand.length
? ['-o', `ProxyCommand=${opts.proxyCommand.join(' ')}`]
: []),
...(opts.port ? ['-p', opts.port.toString()] : []),
`${opts.username}@ssh.${opts.proxyUrl}`,
opts.command,
];
}
}

View File

@ -15,13 +15,24 @@
* limitations under the License.
*/
import { Flags, Args } from '@oclif/core';
import { flags } from '@oclif/command';
import Command from '../command';
import { ExpectedError } from '../errors';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, getCliUx, stripIndent } from '../utils/lazy';
import { applicationIdInfo } from '../utils/messages';
interface FlagsDef {
fleet?: string;
device?: string;
duration?: string;
help: void;
}
interface ArgsDef {
action: string;
}
export default class SupportCmd extends Command {
public static description = stripIndent`
Grant or revoke support access for devices or fleets.
@ -45,17 +56,18 @@ export default class SupportCmd extends Command {
'balena support disable -f myorg/myfleet',
];
public static args = {
action: Args.string({
public static args = [
{
name: 'action',
description: 'enable|disable support access',
options: ['enable', 'disable'],
}),
};
},
];
public static usage = 'support <action>';
public static flags = {
device: Flags.string({
public static flags: flags.Input<FlagsDef> = {
device: flags.string({
description: 'comma-separated list (no spaces) of device UUIDs',
char: 'd',
}),
@ -64,7 +76,7 @@ export default class SupportCmd extends Command {
description:
'comma-separated list (no spaces) of fleet names or slugs (preferred)',
},
duration: Flags.string({
duration: flags.string({
description:
'length of time to enable support for, in (h)ours or (d)ays, e.g. 12h, 2d',
char: 't',
@ -75,7 +87,9 @@ export default class SupportCmd extends Command {
public static authenticated = true;
public async run() {
const { args: params, flags: options } = await this.parse(SupportCmd);
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
SupportCmd,
);
const balena = getBalenaSdk();
const ux = getCliUx();
@ -135,7 +149,7 @@ export default class SupportCmd extends Command {
console.log(
`Access has been granted for ${duration}, expiring ${new Date(
expiryTs,
).toISOString()}`,
).toLocaleString()}`,
);
}
}

View File

@ -15,12 +15,23 @@
* limitations under the License.
*/
import { Args } from '@oclif/core';
import { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { applicationIdInfo } from '../../utils/messages';
interface FlagsDef {
fleet?: string;
device?: string;
release?: string;
help: void;
}
interface ArgsDef {
tagKey: string;
}
export default class TagRmCmd extends Command {
public static description = stripIndent`
Remove a tag from a fleet, device or release.
@ -38,16 +49,17 @@ export default class TagRmCmd extends Command {
'$ balena tag rm myTagKey --release b376b0e544e9429483b656490e5b9443b4349bd6',
];
public static args = {
tagKey: Args.string({
public static args = [
{
name: 'tagKey',
description: 'the key string of the tag',
required: true,
}),
};
},
];
public static usage = 'tag rm <tagKey>';
public static flags = {
public static flags: flags.Input<FlagsDef> = {
fleet: {
...cf.fleet,
exclusive: ['device', 'release'],
@ -66,7 +78,9 @@ export default class TagRmCmd extends Command {
public static authenticated = true;
public async run() {
const { args: params, flags: options } = await this.parse(TagRmCmd);
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
TagRmCmd,
);
const balena = getBalenaSdk();
@ -76,6 +90,8 @@ export default class TagRmCmd extends Command {
throw new ExpectedError(TagRmCmd.missingResourceMessage);
}
const { tryAsInteger } = await import('../../utils/validation');
if (options.fleet) {
const { getFleetSlug } = await import('../../utils/sdk');
return balena.models.application.tags.remove(
@ -84,7 +100,10 @@ export default class TagRmCmd extends Command {
);
}
if (options.device) {
return balena.models.device.tags.remove(options.device, params.tagKey);
return balena.models.device.tags.remove(
tryAsInteger(options.device),
params.tagKey,
);
}
if (options.release) {
const { disambiguateReleaseParam } = await import(

View File

@ -15,12 +15,24 @@
* limitations under the License.
*/
import { Args } from '@oclif/core';
import { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { applicationIdInfo } from '../../utils/messages';
interface FlagsDef {
fleet?: string;
device?: string;
release?: string;
help: void;
}
interface ArgsDef {
tagKey: string;
value?: string;
}
export default class TagSetCmd extends Command {
public static description = stripIndent`
Set a tag on a fleet, device or release.
@ -45,20 +57,22 @@ export default class TagSetCmd extends Command {
'$ balena tag set myCompositeTag --release b376b0e544e9429483b656490e5b9443b4349bd6',
];
public static args = {
tagKey: Args.string({
public static args = [
{
name: 'tagKey',
description: 'the key string of the tag',
required: true,
}),
value: Args.string({
},
{
name: 'value',
description: 'the optional value associated with the tag',
required: false,
}),
};
},
];
public static usage = 'tag set <tagKey> [value]';
public static flags = {
public static flags: flags.Input<FlagsDef> = {
fleet: {
...cf.fleet,
exclusive: ['device', 'release'],
@ -77,7 +91,9 @@ export default class TagSetCmd extends Command {
public static authenticated = true;
public async run() {
const { args: params, flags: options } = await this.parse(TagSetCmd);
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
TagSetCmd,
);
const balena = getBalenaSdk();
@ -89,6 +105,8 @@ export default class TagSetCmd extends Command {
params.value ??= '';
const { tryAsInteger } = await import('../../utils/validation');
if (options.fleet) {
const { getFleetSlug } = await import('../../utils/sdk');
return balena.models.application.tags.set(
@ -99,7 +117,7 @@ export default class TagSetCmd extends Command {
}
if (options.device) {
return balena.models.device.tags.set(
options.device,
tryAsInteger(options.device),
params.tagKey,
params.value,
);

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.
@ -15,11 +15,23 @@
* limitations under the License.
*/
import { flags } from '@oclif/command';
import Command from '../command';
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';
import { isV14 } from '../utils/version';
interface FlagsDef extends DataSetOutputOptions {
fleet?: string;
device?: string;
release?: string;
help: void;
}
export default class TagsCmd extends Command {
public static description = stripIndent`
@ -40,7 +52,7 @@ export default class TagsCmd extends Command {
public static usage = 'tags';
public static flags = {
public static flags: flags.Input<FlagsDef> = {
fleet: {
...cf.fleet,
exclusive: ['device', 'release'],
@ -53,13 +65,14 @@ export default class TagsCmd extends Command {
...cf.release,
exclusive: ['fleet', 'device'],
},
...(isV14() ? cf.dataSetOutputFlags : {}),
help: cf.help,
};
public static authenticated = true;
public async run() {
const { flags: options } = await this.parse(TagsCmd);
const { flags: options } = this.parse<FlagsDef, {}>(TagsCmd);
const balena = getBalenaSdk();
@ -68,7 +81,9 @@ export default class TagsCmd extends Command {
throw new ExpectedError(this.missingResourceMessage);
}
let tags;
const { tryAsInteger } = await import('../utils/validation');
let tags: ApplicationTag[] | DeviceTag[] | ReleaseTag[] = [];
if (options.fleet) {
const { getFleetSlug } = await import('../utils/sdk');
@ -77,7 +92,9 @@ export default class TagsCmd extends Command {
);
}
if (options.device) {
tags = await balena.models.device.tags.getAllByDevice(options.device);
tags = await balena.models.device.tags.getAllByDevice(
tryAsInteger(options.device),
);
}
if (options.release) {
const { disambiguateReleaseParam } = await import(
@ -91,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

@ -15,7 +15,7 @@
* limitations under the License.
*/
import { Flags, Args } from '@oclif/core';
import { flags } from '@oclif/command';
import Command from '../command';
import {
NoPortsDefinedError,
@ -28,6 +28,15 @@ import { lowercaseIfSlug } from '../utils/normalization';
import type { Server, Socket } from 'net';
interface FlagsDef {
port: string[];
help: void;
}
interface ArgsDef {
deviceOrFleet: string;
}
export default class TunnelCmd extends Command {
public static description = stripIndent`
Tunnel local ports to your balenaOS device.
@ -70,18 +79,19 @@ export default class TunnelCmd extends Command {
'$ balena tunnel myFleet -p 8080:3000 -p 8081:9000',
];
public static args = {
deviceOrFleet: Args.string({
description: 'device UUID or fleet name/slug',
public static args = [
{
name: 'deviceOrFleet',
description: 'device UUID or fleet name/slug/ID',
required: true,
parse: lowercaseIfSlug,
}),
};
},
];
public static usage = 'tunnel <deviceOrFleet>';
public static flags = {
port: Flags.string({
public static flags: flags.Input<FlagsDef> = {
port: flags.string({
description:
'port mapping in the format <remotePort>[:[localIP:]localPort]',
char: 'p',
@ -94,7 +104,9 @@ export default class TunnelCmd extends Command {
public static authenticated = true;
public async run() {
const { args: params, flags: options } = await this.parse(TunnelCmd);
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
TunnelCmd,
);
const logger = await Command.getLogger();
const sdk = getBalenaSdk();
@ -124,7 +136,8 @@ export default class TunnelCmd extends Command {
// Ascertain device uuid
const { getOnlineTargetDeviceUuid } = await import('../utils/patterns');
const uuid = await getOnlineTargetDeviceUuid(sdk, params.deviceOrFleet);
logger.logInfo(`Opening a tunnel to ${uuid}...`);
const device = await sdk.models.device.get(uuid);
logger.logInfo(`Opening a tunnel to ${device.uuid}...`);
const _ = await import('lodash');
const localListeners = _.chain(options.port)
@ -134,27 +147,31 @@ export default class TunnelCmd extends Command {
.map(async ({ localPort, localAddress, remotePort }) => {
try {
const { tunnelConnectionToDevice } = await import('../utils/tunnel');
const handler = await tunnelConnectionToDevice(uuid, remotePort, sdk);
const handler = await tunnelConnectionToDevice(
device.uuid,
remotePort,
sdk,
);
const { createServer } = await import('net');
const server = createServer(async (client: Socket) => {
try {
await handler(client);
logConnection(
client.remoteAddress ?? '',
client.remotePort ?? 0,
client.localAddress ?? '',
client.localPort ?? 0,
uuid,
client.remoteAddress || '',
client.remotePort || 0,
client.localAddress,
client.localPort,
device.vpn_address || '',
remotePort,
);
} catch (err) {
logConnection(
client.remoteAddress ?? '',
client.remotePort ?? 0,
client.localAddress ?? '',
client.localPort ?? 0,
uuid,
client.remoteAddress || '',
client.remotePort || 0,
client.localAddress,
client.localPort,
device.vpn_address || '',
remotePort,
err,
);
@ -169,15 +186,15 @@ export default class TunnelCmd extends Command {
});
logger.logInfo(
` - tunnelling ${localAddress}:${localPort} to ${uuid}:${remotePort}`,
` - tunnelling ${localAddress}:${localPort} to ${device.uuid}:${remotePort}`,
);
return true;
} catch (err) {
logger.logWarn(
` - not tunnelling ${localAddress}:${localPort} to ${uuid}:${remotePort}, failed ${JSON.stringify(
err.message,
)}`,
` - not tunnelling ${localAddress}:${localPort} to ${
device.uuid
}:${remotePort}, failed ${JSON.stringify(err.message)}`,
);
return false;

View File

@ -15,10 +15,15 @@
* limitations under the License.
*/
import { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { stripIndent, getChalk, getVisuals } from '../../utils/lazy';
interface FlagsDef {
help: void;
}
export default class UtilAvailableDrivesCmd extends Command {
public static description = stripIndent`
List available drives.
@ -29,14 +34,14 @@ export default class UtilAvailableDrivesCmd extends Command {
public static usage = 'util available-drives';
public static flags = {
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static offlineCompatible = true;
public async run() {
await this.parse(UtilAvailableDrivesCmd);
this.parse<FlagsDef, {}>(UtilAvailableDrivesCmd);
const sdk = await import('etcher-sdk');

View File

@ -15,10 +15,16 @@
* limitations under the License.
*/
import { Flags } from '@oclif/core';
import { flags } from '@oclif/command';
import Command from '../command';
import { stripIndent } from '../utils/lazy';
interface FlagsDef {
all: boolean;
json: boolean;
help: void;
}
export interface JsonVersions {
'balena-cli': string;
'Node.js': string;
@ -53,24 +59,24 @@ export default class VersionCmd extends Command {
public static offlineCompatible = true;
public static flags = {
all: Flags.boolean({
public static flags: flags.Input<FlagsDef> = {
all: flags.boolean({
default: false,
char: 'a',
description:
'include version information for additional components (Node.js)',
}),
json: Flags.boolean({
json: flags.boolean({
default: false,
char: 'j',
description:
'output version information in JSON format for programmatic use',
}),
help: Flags.help({ char: 'h' }),
help: flags.help({ char: 'h' }),
};
public async run() {
const { flags: options } = await this.parse(VersionCmd);
const { flags: options } = this.parse<FlagsDef, {}>(VersionCmd);
const versions: JsonVersions = {
'balena-cli': (await import('../../package.json')).version,
'Node.js':

View File

@ -32,41 +32,22 @@ export default class WhoamiCmd extends Command {
public static authenticated = true;
public async run() {
await this.parse(WhoamiCmd);
this.parse<{}, {}>(WhoamiCmd);
const balena = getBalenaSdk();
const [whoamiResult, url] = await Promise.all([
const [username, email, url] = await Promise.all([
balena.auth.whoami(),
balena.auth.getEmail(),
balena.settings.get('balenaUrl'),
]);
if (whoamiResult?.actorType === 'user') {
const { username, email } = whoamiResult;
console.log(
getVisuals().table.vertical({ username, email, url }, [
'$account information$',
'username',
'email',
'url',
]),
);
} else if (whoamiResult?.actorType === 'device') {
console.log(
getVisuals().table.vertical({ device: whoamiResult.uuid, url }, [
'$account information$',
'device',
'url',
]),
);
} else if (whoamiResult?.actorType === 'application') {
console.log(
getVisuals().table.vertical({ application: whoamiResult.slug, url }, [
'$account information$',
'application',
'url',
]),
);
}
console.log(
getVisuals().table.vertical({ username, email, url }, [
'$account information$',
'username',
'email',
'url',
]),
);
}
}

Some files were not shown because too many files have changed in this diff Show More