Compare commits

..

No commits in common. "master" and "v14.4.1" have entirely different histories.

300 changed files with 25009 additions and 44816 deletions

4
.gitattributes vendored
View File

@ -4,10 +4,6 @@
*.* -eol *.* -eol
*.sh text eol=lf *.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 # 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 docs/balena-cli.md text eol=lf

View File

@ -1,143 +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: '20.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@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9
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@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
with:
node-version: ${{ inputs.NODE_VERSION }}
cache: npm
- name: Set up Python 3.11
if: runner.os == 'macOS'
uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4
with:
python-version: "3.11"
- 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@8f3fb608891dd2244cdab3d69cd68c0d37a7fe93 # v2
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:SM_CLIENT_CERT_FILE_B64
certutil -decode ${{ runner.temp }}/certificate.base64 ${{ runner.temp }}/Certificate_pkcs12.p12
Remove-Item -path ${{ runner.temp }} -include certificate.base64
env:
SM_CLIENT_CERT_FILE_B64: ${{ fromJSON(inputs.secrets).SM_CLIENT_CERT_FILE_B64 }}
# 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
SM_HOST=${{ fromJSON(inputs.secrets).SM_HOST }}
SM_API_KEY=${{ fromJSON(inputs.secrets).SM_API_KEY }}
SM_CLIENT_CERT_FILE='${{ runner.temp }}\Certificate_pkcs12.p12'
SM_CLIENT_CERT_PASSWORD=${{ fromJSON(inputs.secrets).SM_CLIENT_CERT_PASSWORD }}
SM_CODE_SIGNING_CERT_SHA1_HASH=${{ fromJSON(inputs.secrets).SM_CODE_SIGNING_CERT_SHA1_HASH }}
curl --silent --retry 3 --fail https://one.digicert.com/signingmanager/api-ui/v1/releases/smtools-windows-x64.msi/download \
-H "x-api-key:$SM_API_KEY" \
-o smtools-windows-x64.msi
msiexec -i smtools-windows-x64.msi -qn
PATH="/c/Program Files/DigiCert/DigiCert One Signing Manager Tools:${PATH}"
smksp_registrar.exe list
smctl.exe keypair ls
/c/Windows/System32/certutil.exe -csp "DigiCert Signing Manager KSP" -key -user
smksp_cert_sync.exe
# (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://docs.digicert.com/es/software-trust-manager/ci-cd-integrations/plugins/github-custom-action-for-keypair-signing.html
TIMESTAMP_SERVER: http://timestamp.digicert.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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: gh-release-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ strategy.job-index }}
path: dist
retention-days: 1
if-no-files-found: error

View File

@ -1,65 +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: '20.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@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
with:
node-version: ${{ inputs.NODE_VERSION }}
cache: npm
- name: Set up Python 3.11
if: runner.os == 'macOS'
uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4
with:
python-version: "3.11"
- 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:core
- name: Compress custom source
shell: pwsh
run: tar --exclude-vcs -acf ${{ runner.temp }}/custom.tgz .
- name: Upload custom artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
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,4 +0,0 @@
{
"extends": ["github>balena-io/renovate-config"],
"postUpdateOptions": ["npmDedupe"]
}

View File

@ -1,45 +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_test_matrix: >
{
"os": [
["self-hosted", "X64"],
["self-hosted", "ARM64"],
["macos-13"],
["windows-2019"],
["macos-latest-xlarge"]
]
}
custom_publish_matrix: >
{
"os": [
["self-hosted", "X64"],
["self-hosted", "ARM64"],
["macos-13"],
["windows-2019"],
["macos-latest-xlarge"]
]
}
github_prerelease: false
restrict_custom_actions: false

View File

@ -1 +0,0 @@
node automation/check-npm-version.js && ts-node automation/check-doc.ts

20
.resinci.yml Normal file
View File

@ -0,0 +1,20 @@
---
npm:
platforms:
- name: linux
os: ubuntu
architecture: x86_64
node_versions:
- "12"
- "14"
##
## Temporarily skip Alpine tests until the following issues are resolved:
## * https://github.com/concourse/concourse/issues/7905
## * https://github.com/product-os/balena-concourse/issues/631
##
# - 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

@ -115,27 +115,14 @@ The content sources for the auto generation of `docs/balena-cli.md` are:
* [Selected * [Selected
sections](https://github.com/balena-io/balena-cli/blob/v12.23.0/automation/capitanodoc/capitanodoc.ts#L199-L204) sections](https://github.com/balena-io/balena-cli/blob/v12.23.0/automation/capitanodoc/capitanodoc.ts#L199-L204)
of the README file. of the README file.
* The CLI's command documentation in source code (`src/commands/` folder), for example: * The CLI's command documentation in source code (`lib/commands/` folder), for example:
* `src/commands/push.ts` * `lib/commands/push.ts`
* `src/commands/env/add.ts` * `lib/commands/env/add.ts`
The README file is manually edited, but subsections are automatically extracted for inclusion in The README file is manually edited, but subsections are automatically extracted for inclusion in
`docs/balena-cli.md` by the `getCapitanoDoc()` function in `docs/balena-cli.md` by the `getCapitanoDoc()` function in
[`automation/capitanodoc/capitanodoc.ts`](https://github.com/balena-io/balena-cli/blob/master/automation/capitanodoc/capitanodoc.ts). [`automation/capitanodoc/capitanodoc.ts`](https://github.com/balena-io/balena-cli/blob/master/automation/capitanodoc/capitanodoc.ts).
**IMPORTANT**
The file [`capitanodoc.ts`](https://github.com/balena-io/balena-cli/blob/master/automation/capitanodoc/capitanodoc.ts) lists
commands to generate documentation from. At the moment, it's manually updated and maintained alphabetically.
To add a new command to be documented,
1. Find the resource which it is part of or create a new one.
2. List the location of the build file
3. Make sure to add your files in alphabetical order
Once added, run the command `npm run build` to generate the documentation
The `INSTALL*.md` and `TROUBLESHOOTING.md` files are also manually edited. The `INSTALL*.md` and `TROUBLESHOOTING.md` files are also manually edited.
## Patches folder ## Patches folder
@ -223,7 +210,7 @@ command, or by manually editing or copying files to the `node_modules` folder.
Unexpected behavior may then be observed because of the CLI's use of the Unexpected behavior may then be observed because of the CLI's use of the
[fast-boot2](https://www.npmjs.com/package/fast-boot2) package that caches module resolution. [fast-boot2](https://www.npmjs.com/package/fast-boot2) package that caches module resolution.
`fast-boot2` is configured in `src/fast-boot.ts` to automatically invalidate the cache if `fast-boot2` is configured in `lib/fast-boot.ts` to automatically invalidate the cache if
changes are made to the `package.json` or `npm-shrinkwrap.json` files, but the cache won't changes are made to the `package.json` or `npm-shrinkwrap.json` files, but the cache won't
be automatically invalidated if `npm link` is used or if manual modifications are made to the be automatically invalidated if `npm link` is used or if manual modifications are made to the
`node_modules` folder. In this situation: `node_modules` folder. In this situation:

View File

@ -40,7 +40,7 @@ By default, the CLI is installed to the following folders:
OS | Folders OS | Folders
--- | --- --- | ---
Windows: | `C:\Program Files\balena-cli\` Windows: | `C:\Program Files\balena-cli\`
macOS: | `/usr/local/src/balena-cli/` <br> `/usr/local/bin/balena` macOS: | `/usr/local/lib/balena-cli/` <br> `/usr/local/bin/balena`
## Standalone Zip Package ## Standalone Zip Package
@ -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 The npm installation involves building native (platform-specific) binary modules, which require
some development tools to be installed first, as follows. some development tools to be installed first, as follows.
> **The balena CLI currently requires Node.js version ^20.6.0** > **The balena CLI currently requires Node.js version 12 (min 12.8.0).**
> **Versions 21 and later are not yet fully supported.** > **Versions 13 and later are not yet fully supported.**
### Install development tools ### 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++ $ 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 $ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
$ . ~/.bashrc $ . ~/.bashrc
$ nvm install 20 $ nvm install 12
``` ```
The `curl` command line above uses The `curl` command line above uses
@ -106,15 +106,15 @@ recommended.
```sh ```sh
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash $ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
$ . ~/.bashrc $ . ~/.bashrc
$ nvm install 20 $ nvm install 12
``` ```
#### **Windows** (not WSL) #### **Windows** (not WSL)
Install: 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 * If you'd like the ability to switch between Node.js versions, install
- Node.js v20 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) [nvm-windows](https://github.com/coreybutler/nvm-windows#node-version-manager-nvm-for-windows)
instead. instead.
* The [MSYS2 shell](https://www.msys2.org/), which provides `git`, `make`, `g++` and more: * The [MSYS2 shell](https://www.msys2.org/), which provides `git`, `make`, `g++` and more:
@ -145,7 +145,7 @@ container) in order to allow npm scripts like `postinstall` to be executed.
## Additional Dependencies ## Additional Dependencies
The `balena device ssh`, `device detect`, `build`, `deploy` and `preload` commands may require The `balena ssh`, `scan`, `build`, `deploy` and `preload` commands may require
additional software to be installed. Check the Additional Dependencies sections for each operating additional software to be installed. Check the Additional Dependencies sections for each operating
system: system:

View File

@ -33,7 +33,7 @@ as described above.
## sudo configuration ## sudo configuration
A few CLI commands require execution through sudo, e.g. `sudo balena device detect`. A few CLI commands require execution through sudo, e.g. `sudo balena scan`.
If your Linux distribution has an `/etc/sudoers` file that defines a `secure_path` If your Linux distribution has an `/etc/sudoers` file that defines a `secure_path`
setting, run `sudo visudo` to edit it and add the balena CLI's installation folder to setting, run `sudo visudo` to edit it and add the balena CLI's installation folder to
the ***pre-existing*** `secure_path` setting, for example: the ***pre-existing*** `secure_path` setting, for example:
@ -61,19 +61,19 @@ instructions](https://docs.docker.com/install/overview/) to install Docker on th
workstation as the balena CLI. The [advanced installation workstation as the balena CLI. The [advanced installation
options](./INSTALL-ADVANCED.md#additional-dependencies) document describes other possibilities. options](./INSTALL-ADVANCED.md#additional-dependencies) document describes other possibilities.
### balena device ssh ### balena ssh
The `balena device ssh` command requires the `ssh` command-line tool to be available. Most Linux The `balena ssh` command requires the `ssh` command-line tool to be available. Most Linux
distributions will already have it installed. Otherwise, `sudo apt-get install openssh-client` distributions will already have it installed. Otherwise, `sudo apt-get install openssh-client`
should do the trick on Debian or Ubuntu. should do the trick on Debian or Ubuntu.
The `balena device ssh` command also requires an SSH key to be added to your balena account: see [SSH The `balena ssh` command also requires an SSH key to be added to your balena account: see [SSH
Access documentation](https://www.balena.io/docs/learn/manage/ssh-access/). The `balena key*` Access documentation](https://www.balena.io/docs/learn/manage/ssh-access/). The `balena key*`
command set can also be used to list and manage SSH keys: see `balena help -v`. command set can also be used to list and manage SSH keys: see `balena help -v`.
### balena device detect ### balena scan
The `balena device detect` command requires a multicast DNS (mDNS) service like The `balena scan` command requires a multicast DNS (mDNS) service like
[Avahi](https://en.wikipedia.org/wiki/Avahi_(software)), which is installed by default on most [Avahi](https://en.wikipedia.org/wiki/Avahi_(software)), which is installed by default on most
desktop Linux distributions. Otherwise, on Debian or Ubuntu, the installation command would be desktop Linux distributions. Otherwise, on Debian or Ubuntu, the installation command would be
`sudo apt-get install avahi-daemon`. `sudo apt-get install avahi-daemon`.

View File

@ -19,7 +19,7 @@ Selected operating system: **macOS**
- On the terminal prompt, type `balena version` and hit Enter. It should display - On the terminal prompt, type `balena version` and hit Enter. It should display
the version of the balena CLI that you have installed. the version of the balena CLI that you have installed.
No further steps are required to run most CLI commands. The `balena device ssh`, `build`, `deploy` No further steps are required to run most CLI commands. The `balena ssh`, `build`, `deploy`
and `preload` commands may require additional software to be installed, as described and `preload` commands may require additional software to be installed, as described
in the next section. in the next section.
@ -27,7 +27,7 @@ To update the balena CLI, repeat the steps above for the new version.
To uninstall it, run the following command on a terminal prompt: To uninstall it, run the following command on a terminal prompt:
```text ```text
sudo /usr/local/src/balena-cli/bin/uninstall sudo /usr/local/lib/balena-cli/bin/uninstall
``` ```
## Additional Dependencies ## Additional Dependencies
@ -41,9 +41,9 @@ instructions](https://docs.docker.com/install/overview/) to install Docker on th
workstation as the balena CLI. The [advanced installation workstation as the balena CLI. The [advanced installation
options](./INSTALL-ADVANCED.md#additional-dependencies) document describes other possibilities. options](./INSTALL-ADVANCED.md#additional-dependencies) document describes other possibilities.
### balena device ssh ### balena ssh
The `balena device ssh` command requires the `ssh` command-line tool to be available. To check whether The `balena ssh` command requires the `ssh` command-line tool to be available. To check whether
it is already installed, run `ssh` on a Terminal window. If it is not yet installed, the options it is already installed, run `ssh` on a Terminal window. If it is not yet installed, the options
include: include:
@ -52,7 +52,7 @@ include:
Components → Command Line Tools → Install. Components → Command Line Tools → Install.
* Or, install [Homebrew](https://brew.sh/), then `brew install openssh` * Or, install [Homebrew](https://brew.sh/), then `brew install openssh`
The `balena device ssh` command also requires an SSH key to be added to your balena account: see [SSH The `balena ssh` command also requires an SSH key to be added to your balena account: see [SSH
Access documentation](https://www.balena.io/docs/learn/manage/ssh-access/). The `balena key*` Access documentation](https://www.balena.io/docs/learn/manage/ssh-access/). The `balena key*`
command set can also be used to list and manage SSH keys: see `balena help -v`. command set can also be used to list and manage SSH keys: see `balena help -v`.

View File

@ -19,7 +19,7 @@ Selected operating system: **Windows**
- On the command prompt, type `balena version` and hit Enter. It should display - On the command prompt, type `balena version` and hit Enter. It should display
the version of the balena CLI that you have installed. the version of the balena CLI that you have installed.
No further steps are required to run most CLI commands. The `balena device ssh`, `device detect`, `build`, No further steps are required to run most CLI commands. The `balena ssh`, `scan`, `build`,
`deploy` and `preload` commands may require additional software to be installed, as `deploy` and `preload` commands may require additional software to be installed, as
described below. described below.
@ -34,9 +34,9 @@ instructions](https://docs.docker.com/install/overview/) to install Docker on th
workstation as the balena CLI. The [advanced installation workstation as the balena CLI. The [advanced installation
options](./INSTALL-ADVANCED.md#additional-dependencies) document describes other possibilities. options](./INSTALL-ADVANCED.md#additional-dependencies) document describes other possibilities.
### balena device ssh ### balena ssh
The `balena device ssh` command requires the `ssh` command-line tool to be available. Microsoft started The `balena ssh` command requires the `ssh` command-line tool to be available. Microsoft started
distributing an SSH client with Windows 10, which is automatically installed through Windows distributing an SSH client with Windows 10, which is automatically installed through Windows
Update. To check whether it is installed, run `ssh` on a Windows Command Prompt or PowerShell. It Update. To check whether it is installed, run `ssh` on a Windows Command Prompt or PowerShell. It
can also be [manually can also be [manually
@ -44,13 +44,13 @@ installed](https://docs.microsoft.com/en-us/windows-server/administration/openss
if needed. For older versions of Windows, there are several ssh/OpenSSH clients provided by 3rd if needed. For older versions of Windows, there are several ssh/OpenSSH clients provided by 3rd
parties. parties.
The `balena device ssh` command also requires an SSH key to be added to your balena account: see [SSH The `balena ssh` command also requires an SSH key to be added to your balena account: see [SSH
Access documentation](https://www.balena.io/docs/learn/manage/ssh-access/). The `balena key*` Access documentation](https://www.balena.io/docs/learn/manage/ssh-access/). The `balena key*`
command set can also be used to list and manage SSH keys: see `balena help -v`. command set can also be used to list and manage SSH keys: see `balena help -v`.
### balena device detect ### balena scan
The `balena device detect` command requires a multicast DNS (mDNS) service like Apple's Bonjour. The `balena scan` command requires a multicast DNS (mDNS) service like Apple's Bonjour.
Many Windows machines will already have this service installed, as it is bundled in popular Many Windows machines will already have this service installed, as it is bundled in popular
applications such as Skype (Wikipedia lists [several others](https://en.wikipedia.org/wiki/Bonjour_(software))). applications such as Skype (Wikipedia lists [several others](https://en.wikipedia.org/wiki/Bonjour_(software))).
Otherwise, Bonjour for Windows can be downloaded and installed from: https://support.apple.com/kb/DL999 Otherwise, Bonjour for Windows can be downloaded and installed from: https://support.apple.com/kb/DL999

View File

@ -88,9 +88,9 @@ HTTP(S) proxies can be configured through any of the following methods, in prece
* The `HTTPS_PROXY` and/or `HTTP_PROXY` environment variables, in the same URL format as * The `HTTPS_PROXY` and/or `HTTP_PROXY` environment variables, in the same URL format as
`BALENARC_PROXY`. `BALENARC_PROXY`.
### Proxy setup for balena device ssh ### Proxy setup for balena ssh
In order to work behind a proxy server, the `balena device ssh` command requires the In order to work behind a proxy server, the `balena ssh` command requires the
[`proxytunnel`](http://proxytunnel.sourceforge.net/) package (command-line tool) to be installed. [`proxytunnel`](http://proxytunnel.sourceforge.net/) package (command-line tool) to be installed.
`proxytunnel` is available for Linux distributions like Ubuntu/Debian (`apt install proxytunnel`), `proxytunnel` is available for Linux distributions like Ubuntu/Debian (`apt install proxytunnel`),
and for macOS through [Homebrew](https://brew.sh/). Windows support is limited to the [Windows and for macOS through [Homebrew](https://brew.sh/). Windows support is limited to the [Windows
@ -110,7 +110,7 @@ The `BALENARC_NO_PROXY` variable may be used to exclude specified destinations f
> * This feature requires CLI version 11.30.8 or later. In the case of the npm [installation > * This feature requires CLI version 11.30.8 or later. In the case of the npm [installation
> option](https://github.com/balena-io/balena-cli/blob/master/INSTALL.md), it also requires > option](https://github.com/balena-io/balena-cli/blob/master/INSTALL.md), it also requires
> Node.js version 10.16.0 or later. > Node.js version 10.16.0 or later.
> * To exclude a `balena device ssh` target from proxying (IP address or `.local` hostname), the > * To exclude a `balena ssh` target from proxying (IP address or `.local` hostname), the
> `--noproxy` option should be specified in addition to the `BALENARC_NO_PROXY` variable. > `--noproxy` option should be specified in addition to the `BALENARC_NO_PROXY` variable.
By default (if `BALENARC_NO_PROXY` is not defined), all [private IPv4 By default (if `BALENARC_NO_PROXY` is not defined), all [private IPv4

View File

@ -31,7 +31,7 @@ command again.
Check whether the SD card is locked (a physical switch on the side of the card). Check whether the SD card is locked (a physical switch on the side of the card).
## I get `connect ETIMEDOUT` with `balena device tunnel` ## I get `connect ETIMEDOUT` with `balena tunnel`
Please update the CLI to the latest version. This issue was fixed in v12.38.5. Please update the CLI to the latest version. This issue was fixed in v12.38.5.
For more details, see: https://github.com/balena-io/balena-cli/issues/2172 For more details, see: https://github.com/balena-io/balena-cli/issues/2172
@ -79,10 +79,10 @@ Try resetting the ownership by running:
$ sudo chown -R <user> $HOME/.balena $ sudo chown -R <user> $HOME/.balena
``` ```
## Broken line wrapping / cursor behavior with `balena device ssh` ## Broken line wrapping / cursor behavior with `balena ssh`
Users sometimes come across broken line wrapping or cursor behavior in text terminals, for example Users sometimes come across broken line wrapping or cursor behavior in text terminals, for example
when long command lines are typed in a `balena device ssh` session, or when using text editors like `vim` when long command lines are typed in a `balena ssh` session, or when using text editors like `vim`
or `nano`. This is not something specific to the balena CLI, being also a commonly reported issue or `nano`. This is not something specific to the balena CLI, being also a commonly reported issue
with standard remote terminal tools like `ssh` or `telnet`. It is often a remote shell with standard remote terminal tools like `ssh` or `telnet`. It is often a remote shell
configuration issue (files like `/etc/profile`, `~/.bash_profile`, `~/.bash_login`, `~/.profile` configuration issue (files like `/etc/profile`, `~/.bash_profile`, `~/.bash_login`, `~/.profile`

View File

@ -15,20 +15,21 @@
* limitations under the License. * limitations under the License.
*/ */
import type { JsonVersions } from '../src/commands/version/index'; 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 archiver from 'archiver';
import { exec, execFile } from 'child_process'; import * as Bluebird from 'bluebird';
import { execFile } from 'child_process';
import * as filehound from 'filehound'; import * as filehound from 'filehound';
import type { Stats } from 'fs'; import { Stats } from 'fs';
import * as fs from 'fs-extra'; import * as fs from 'fs-extra';
import * as klaw from 'klaw'; import * as klaw from 'klaw';
import * as _ from 'lodash';
import * as path from 'path'; import * as path from 'path';
import * as rimraf from 'rimraf'; import * as rimraf from 'rimraf';
import * as semver from 'semver'; import * as semver from 'semver';
import { promisify } from 'util'; import { promisify } from 'util';
import { notarize } from '@electron/notarize';
import { stripIndent } from '../build/utils/lazy'; import { stripIndent } from '../build/utils/lazy';
import { import {
@ -40,12 +41,12 @@ import {
} from './utils'; } from './utils';
const execFileAsync = promisify(execFile); const execFileAsync = promisify(execFile);
const execAsync = promisify(exec);
const rimrafAsync = promisify(rimraf);
export const packageJSON = loadPackageJson(); export const packageJSON = loadPackageJson();
export const version = 'v' + packageJSON.version; export const version = 'v' + packageJSON.version;
const arch = process.arch; const arch = process.arch;
const MSYS2_BASH =
process.env.MSYSSHELLPATH || 'C:\\msys64\\usr\\bin\\bash.exe';
function dPath(...paths: string[]) { function dPath(...paths: string[]) {
return path.join(ROOT, 'dist', ...paths); return path.join(ROOT, 'dist', ...paths);
@ -61,13 +62,9 @@ const standaloneZips: PathByPlatform = {
win32: dPath(`balena-cli-${version}-windows-${arch}-standalone.zip`), win32: dPath(`balena-cli-${version}-windows-${arch}-standalone.zip`),
}; };
const getOclifInstallersOriginalNames = async (): Promise<PathByPlatform> => { const oclifInstallers: PathByPlatform = {
const { stdout } = await execAsync('git rev-parse --short HEAD'); darwin: dPath('macos', `balena-${version}.pkg`),
const sha = stdout.trim(); win32: dPath('win32', `balena-${version}-${arch}.exe`),
return {
darwin: dPath('macos', `balena-${version}-${sha}-${arch}.pkg`),
win32: dPath('win32', `balena-${version}-${sha}-${arch}.exe`),
};
}; };
const renamedOclifInstallers: PathByPlatform = { const renamedOclifInstallers: PathByPlatform = {
@ -92,7 +89,7 @@ async function diffPkgOutput(pkgOut: string) {
'tests', 'tests',
'test-data', 'test-data',
'pkg', 'pkg',
`expected-warnings-${process.platform}-${arch}.txt`, `expected-warnings-${process.platform}.txt`,
); );
const absSavedPath = path.join(ROOT, relSavedPath); const absSavedPath = path.join(ROOT, relSavedPath);
const ignoreStartsWith = [ const ignoreStartsWith = [
@ -160,7 +157,7 @@ ${sep}
* messages (stdout and stderr) in order to call diffPkgOutput(). * messages (stdout and stderr) in order to call diffPkgOutput().
*/ */
async function execPkg(...args: any[]) { async function execPkg(...args: any[]) {
const { exec: pkgExec } = await import('@yao-pkg/pkg'); const { exec: pkgExec } = await import('pkg');
const outTap = new StdOutTap(true); const outTap = new StdOutTap(true);
try { try {
outTap.tap(); outTap.tap();
@ -185,18 +182,9 @@ async function execPkg(...args: any[]) {
* to be directly executed from inside another binary executable.) * to be directly executed from inside another binary executable.)
*/ */
async function buildPkg() { async function buildPkg() {
// https://github.com/vercel/pkg#targets
let targets = `linux-${arch}`;
if (process.platform === 'darwin') {
targets = `macos-${arch}`;
}
// TBC: not yet possible to build for Windows arm64 on x64 nodes
if (process.platform === 'win32') {
targets = `win-x64`;
}
const args = [ const args = [
'--targets', '--target',
targets, 'host',
'--output', '--output',
'build-bin/balena', 'build-bin/balena',
'package.json', 'package.json',
@ -211,6 +199,7 @@ async function buildPkg() {
const paths: Array<[string, string[], string[]]> = [ const paths: Array<[string, string[], string[]]> = [
// [platform, [source path], [destination path]] // [platform, [source path], [destination path]]
['*', ['open', 'xdg-open'], ['xdg-open']], ['*', ['open', 'xdg-open'], ['xdg-open']],
['*', ['opn', 'xdg-open'], ['xdg-open-402']],
['darwin', ['denymount', 'bin', 'denymount'], ['denymount']], ['darwin', ['denymount', 'bin', 'denymount'], ['denymount']],
]; ];
await Promise.all( await Promise.all(
@ -305,7 +294,7 @@ async function zipPkg() {
); );
} }
await fs.mkdirp(path.dirname(outputFile)); await fs.mkdirp(path.dirname(outputFile));
await new Promise<void>((resolve, reject) => { await new Promise((resolve, reject) => {
console.log(`Zipping standalone package to "${outputFile}"...`); console.log(`Zipping standalone package to "${outputFile}"...`);
const archive = archiver('zip', { const archive = archiver('zip', {
@ -326,11 +315,7 @@ async function zipPkg() {
}); });
} }
export async function signFilesForNotarization() { async function signFilesForNotarization() {
console.log('Signing files for notarization');
if (process.platform !== 'darwin') {
return;
}
console.log('Deleting unneeded zip files...'); console.log('Deleting unneeded zip files...');
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
klaw('node_modules/') klaw('node_modules/')
@ -430,7 +415,6 @@ export async function buildStandaloneZip() {
} }
async function renameInstallerFiles() { async function renameInstallerFiles() {
const oclifInstallers = await getOclifInstallersOriginalNames();
if (await fs.pathExists(oclifInstallers[process.platform])) { if (await fs.pathExists(oclifInstallers[process.platform])) {
await fs.rename( await fs.rename(
oclifInstallers[process.platform], oclifInstallers[process.platform],
@ -441,30 +425,20 @@ async function renameInstallerFiles() {
/** /**
* If the CSC_LINK and CSC_KEY_PASSWORD env vars are set, digitally sign the * If the CSC_LINK and CSC_KEY_PASSWORD env vars are set, digitally sign the
* executable installer using Microsoft SignTool.exe (Sign Tool) * executable installer by running the balena-io/scripts/shared/sign-exe.sh
* https://learn.microsoft.com/en-us/dotnet/framework/tools/signtool-exe * script (which must be in the PATH) using a MSYS2 bash shell.
*/ */
async function signWindowsInstaller() { async function signWindowsInstaller() {
if (process.env.SM_CODE_SIGNING_CERT_SHA1_HASH) { if (process.env.CSC_LINK && process.env.CSC_KEY_PASSWORD) {
const exeName = renamedOclifInstallers[process.platform]; const exeName = renamedOclifInstallers[process.platform];
console.log(`Signing installer "${exeName}"`); console.log(`Signing installer "${exeName}"`);
// trust ... await execFileAsync(MSYS2_BASH, [
await execFileAsync('signtool.exe', [ 'sign-exe.sh',
'sign', '-f',
'-sha1', exeName,
process.env.SM_CODE_SIGNING_CERT_SHA1_HASH,
'-tr',
process.env.TIMESTAMP_SERVER || 'http://timestamp.comodoca.com',
'-td',
'SHA256',
'-fd',
'SHA256',
'-d', '-d',
`balena-cli ${version}`, `balena-cli ${version}`,
exeName,
]); ]);
// ... but verify
await execFileAsync('signtool.exe', ['verify', '-pa', '-v', exeName]);
} else { } else {
console.log( console.log(
'Skipping installer signing step because CSC_* env vars are not set', 'Skipping installer signing step because CSC_* env vars are not set',
@ -476,20 +450,14 @@ async function signWindowsInstaller() {
* Wait for Apple Installer Notarization to continue * Wait for Apple Installer Notarization to continue
*/ */
async function notarizeMacInstaller(): Promise<void> { async function notarizeMacInstaller(): Promise<void> {
const teamId = process.env.XCODE_APP_LOADER_TEAM_ID || '66H43P8FRG'; const appleId = 'accounts+apple@balena.io';
const appleId = const { notarize } = await import('electron-notarize');
process.env.XCODE_APP_LOADER_EMAIL || 'accounts+apple@balena.io'; await notarize({
const appleIdPassword = process.env.XCODE_APP_LOADER_PASSWORD; appBundleId: 'io.balena.etcher',
appPath: renamedOclifInstallers.darwin,
if (appleIdPassword && teamId) { appleId,
await notarize({ appleIdPassword: '@keychain:CLI_PASSWORD',
tool: 'notarytool', });
teamId,
appPath: renamedOclifInstallers.darwin,
appleId,
appleIdPassword,
});
}
} }
/** /**
@ -503,10 +471,9 @@ export async function buildOclifInstaller() {
let packOpts = ['-r', ROOT]; let packOpts = ['-r', ROOT];
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
packOS = 'macos'; packOS = 'macos';
packOpts = packOpts.concat('--targets', `darwin-${arch}`);
} else if (process.platform === 'win32') { } else if (process.platform === 'win32') {
packOS = 'win'; packOS = 'win';
packOpts = packOpts.concat('--targets', 'win32-x64'); packOpts = packOpts.concat('-t', 'win32-x64');
} }
if (packOS) { if (packOS) {
console.log(`Building oclif installer for CLI ${version}`); console.log(`Building oclif installer for CLI ${version}`);
@ -517,14 +484,17 @@ export async function buildOclifInstaller() {
} }
for (const dir of dirs) { for (const dir of dirs) {
console.log(`rimraf(${dir})`); console.log(`rimraf(${dir})`);
await rimrafAsync(dir); await Bluebird.fromCallback((cb) => rimraf(dir, cb));
}
if (process.platform === 'darwin') {
console.log('Signing files for notarization...');
await signFilesForNotarization();
} }
console.log('======================================================='); console.log('=======================================================');
console.log(`oclif ${packCmd} ${packOpts.join(' ')}`); console.log(`oclif "${packCmd}" "${packOpts.join('" "')}"`);
console.log(`cwd="${process.cwd()}" ROOT="${ROOT}"`); console.log(`cwd="${process.cwd()}" ROOT="${ROOT}"`);
console.log('======================================================='); console.log('=======================================================');
const oclifPath = path.join(ROOT, 'node_modules', 'oclif'); await oclifRun([packCmd].concat(...packOpts));
await oclifRun([packCmd].concat(...packOpts), oclifPath);
await renameInstallerFiles(); await renameInstallerFiles();
// The Windows installer is explicitly signed here (oclif doesn't do it). // The Windows installer is explicitly signed here (oclif doesn't do it).
// The macOS installer is automatically signed by oclif (which runs the // The macOS installer is automatically signed by oclif (which runs the

View File

@ -17,7 +17,6 @@
import * as path from 'path'; import * as path from 'path';
import { MarkdownFileParser } from './utils'; import { MarkdownFileParser } from './utils';
import { GlobSync } from 'glob';
/** /**
* This is the skeleton of CLI documentation/reference web page at: * This is the skeleton of CLI documentation/reference web page at:
@ -25,111 +24,173 @@ import { GlobSync } from 'glob';
* *
* The `getCapitanoDoc` function in this module parses README.md and adds * The `getCapitanoDoc` function in this module parses README.md and adds
* some content to this object. * some content to this object.
*
* IMPORTANT
*
* All commands need to be stored under a folder in src/commands to maintain uniformity
* Generating docs will error out if directive not followed
* To add a custom heading for command docs, add the heading next to the folder name
* in the `commandHeadings` dictionary.
*
* This dictionary is the source of truth that creates the docs config which is used
* to generate the CLI documentation. By default, the folder name will be used.
*
*/ */
const capitanoDoc = {
interface Category {
title: string;
files: string[];
}
interface Documentation {
title: string;
introduction: string;
categories: Category[];
}
// Mapping folders names to custom headings in the docs
const commandHeadings: { [key: string]: string } = {
'api-key': 'API Keys',
login: 'Authentication',
whoami: 'Authentication',
logout: 'Authentication',
env: 'Environment Variables',
help: 'Help and Version',
'ssh-key': 'SSH Keys',
organization: 'Organizations',
os: 'OS',
util: 'Utilities',
build: 'Deploy',
join: 'Platform',
leave: 'Platform',
app: 'Apps',
block: 'Blocks',
device: 'Devices',
fleet: 'Fleets',
release: 'Releases',
tag: 'Tags',
};
// Fetch all available commands
const allCommandsPaths = new GlobSync('build/commands/**/*.js', {
ignore: 'build/commands/internal/**',
}).found;
// Throw error if any commands found outside of command directories
const illegalCommandPaths = allCommandsPaths.filter((commandPath: string) =>
/^build\/commands\/[^/]+\.js$/.test(commandPath),
);
if (illegalCommandPaths.length !== 0) {
throw new Error(
`Found the following commands without a command directory: ${illegalCommandPaths}\n
To resolve this error, move the respective commands to their resource directories or create new ones.\n
Refer to the automation/capitanodoc/capitanodoc.ts file for more information.`,
);
}
// Docs config template
const capitanoDoc: Documentation = {
title: 'balena CLI Documentation', title: 'balena CLI Documentation',
introduction: '', introduction: '',
categories: [], categories: [
{
title: 'API keys',
files: ['build/commands/api-key/generate.js'],
},
{
title: 'Fleet',
files: [
'build/commands/fleets.js',
'build/commands/fleet/index.js',
'build/commands/fleet/create.js',
'build/commands/fleet/purge.js',
'build/commands/fleet/rename.js',
'build/commands/fleet/restart.js',
'build/commands/fleet/rm.js',
],
},
{
title: 'Authentication',
files: [
'build/commands/login.js',
'build/commands/logout.js',
'build/commands/whoami.js',
],
},
{
title: 'Device',
files: [
'build/commands/devices/index.js',
'build/commands/devices/supported.js',
'build/commands/device/index.js',
'build/commands/device/deactivate.js',
'build/commands/device/identify.js',
'build/commands/device/init.js',
'build/commands/device/local-mode.js',
'build/commands/device/move.js',
'build/commands/device/os-update.js',
'build/commands/device/public-url.js',
'build/commands/device/purge.js',
'build/commands/device/reboot.js',
'build/commands/device/register.js',
'build/commands/device/rename.js',
'build/commands/device/restart.js',
'build/commands/device/rm.js',
'build/commands/device/shutdown.js',
],
},
{
title: 'Releases',
files: [
'build/commands/releases.js',
'build/commands/release/index.js',
'build/commands/release/finalize.js',
],
},
{
title: 'Environment Variables',
files: [
'build/commands/envs.js',
'build/commands/env/add.js',
'build/commands/env/rename.js',
'build/commands/env/rm.js',
],
},
{
title: 'Tags',
files: [
'build/commands/tags.js',
'build/commands/tag/rm.js',
'build/commands/tag/set.js',
],
},
{
title: 'Help and Version',
files: ['help', 'build/commands/version.js'],
},
{
title: 'Keys',
files: [
'build/commands/keys.js',
'build/commands/key/index.js',
'build/commands/key/add.js',
'build/commands/key/rm.js',
],
},
{
title: 'Logs',
files: ['build/commands/logs.js'],
},
{
title: 'Network',
files: [
'build/commands/scan.js',
'build/commands/ssh.js',
'build/commands/tunnel.js',
],
},
{
title: 'Notes',
files: ['build/commands/note.js'],
},
{
title: 'OS',
files: [
'build/commands/os/build-config.js',
'build/commands/os/configure.js',
'build/commands/os/versions.js',
'build/commands/os/download.js',
'build/commands/os/initialize.js',
],
},
{
title: 'Config',
files: [
'build/commands/config/generate.js',
'build/commands/config/inject.js',
'build/commands/config/read.js',
'build/commands/config/reconfigure.js',
'build/commands/config/write.js',
],
},
{
title: 'Preload',
files: ['build/commands/preload.js'],
},
{
title: 'Push',
files: ['build/commands/push.js'],
},
{
title: 'Settings',
files: ['build/commands/settings.js'],
},
{
title: 'Local',
files: [
'build/commands/local/configure.js',
'build/commands/local/flash.js',
],
},
{
title: 'Deploy',
files: ['build/commands/build.js', 'build/commands/deploy.js'],
},
{
title: 'Platform',
files: ['build/commands/join.js', 'build/commands/leave.js'],
},
{
title: 'Utilities',
files: ['build/commands/util/available-drives.js'],
},
{
title: 'Support',
files: ['build/commands/support.js'],
},
],
}; };
// Helper function to capitalize each word of directory name
function formatTitle(dir: string): string {
return dir.replace(/(^\w|\s\w)/g, (word) => word.toUpperCase());
}
// Create a map to track the categories for faster lookup
const categoriesMap: { [key: string]: Category } = {};
for (const commandPath of allCommandsPaths) {
const commandDir = path.basename(path.dirname(commandPath));
const heading = commandHeadings[commandDir] || formatTitle(commandDir);
if (!categoriesMap[heading]) {
categoriesMap[heading] = { title: heading, files: [] };
capitanoDoc.categories.push(categoriesMap[heading]);
}
categoriesMap[heading].files.push(commandPath);
}
// Sort Category titles alphabetically
capitanoDoc.categories = capitanoDoc.categories.sort((a, b) =>
a.title.localeCompare(b.title),
);
// Sort Category file paths alphabetically
capitanoDoc.categories.forEach((category) => {
category.files.sort((a, b) => a.localeCompare(b));
});
/** /**
* Modify and return the `capitanoDoc` object above in order to generate the * Modify and return the `capitanoDoc` object above in order to render the
* CLI documentation at docs/balena-cli.md * CLI documentation/reference web page at:
* https://www.balena.io/docs/reference/cli/
* *
* This function parses the README.md file to extract relevant sections * This function parses the README.md file to extract relevant sections
* for the documentation web page. * for the documentation web page.
@ -145,7 +206,7 @@ export async function getCapitanoDoc(): Promise<typeof capitanoDoc> {
throw new Error(`Error parsing section title`); throw new Error(`Error parsing section title`);
} }
// match[1] has the title, match[2] has the rest // match[1] has the title, match[2] has the rest
return match?.[2]; return match && match[2];
}), }),
mdParser.getSectionOfTitle('Installation'), mdParser.getSectionOfTitle('Installation'),
mdParser.getSectionOfTitle('Choosing a shell (command prompt/terminal)'), mdParser.getSectionOfTitle('Choosing a shell (command prompt/terminal)'),

View File

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import type { Command as OclifCommandClass } from '@oclif/core'; import { Command as OclifCommandClass } from '@oclif/command';
type OclifCommand = typeof OclifCommandClass; type OclifCommand = typeof OclifCommandClass;
@ -26,7 +26,7 @@ export interface Document {
export interface Category { export interface Category {
title: string; title: string;
commands: Array<OclifCommand & { name: string }>; commands: OclifCommand[];
} }
export { OclifCommand }; export { OclifCommand };

View File

@ -16,8 +16,9 @@
*/ */
import * as path from 'path'; import * as path from 'path';
import { getCapitanoDoc } from './capitanodoc'; import { getCapitanoDoc } from './capitanodoc';
import type { Category, Document, OclifCommand } from './doc-types'; import { Category, Document, OclifCommand } from './doc-types';
import * as markdown from './markdown'; import * as markdown from './markdown';
import { stripIndent } from '../../lib/utils/lazy';
/** /**
* Generates the markdown document (as a string) for the CLI documentation * Generates the markdown document (as a string) for the CLI documentation
@ -38,7 +39,7 @@ export async function renderMarkdown(): Promise<string> {
}; };
for (const jsFilename of commandCategory.files) { for (const jsFilename of commandCategory.files) {
category.commands.push(await importOclifCommands(jsFilename)); category.commands.push(...importOclifCommands(jsFilename));
} }
result.categories.push(category); result.categories.push(category);
} }
@ -46,23 +47,49 @@ export async function renderMarkdown(): Promise<string> {
return markdown.render(result); return markdown.render(result);
} }
async function importOclifCommands(jsFilename: string) { // Help is now managed via a plugin
const command = (await import(path.join(process.cwd(), jsFilename))) // This fake command allows capitanodoc to include help in docs
.default as OclifCommand; class FakeHelpCommand {
description = stripIndent`
List balena commands, or get detailed help for a specific command.
return { List balena commands, or get detailed help for a specific command.
...command, `;
// build/commands/device/index.js -> device
// build/commands/device/list.js -> device list examples = [
name: jsFilename '$ balena help',
.split('/') '$ balena help login',
.slice(2) '$ balena help os download',
.join(' ') ];
.split('.')
.slice(0, 1) args = [
.join(' ') {
.split(' index')[0], name: 'command',
} as Category['commands'][0]; description: 'command to show help for',
},
];
usage = 'help [command]';
flags = {
verbose: {
description: 'show additional commands',
char: '-v',
},
};
}
function importOclifCommands(jsFilename: string): OclifCommand[] {
// TODO: Currently oclif commands with no `usage` overridden will cause
// an error when parsed. This should be improved so that `usage` does not have
// to be overridden if not necessary.
const command: OclifCommand =
jsFilename === 'help'
? (new FakeHelpCommand() as unknown as OclifCommand)
: (require(path.join(process.cwd(), jsFilename)).default as OclifCommand);
return [command];
} }
/** /**
@ -78,5 +105,5 @@ async function printMarkdown() {
} }
} }
// eslint-disable-next-line @typescript-eslint/no-floating-promises // tslint:disable-next-line:no-floating-promises
printMarkdown(); printMarkdown();

View File

@ -14,31 +14,16 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import { Parser } from '@oclif/core'; import { flagUsages } from '@oclif/parser';
import * as ent from 'ent'; import * as ent from 'ent';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { capitanoizeOclifUsage } from '../../src/utils/oclif-utils'; import { getManualSortCompareFunction } from '../../lib/utils/helpers';
import type { Category, Document } from './doc-types'; import { capitanoizeOclifUsage } from '../../lib/utils/oclif-utils';
import { Category, Document, OclifCommand } from './doc-types';
function renderOclifCommand(command: Category['commands'][0]): string[] { function renderOclifCommand(command: OclifCommand): string[] {
const result = [`## ${ent.encode(command.name || '')}`]; const result = [`## ${ent.encode(command.usage || '')}`];
if (command.aliases?.length) {
result.push('### Aliases');
result.push(
command.aliases
.map(
(alias) =>
`- \`${alias}\`${command.deprecateAliases ? ' *(deprecated)*' : ''}`,
)
.join('\n'),
);
result.push(
`\nTo use one of the aliases, replace \`${command.name}\` with the alias.`,
);
}
result.push('### Description');
const description = (command.description || '') const description = (command.description || '')
.split('\n') .split('\n')
.slice(1) // remove the first line, which oclif uses as help header .slice(1) // remove the first line, which oclif uses as help header
@ -52,8 +37,8 @@ function renderOclifCommand(command: Category['commands'][0]): string[] {
if (!_.isEmpty(command.args)) { if (!_.isEmpty(command.args)) {
result.push('### Arguments'); result.push('### Arguments');
for (const [name, arg] of Object.entries(command.args!)) { for (const arg of command.args!) {
result.push(`#### ${name.toUpperCase()}`, arg.description || ''); result.push(`#### ${arg.name.toUpperCase()}`, arg.description || '');
} }
} }
@ -64,7 +49,7 @@ function renderOclifCommand(command: Category['commands'][0]): string[] {
continue; continue;
} }
flag.name = name; flag.name = name;
const flagUsage = Parser.flagUsages([flag]) const flagUsage = flagUsages([flag])
.map(([usage, _description]) => usage) .map(([usage, _description]) => usage)
.join() .join()
.trim(); .trim();
@ -95,7 +80,7 @@ function renderToc(categories: Category[]): string[] {
result.push( result.push(
category.commands category.commands
.map((command) => { .map((command) => {
const signature = capitanoizeOclifUsage(command.name); const signature = capitanoizeOclifUsage(command.usage);
return `\t- [${ent.encode(signature)}](${getAnchor(signature)})`; return `\t- [${ent.encode(signature)}](${getAnchor(signature)})`;
}) })
.join('\n'), .join('\n'),
@ -104,7 +89,33 @@ function renderToc(categories: Category[]): string[] {
return result; return result;
} }
const manualCategorySorting: { [category: string]: string[] } = {
'Environment Variables': ['envs', 'env rm', 'env add', 'env rename'],
OS: [
'os versions',
'os download',
'os build config',
'os configure',
'os initialize',
],
};
function sortCommands(doc: Document): void {
for (const category of doc.categories) {
if (category.title in manualCategorySorting) {
category.commands = category.commands.sort(
getManualSortCompareFunction<OclifCommand, string>(
manualCategorySorting[category.title],
(cmd: OclifCommand, x: string) =>
(cmd.usage || '').toString().replace(/\W+/g, ' ').includes(x),
),
);
}
}
}
export function render(doc: Document) { export function render(doc: Document) {
sortCommands(doc);
const result = [ const result = [
`# ${doc.title}`, `# ${doc.title}`,
doc.introduction, doc.introduction,

View File

@ -15,9 +15,41 @@
* limitations under the License. * limitations under the License.
*/ */
import type { OptionDefinition } from 'capitano';
import * as ent from 'ent';
import * as fs from 'fs'; import * as fs from 'fs';
import * as readline from 'readline'; import * as readline from 'readline';
export function getOptionPrefix(signature: string) {
if (signature.length > 1) {
return '--';
} else {
return '-';
}
}
export function getOptionSignature(signature: string) {
return `${getOptionPrefix(signature)}${signature}`;
}
export function parseCapitanoOption(option: OptionDefinition): string {
let result = getOptionSignature(option.signature);
if (Array.isArray(option.alias)) {
for (const alias of option.alias) {
result += `, ${getOptionSignature(alias)}`;
}
} else if (typeof option.alias === 'string') {
result += `, ${getOptionSignature(option.alias)}`;
}
if (option.parameter) {
result += ` <${option.parameter}>`;
}
return ent.encode(result);
}
export class MarkdownFileParser { export class MarkdownFileParser {
constructor(public mdFilePath: string) {} constructor(public mdFilePath: string) {}

View File

@ -15,12 +15,11 @@
* limitations under the License. * limitations under the License.
*/ */
// eslint-disable-next-line no-restricted-imports const stripIndent = require('common-tags/lib/stripIndent');
import { stripIndent } from 'common-tags'; const _ = require('lodash');
import * as _ from 'lodash'; const { promises: fs } = require('fs');
import { promises as fs } from 'fs'; const path = require('path');
import * as path from 'path'; const simplegit = require('simple-git/promise');
import { simpleGit } from 'simple-git';
const ROOT = path.normalize(path.join(__dirname, '..')); const ROOT = path.normalize(path.join(__dirname, '..'));
@ -32,7 +31,7 @@ const ROOT = path.normalize(path.join(__dirname, '..'));
* using `touch`. * using `touch`.
*/ */
async function checkBuildTimestamps() { async function checkBuildTimestamps() {
const git = simpleGit(ROOT); const git = simplegit(ROOT);
const docFile = path.join(ROOT, 'docs', 'balena-cli.md'); const docFile = path.join(ROOT, 'docs', 'balena-cli.md');
const [docStat, gitStatus] = await Promise.all([ const [docStat, gitStatus] = await Promise.all([
fs.stat(docFile), fs.stat(docFile),
@ -43,8 +42,8 @@ async function checkBuildTimestamps() {
...gitStatus.staged, ...gitStatus.staged,
...gitStatus.renamed.map((o) => o.to), ...gitStatus.renamed.map((o) => o.to),
]) ])
// select only staged files that start with src/ or typings/ // select only staged files that start with lib/ or typings/
.filter((f) => f.match(/^(src|typings)[/\\]/)) .filter((f) => f.match(/^(lib|typings)[/\\]/))
.map((f) => path.join(ROOT, f)); .map((f) => path.join(ROOT, f));
const fStats = await Promise.all(stagedFiles.map((f) => fs.stat(f))); const fStats = await Promise.all(stagedFiles.map((f) => fs.stat(f)));
@ -82,5 +81,4 @@ async function run() {
} }
} }
// eslint-disable-next-line @typescript-eslint/no-floating-promises
run(); run();

View File

@ -23,8 +23,8 @@ function parseSemver(version) {
* @param {string} v2 * @param {string} v2
*/ */
function semverGte(v1, v2) { function semverGte(v1, v2) {
const v1Array = parseSemver(v1); let v1Array = parseSemver(v1);
const v2Array = parseSemver(v2); let v2Array = parseSemver(v2);
for (let i = 0; i < 3; i++) { for (let i = 0; i < 3; i++) {
if (v1Array[i] < v2Array[i]) { if (v1Array[i] < v2Array[i]) {
return false; return false;

257
automation/deploy-bin.ts Normal file
View File

@ -0,0 +1,257 @@
/**
* @license
* Copyright 2019 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as Bluebird from 'bluebird';
import * as _ from 'lodash';
import * as semver from 'semver';
import { finalReleaseAssets, version } from './build-bin';
const { GITHUB_TOKEN } = process.env;
/**
* Create or update a release in GitHub's releases page, uploading the
* installer files (standalone zip + native oclif installers).
*/
export async function createGitHubRelease() {
console.log(`Publishing release ${version} to GitHub`);
const publishRelease = await import('publish-release');
const ghRelease = await Bluebird.fromCallback(
publishRelease.bind(null, {
token: GITHUB_TOKEN || '',
owner: 'balena-io',
repo: 'balena-cli',
tag: version,
name: `balena-CLI ${version}`,
reuseRelease: true,
assets: finalReleaseAssets[process.platform],
}),
);
console.log(`Release ${version} successful: ${ghRelease.html_url}`);
}
/**
* Top-level function to create a CLI release in GitHub's releases page:
* call zipStandaloneInstaller(), rename the files as we'd like them to
* display on the releases page, and call createGitHubRelease() to upload
* the files.
*/
export async function release() {
try {
await createGitHubRelease();
} catch (err) {
throw new Error(`Error creating GitHub release:\n${err}`);
}
}
/** Return a cached Octokit instance, creating a new one as needed. */
const getOctokit = _.once(function () {
const Octokit = (
require('@octokit/rest') as typeof import('@octokit/rest')
).Octokit.plugin(
(
require('@octokit/plugin-throttling') as typeof import('@octokit/plugin-throttling')
).throttling,
);
return new Octokit({
auth: GITHUB_TOKEN,
throttle: {
onRateLimit: (retryAfter: number, options: any) => {
console.warn(
`Request quota exhausted for request ${options.method} ${options.url}`,
);
// retries 3 times
if (options.request.retryCount < 3) {
console.log(`Retrying after ${retryAfter} seconds!`);
return true;
}
},
onAbuseLimit: (_retryAfter: number, options: any) => {
// does not retry, only logs a warning
console.warn(
`Abuse detected for request ${options.method} ${options.url}`,
);
},
},
});
});
/**
* Extract pagination information (current page, total pages, ordinal number)
* from the 'link' response header (example below), using the parse-link-header
* npm package:
* "link": "<https://api.github.com/repositories/187370853/releases?per_page=2&page=2>; rel=\"next\",
* <https://api.github.com/repositories/187370853/releases?per_page=2&page=3>; rel=\"last\""
*
* @param response Octokit response object (including response.headers.link)
* @param perPageDefault Default per_page pagination value if missing in URL
* @return Object where 'page' is the current page number (1-based),
* 'pages' is the total number of pages, and 'ordinal' is the ordinal number
* (3rd, 4th, 5th...) of the first item in the current page.
*/
function getPageNumbers(
response: any,
perPageDefault: number,
): { page: number; pages: number; ordinal: number } {
const res = { page: 1, pages: 1, ordinal: 1 };
if (!response.headers.link) {
return res;
}
const parse =
require('parse-link-header') as typeof import('parse-link-header');
const parsed = parse(response.headers.link);
if (parsed == null) {
throw new Error(`Failed to parse link header: '${response.headers.link}'`);
}
let perPage = perPageDefault;
if (parsed.next) {
if (parsed.next.per_page) {
perPage = parseInt(parsed.next.per_page, 10);
}
res.page = parseInt(parsed.next.page, 10) - 1;
res.pages = parseInt(parsed.last.page, 10);
} else {
if (parsed.prev.per_page) {
perPage = parseInt(parsed.prev.per_page, 10);
}
res.page = res.pages = parseInt(parsed.prev.page, 10) + 1;
}
res.ordinal = (res.page - 1) * perPage + 1;
return res;
}
/**
* Iterate over every GitHub release in the given owner/repo, check whether
* its tag_name matches against the affectedVersions semver spec, and if so
* replace its release description (body) with the given newDescription value.
* @param owner GitHub repo owner, e.g. 'balena-io' or 'pdcastro'
* @param repo GitHub repo, e.g. 'balena-cli'
* @param affectedVersions Semver spec, e.g. '2.6.1 - 7.10.9 || 8.0.0'
* @param newDescription New release description (body)
* @param editID Short string present in newDescription, e.g. '[AA101]', that
* can be searched to determine whether that release has already been updated.
*/
async function updateGitHubReleaseDescriptions(
owner: string,
repo: string,
affectedVersions: string,
newDescription: string,
editID: string,
) {
const perPage = 30;
const octokit = getOctokit();
const options = await octokit.repos.listReleases.endpoint.merge({
owner,
repo,
per_page: perPage,
});
let errCount = 0;
type Release =
import('@octokit/rest').RestEndpointMethodTypes['repos']['listReleases']['response']['data'][0];
for await (const response of octokit.paginate.iterator<Release>(options)) {
const {
page: thisPage,
pages: totalPages,
ordinal,
} = getPageNumbers(response, perPage);
let i = 0;
for (const cliRelease of response.data) {
const prefix = `[#${ordinal + i++} pg ${thisPage}/${totalPages}]`;
if (!cliRelease.id) {
console.error(
`${prefix} Error: missing release ID (errCount=${++errCount})`,
);
continue;
}
const skipMsg = `${prefix} skipping release "${cliRelease.tag_name}" (${cliRelease.id})`;
if (cliRelease.draft === true) {
console.info(`${skipMsg}: draft release`);
continue;
} else if (cliRelease.body && cliRelease.body.includes(editID)) {
console.info(`${skipMsg}: already updated`);
continue;
} else if (!semver.satisfies(cliRelease.tag_name, affectedVersions)) {
console.info(`${skipMsg}: outside version range`);
continue;
} else {
const updatedRelease = {
owner,
repo,
release_id: cliRelease.id,
body: newDescription,
};
let oldBodyPreview = cliRelease.body;
if (oldBodyPreview) {
oldBodyPreview = oldBodyPreview.replace(/\s+/g, ' ').trim();
if (oldBodyPreview.length > 12) {
oldBodyPreview = oldBodyPreview.substring(0, 9) + '...';
}
}
console.info(
`${prefix} updating release "${cliRelease.tag_name}" (${cliRelease.id}) old body="${oldBodyPreview}"`,
);
try {
await octokit.repos.updateRelease(updatedRelease);
} catch (err) {
console.error(
`${skipMsg}: Error: ${err.message} (count=${++errCount})`,
);
continue;
}
}
}
}
}
/**
* Add a warning description to CLI releases affected by a mixpanel tracking
* security issue (#1359). This function can be executed "manually" with the
* following command line:
*
* npx ts-node --type-check -P automation/tsconfig.json automation/run.ts fix1359
*/
export async function updateDescriptionOfReleasesAffectedByIssue1359() {
// Run only on Linux/Node10, instead of all platform/Node combinations.
// (It could have been any other platform, as long as it only runs once.)
if (process.platform !== 'linux' || semver.major(process.version) !== 10) {
return;
}
const owner = 'balena-io';
const repo = 'balena-cli';
const affectedVersions =
'2.6.1 - 7.10.9 || 8.0.0 - 8.1.0 || 9.0.0 - 9.15.6 || 10.0.0 - 10.17.5 || 11.0.0 - 11.7.2';
const editID = '[AA100]';
let newDescription = `
Please note: the "login" command in this release is affected by a
security issue fixed in versions
[7.10.10](https://github.com/balena-io/balena-cli/releases/tag/v7.10.10),
[8.1.1](https://github.com/balena-io/balena-cli/releases/tag/v8.1.1),
[9.15.7](https://github.com/balena-io/balena-cli/releases/tag/v9.15.7),
[10.17.6](https://github.com/balena-io/balena-cli/releases/tag/v10.17.6),
[11.7.3](https://github.com/balena-io/balena-cli/releases/tag/v11.7.3)
and later. If you need to use this version, avoid passing your password,
keys or tokens as command-line arguments. ${editID}`;
// remove line breaks and collapse white space
newDescription = newDescription.replace(/\s+/g, ' ').trim();
await updateGitHubReleaseDescriptions(
owner,
repo,
affectedVersions,
newDescription,
editID,
);
}

View File

@ -21,9 +21,12 @@ import {
buildOclifInstaller, buildOclifInstaller,
buildStandaloneZip, buildStandaloneZip,
catchUncommitted, catchUncommitted,
signFilesForNotarization,
testShrinkwrap, testShrinkwrap,
} from './build-bin'; } from './build-bin';
import {
release,
updateDescriptionOfReleasesAffectedByIssue1359,
} from './deploy-bin';
// DEBUG set to falsy for negative values else is truthy // DEBUG set to falsy for negative values else is truthy
process.env.DEBUG = ['0', 'no', 'false', '', undefined].includes( process.env.DEBUG = ['0', 'no', 'false', '', undefined].includes(
@ -37,6 +40,7 @@ process.env.DEBUG = ['0', 'no', 'false', '', undefined].includes(
* of the following strings, then call the appropriate functions: * of the following strings, then call the appropriate functions:
* 'build:installer' (to build a native oclif installer) * 'build:installer' (to build a native oclif installer)
* 'build:standalone' (to build a standalone pkg package) * 'build:standalone' (to build a standalone pkg package)
* 'release' (to create/update a GitHub release)
* *
* @param args Arguments to parse (default is process.argv.slice(2)) * @param args Arguments to parse (default is process.argv.slice(2))
*/ */
@ -50,16 +54,32 @@ async function parse(args?: string[]) {
const commands: { [cmd: string]: () => void | Promise<void> } = { const commands: { [cmd: string]: () => void | Promise<void> } = {
'build:installer': buildOclifInstaller, 'build:installer': buildOclifInstaller,
'build:standalone': buildStandaloneZip, 'build:standalone': buildStandaloneZip,
'sign:binaries': signFilesForNotarization,
'catch-uncommitted': catchUncommitted, 'catch-uncommitted': catchUncommitted,
'test-shrinkwrap': testShrinkwrap, 'test-shrinkwrap': testShrinkwrap,
fix1359: updateDescriptionOfReleasesAffectedByIssue1359,
release,
}; };
for (const arg of args) { for (const arg of args) {
if (!Object.hasOwn(commands, arg)) { if (!commands.hasOwnProperty(arg)) {
throw new Error(`command unknown: ${arg}`); throw new Error(`command unknown: ${arg}`);
} }
} }
// The BUILD_TMP env var is used as an alternative location for oclif
// (patched) to copy/extract the CLI files, run npm install and then
// create the NSIS executable installer for Windows. This was necessary
// to avoid issues with a 260-char limit on Windows paths (possibly a
// limitation of some library used by NSIS), as the "current working dir"
// provided by balena CI is a rather long path to start with.
if (process.platform === 'win32' && !process.env.BUILD_TMP) {
const randID = (await import('crypto'))
.randomBytes(6)
.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_'); // base64url (RFC 4648)
process.env.BUILD_TMP = `C:\\tmp\\${randID}`;
}
for (const arg of args) { for (const arg of args) {
try { try {
const cmdFunc = commands[arg]; const cmdFunc = commands[arg];
@ -83,5 +103,5 @@ export async function run(args?: string[]) {
} }
} }
// eslint-disable-next-line @typescript-eslint/no-floating-promises // tslint:disable-next-line:no-floating-promises
run(); run();

View File

@ -3,7 +3,7 @@ import * as semver from 'semver';
const changeTypes = ['major', 'minor', 'patch'] as const; const changeTypes = ['major', 'minor', 'patch'] as const;
const validateChangeType = (maybeChangeType = 'minor') => { const validateChangeType = (maybeChangeType: string = 'minor') => {
maybeChangeType = maybeChangeType.toLowerCase(); maybeChangeType = maybeChangeType.toLowerCase();
switch (maybeChangeType) { switch (maybeChangeType) {
case 'patch': case 'patch':
@ -107,11 +107,11 @@ async function $main() {
const changeType = process.argv[4] const changeType = process.argv[4]
? // if the caller specified a change type, use that one ? // if the caller specified a change type, use that one
validateChangeType(process.argv[4]) validateChangeType(process.argv[4])
: // use the same change type as in the dependency, but avoid major bumps : // use the same change type as in the dependency, but avoid major bumps
semverChangeType && semverChangeType !== 'major' semverChangeType && semverChangeType !== 'major'
? semverChangeType ? semverChangeType
: 'minor'; : 'minor';
console.log(`Using Change-type: ${changeType}`); console.log(`Using Change-type: ${changeType}`);
let { stdout: currentBranch } = await run('git rev-parse --abbrev-ref HEAD'); let { stdout: currentBranch } = await run('git rev-parse --abbrev-ref HEAD');
@ -136,4 +136,5 @@ async function main() {
} }
} }
void main(); // tslint:disable-next-line:no-floating-promises
main();

View File

@ -16,10 +16,8 @@
*/ */
import { spawn } from 'child_process'; import { spawn } from 'child_process';
import * as _ from 'lodash';
import * as path from 'path'; import * as path from 'path';
import * as fs from 'fs';
import { diffTrimmedLines } from 'diff';
import * as whichMod from 'which';
export const ROOT = path.join(__dirname, '..'); export const ROOT = path.join(__dirname, '..');
@ -67,6 +65,7 @@ export class StdOutTap {
* https://www.npmjs.com/package/diff * https://www.npmjs.com/package/diff
*/ */
export function diffLines(str1: string, str2: string): string { export function diffLines(str1: string, str2: string): string {
const { diffTrimmedLines } = require('diff');
const diffObjs = diffTrimmedLines(str1, str2); const diffObjs = diffTrimmedLines(str1, str2);
const prefix = (chunk: string, char: string) => const prefix = (chunk: string, char: string) =>
chunk chunk
@ -78,18 +77,15 @@ export function diffLines(str1: string, str2: string): string {
return part.added return part.added
? prefix(part.value, '+') ? prefix(part.value, '+')
: part.removed : part.removed
? prefix(part.value, '-') ? prefix(part.value, '-')
: prefix(part.value, ' '); : prefix(part.value, ' ');
}) })
.join('\n'); .join('\n');
return diffStr; return diffStr;
} }
export function loadPackageJson() { export function loadPackageJson() {
const packageJsonPath = path.join(ROOT, 'package.json'); return require(path.join(ROOT, 'package.json'));
const packageJson = fs.readFileSync(packageJsonPath, 'utf8');
return JSON.parse(packageJson);
} }
/** /**
@ -102,6 +98,7 @@ export function loadPackageJson() {
* @returns The program's full path, e.g. 'C:\WINDOWS\System32\OpenSSH\ssh.EXE' * @returns The program's full path, e.g. 'C:\WINDOWS\System32\OpenSSH\ssh.EXE'
*/ */
export async function which(program: string): Promise<string> { export async function which(program: string): Promise<string> {
const whichMod = await import('which');
let programPath: string; let programPath: string;
try { try {
programPath = await whichMod(program); programPath = await whichMod(program);
@ -132,7 +129,7 @@ export async function whichSpawn(
.on('error', reject) .on('error', reject)
.on('close', resolve); .on('close', resolve);
} catch (err) { } catch (err) {
reject(err as Error); reject(err);
} }
}); });
} catch (err) { } catch (err) {

View File

@ -1 +0,0 @@
run.js

23
bin/balena Executable file
View File

@ -0,0 +1,23 @@
#!/usr/bin/env node
// tslint:disable:no-var-requires
// We boost the threadpool size as ext2fs can deadlock with some
// operations otherwise, if the pool runs out.
process.env.UV_THREADPOOL_SIZE = '64';
// Disable oclif registering ts-node
process.env.OCLIF_TS_NODE = 0;
async function run() {
// Use fast-boot to cache require lookups, speeding up startup
await require('../build/fast-boot').start();
// Set the desired es version for downstream modules that support it
require('@balena/es-version').set('es2018');
// Run the CLI
await require('../build/app').run();
}
run();

View File

@ -1 +0,0 @@
dev.js

89
bin/balena-dev Executable file
View File

@ -0,0 +1,89 @@
#!/usr/bin/env node
// ****************************************************************************
// THIS IS FOR DEV PURPOSES ONLY AND WILL NOT BE PART OF THE PUBLISHED PACKAGE
// Before opening a PR you should build and test your changes using bin/balena
// ****************************************************************************
// tslint:disable:no-var-requires
// We boost the threadpool size as ext2fs can deadlock with some
// operations otherwise, if the pool runs out.
process.env.UV_THREADPOOL_SIZE = '64';
// Note on `fast-boot2`: We do not use `fast-boot2` with `balena-dev` because:
// * fast-boot2's cacheKiller option is configured to include the timestamps of
// the package.json and npm-shrinkwrap.json files, to avoid unexpected CLI
// behavior when changes are made to dependencies during development. This is
// generally a good thing, however, `balena-dev` (a few lines below) edits
// `package.json` to modify oclif paths, and this results in cache
// invalidation and a performance hit rather than speedup.
// * Even if the timestamps are removed from cacheKiller, so that there is no
// cache invalidation, fast-boot's speedup is barely noticeable when ts-node
// is used, e.g. 1.43s vs 1.4s when running `balena version`.
// * `fast-boot` causes unexpected behavior when used with `npm link` or
// when the `node_modules` folder is manually modified (affecting transitive
// dependencies) during development (e.g. bug investigations). A workaround
// is to use `balena-dev` without `fast-boot`. See also notes in
// `CONTRIBUTING.md`.
const path = require('path');
const rootDir = path.join(__dirname, '..');
// Allow balena-dev to work with oclif by temporarily
// pointing oclif config options to lib/ instead of build/
modifyOclifPaths();
// Undo changes on exit
process.on('exit', function () {
modifyOclifPaths(true);
});
// Undo changes in case of ctrl-c
process.on('SIGINT', function () {
modifyOclifPaths(true);
// Note process exit here will interfere with commands that do their own SIGINT handling,
// but without it commands can not be exited.
// So currently using balena-dev does not guarantee proper exit behaviour when using ctrl-c.
// Ideally a better solution is needed.
process.exit();
});
// Set the desired es version for downstream modules that support it
require('@balena/es-version').set('es2018');
// Note: before ts-node v6.0.0, 'transpile-only' (no type checking) was the
// default option. We upgraded ts-node and found that adding 'transpile-only'
// was necessary to avoid a mysterious 'null' error message. On the plus side,
// it is supposed to run faster. We still benefit from type checking when
// running 'npm run build'.
require('ts-node').register({
project: path.join(rootDir, 'tsconfig.json'),
transpileOnly: true,
});
require('../lib/app').run();
// Modify package.json oclif paths from build/ -> lib/, or vice versa
function modifyOclifPaths(revert) {
const fs = require('fs');
const packageJsonPath = path.join(rootDir, 'package.json');
const packageJson = fs.readFileSync(packageJsonPath, 'utf8');
const packageObj = JSON.parse(packageJson);
if (!packageObj.oclif) {
return;
}
let oclifSectionText = JSON.stringify(packageObj.oclif);
if (!revert) {
oclifSectionText = oclifSectionText.replace(/\/build\//g, '/lib/');
} else {
oclifSectionText = oclifSectionText.replace(/\/lib\//g, '/build/');
}
packageObj.oclif = JSON.parse(oclifSectionText);
fs.writeFileSync(
packageJsonPath,
`${JSON.stringify(packageObj, null, 2)}\n`,
'utf8',
);
}

View File

@ -1,3 +0,0 @@
@echo off
node "%~dp0\run" %*

View File

@ -1,90 +0,0 @@
#!/usr/bin/env node
// ****************************************************************************
// THIS IS FOR DEV PURPOSES ONLY AND WILL NOT BE PART OF THE PUBLISHED PACKAGE
// Before opening a PR you should build and test your changes using bin/balena
// ****************************************************************************
// We boost the threadpool size as ext2fs can deadlock with some
// operations otherwise, if the pool runs out.
process.env.UV_THREADPOOL_SIZE = '64';
// Note on `fast-boot2`: We do not use `fast-boot2` with `balena-dev` because:
// * fast-boot2's cacheKiller option is configured to include the timestamps of
// the package.json and npm-shrinkwrap.json files, to avoid unexpected CLI
// behavior when changes are made to dependencies during development. This is
// generally a good thing, however, `balena-dev` (a few lines below) edits
// `package.json` to modify oclif paths, and this results in cache
// invalidation and a performance hit rather than speedup.
// * Even if the timestamps are removed from cacheKiller, so that there is no
// cache invalidation, fast-boot's speedup is barely noticeable when ts-node
// is used, e.g. 1.43s vs 1.4s when running `balena version`.
// * `fast-boot` causes unexpected behavior when used with `npm link` or
// when the `node_modules` folder is manually modified (affecting transitive
// dependencies) during development (e.g. bug investigations). A workaround
// is to use `balena-dev` without `fast-boot`. See also notes in
// `CONTRIBUTING.md`.
const path = require('path');
const rootDir = path.join(__dirname, '..');
// Allow balena-dev to work with oclif by temporarily
// pointing oclif config options to src/ instead of build/
modifyOclifPaths();
// Undo changes on exit
process.on('exit', function () {
modifyOclifPaths(true);
});
// Undo changes in case of ctrl-c
process.on('SIGINT', function () {
modifyOclifPaths(true);
// Note process exit here will interfere with commands that do their own SIGINT handling,
// but without it commands can not be exited.
// So currently using balena-dev does not guarantee proper exit behaviour when using ctrl-c.
// Ideally a better solution is needed.
process.exit();
});
// Set the desired es version for downstream modules that support it
require('@balena/es-version').set('es2018');
// Note: before ts-node v6.0.0, 'transpile-only' (no type checking) was the
// default option. We upgraded ts-node and found that adding 'transpile-only'
// was necessary to avoid a mysterious 'null' error message. On the plus side,
// it is supposed to run faster. We still benefit from type checking when
// running 'npm run build'.
require('ts-node').register({
project: path.join(rootDir, 'tsconfig.json'),
transpileOnly: true,
});
void require('../src/app').run(undefined, {
dir: __dirname,
development: true,
});
// Modify package.json oclif paths from build/ -> src/, or vice versa
function modifyOclifPaths(revert) {
const fs = require('fs');
const packageJsonPath = path.join(rootDir, 'package.json');
const packageJson = fs.readFileSync(packageJsonPath, 'utf8');
const packageObj = JSON.parse(packageJson);
if (!packageObj.oclif) {
return;
}
let oclifSectionText = JSON.stringify(packageObj.oclif);
if (!revert) {
oclifSectionText = oclifSectionText.replace(/\/build\//g, '/src/');
} else {
oclifSectionText = oclifSectionText.replace(/\/src\//g, '/build/');
}
packageObj.oclif = JSON.parse(oclifSectionText);
fs.writeFileSync(
packageJsonPath,
`${JSON.stringify(packageObj, null, 2)}\n`,
'utf8',
);
}

View File

@ -1,3 +0,0 @@
@echo off
node "%~dp0\run" %*

View File

@ -1,21 +0,0 @@
#!/usr/bin/env node
// We boost the threadpool size as ext2fs can deadlock with some
// operations otherwise, if the pool runs out.
process.env.UV_THREADPOOL_SIZE = '64';
// Disable oclif registering ts-node
process.env.OCLIF_TS_NODE = '0';
async function run() {
// Use fast-boot to cache require lookups, speeding up startup
await require('../build/fast-boot').start();
// Set the desired es version for downstream modules that support it
require('@balena/es-version').set('es2018');
// Run the CLI
await require('../build/app').run(undefined, { dir: __dirname });
}
void run();

View File

@ -8,28 +8,25 @@ _balena() {
local context state line curcontext="$curcontext" local context state line curcontext="$curcontext"
# Valid top-level completions # Valid top-level completions
main_commands=( api-key app block build config deploy device device-type env fleet internal join leave local login logout organization os preload push release settings ssh-key support tag util version whoami ) 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 # Sub-completions
api_key_cmds=( generate list revoke ) api_key_cmds=( generate )
app_cmds=( create )
block_cmds=( create )
config_cmds=( generate inject read reconfigure write ) config_cmds=( generate inject read reconfigure write )
device_type_cmds=( list ) 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 detect identify init list local-mode logs move note os-update pin public-url purge reboot register rename restart rm shutdown ssh start-service stop-service track-fleet tunnel ) devices_cmds=( supported )
env_cmds=( list rename rm set ) env_cmds=( add rename rm )
fleet_cmds=( create list pin purge rename restart rm track-latest ) fleet_cmds=( create pin purge rename restart rm track-latest )
internal_cmds=( osinit ) internal_cmds=( osinit )
key_cmds=( add rm )
local_cmds=( configure flash ) local_cmds=( configure flash )
organization_cmds=( list )
os_cmds=( build-config configure download initialize versions ) os_cmds=( build-config configure download initialize versions )
release_cmds=( finalize invalidate list validate ) release_cmds=( finalize invalidate validate )
ssh_key_cmds=( add list rm ) tag_cmds=( rm set )
tag_cmds=( list rm set )
_arguments -C \ _arguments -C \
'(- 1 *)--version[show version and exit]' \ '(- 1 *)--version[show version and exit]' \
'(- 1 *)--help[show help options and exit]' \ '(- 1 *)'{-h,--help}'[show help options and exit]' \
'1:first command:_balena_main_cmds' \ '1:first command:_balena_main_cmds' \
'2:second command:_balena_sec_cmds' \ '2:second command:_balena_sec_cmds' \
&& ret=0 && ret=0
@ -46,21 +43,15 @@ _balena_sec_cmds() {
"api-key") "api-key")
_describe -t api_key_cmds 'api-key_cmd' api_key_cmds "$@" && ret=0 _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") "config")
_describe -t config_cmds 'config_cmd' config_cmds "$@" && ret=0 _describe -t config_cmds 'config_cmd' config_cmds "$@" && ret=0
;; ;;
"device-type")
_describe -t device_type_cmds 'device-type_cmd' device_type_cmds "$@" && ret=0
;;
"device") "device")
_describe -t device_cmds 'device_cmd' device_cmds "$@" && ret=0 _describe -t device_cmds 'device_cmd' device_cmds "$@" && ret=0
;; ;;
"devices")
_describe -t devices_cmds 'devices_cmd' devices_cmds "$@" && ret=0
;;
"env") "env")
_describe -t env_cmds 'env_cmd' env_cmds "$@" && ret=0 _describe -t env_cmds 'env_cmd' env_cmds "$@" && ret=0
;; ;;
@ -70,21 +61,18 @@ _balena_sec_cmds() {
"internal") "internal")
_describe -t internal_cmds 'internal_cmd' internal_cmds "$@" && ret=0 _describe -t internal_cmds 'internal_cmd' internal_cmds "$@" && ret=0
;; ;;
"key")
_describe -t key_cmds 'key_cmd' key_cmds "$@" && ret=0
;;
"local") "local")
_describe -t local_cmds 'local_cmd' local_cmds "$@" && ret=0 _describe -t local_cmds 'local_cmd' local_cmds "$@" && ret=0
;; ;;
"organization")
_describe -t organization_cmds 'organization_cmd' organization_cmds "$@" && ret=0
;;
"os") "os")
_describe -t os_cmds 'os_cmd' os_cmds "$@" && ret=0 _describe -t os_cmds 'os_cmd' os_cmds "$@" && ret=0
;; ;;
"release") "release")
_describe -t release_cmds 'release_cmd' release_cmds "$@" && ret=0 _describe -t release_cmds 'release_cmd' release_cmds "$@" && ret=0
;; ;;
"ssh-key")
_describe -t ssh_key_cmds 'ssh-key_cmd' ssh_key_cmds "$@" && ret=0
;;
"tag") "tag")
_describe -t tag_cmds 'tag_cmd' tag_cmds "$@" && ret=0 _describe -t tag_cmds 'tag_cmd' tag_cmds "$@" && ret=0
;; ;;

View File

@ -7,23 +7,20 @@ _balena_complete()
local cur prev local cur prev
# Valid top-level completions # Valid top-level completions
main_commands="api-key app block build config deploy device device-type env fleet internal join leave local login logout organization os preload push release settings ssh-key support tag util version whoami" 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 # Sub-completions
api_key_cmds="generate list revoke" api_key_cmds="generate"
app_cmds="create"
block_cmds="create"
config_cmds="generate inject read reconfigure write" config_cmds="generate inject read reconfigure write"
device_type_cmds="list" 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 detect identify init list local-mode logs move note os-update pin public-url purge reboot register rename restart rm shutdown ssh start-service stop-service track-fleet tunnel" devices_cmds="supported"
env_cmds="list rename rm set" env_cmds="add rename rm"
fleet_cmds="create list pin purge rename restart rm track-latest" fleet_cmds="create pin purge rename restart rm track-latest"
internal_cmds="osinit" internal_cmds="osinit"
key_cmds="add rm"
local_cmds="configure flash" local_cmds="configure flash"
organization_cmds="list"
os_cmds="build-config configure download initialize versions" os_cmds="build-config configure download initialize versions"
release_cmds="finalize invalidate list validate" release_cmds="finalize invalidate validate"
ssh_key_cmds="add list rm" tag_cmds="rm set"
tag_cmds="list rm set"
@ -40,21 +37,15 @@ _balena_complete()
api-key) api-key)
COMPREPLY=( $(compgen -W "$api_key_cmds" -- $cur) ) COMPREPLY=( $(compgen -W "$api_key_cmds" -- $cur) )
;; ;;
app)
COMPREPLY=( $(compgen -W "$app_cmds" -- $cur) )
;;
block)
COMPREPLY=( $(compgen -W "$block_cmds" -- $cur) )
;;
config) config)
COMPREPLY=( $(compgen -W "$config_cmds" -- $cur) ) COMPREPLY=( $(compgen -W "$config_cmds" -- $cur) )
;; ;;
device-type)
COMPREPLY=( $(compgen -W "$device_type_cmds" -- $cur) )
;;
device) device)
COMPREPLY=( $(compgen -W "$device_cmds" -- $cur) ) COMPREPLY=( $(compgen -W "$device_cmds" -- $cur) )
;; ;;
devices)
COMPREPLY=( $(compgen -W "$devices_cmds" -- $cur) )
;;
env) env)
COMPREPLY=( $(compgen -W "$env_cmds" -- $cur) ) COMPREPLY=( $(compgen -W "$env_cmds" -- $cur) )
;; ;;
@ -64,21 +55,18 @@ _balena_complete()
internal) internal)
COMPREPLY=( $(compgen -W "$internal_cmds" -- $cur) ) COMPREPLY=( $(compgen -W "$internal_cmds" -- $cur) )
;; ;;
key)
COMPREPLY=( $(compgen -W "$key_cmds" -- $cur) )
;;
local) local)
COMPREPLY=( $(compgen -W "$local_cmds" -- $cur) ) COMPREPLY=( $(compgen -W "$local_cmds" -- $cur) )
;; ;;
organization)
COMPREPLY=( $(compgen -W "$organization_cmds" -- $cur) )
;;
os) os)
COMPREPLY=( $(compgen -W "$os_cmds" -- $cur) ) COMPREPLY=( $(compgen -W "$os_cmds" -- $cur) )
;; ;;
release) release)
COMPREPLY=( $(compgen -W "$release_cmds" -- $cur) ) COMPREPLY=( $(compgen -W "$release_cmds" -- $cur) )
;; ;;
ssh-key)
COMPREPLY=( $(compgen -W "$ssh_key_cmds" -- $cur) )
;;
tag) tag)
COMPREPLY=( $(compgen -W "$tag_cmds" -- $cur) ) COMPREPLY=( $(compgen -W "$tag_cmds" -- $cur) )
;; ;;

View File

@ -31,9 +31,9 @@ if (fs.existsSync(commandsFilePath)) {
const commandsJson = JSON.parse(fs.readFileSync(commandsFilePath, 'utf8')); const commandsJson = JSON.parse(fs.readFileSync(commandsFilePath, 'utf8'));
const mainCommands = []; var mainCommands = [];
const additionalCommands = []; var additionalCommands = [];
for (const key of Object.keys(commandsJson.commands).sort()) { for (const key of Object.keys(commandsJson.commands)) {
const cmd = key.split(':'); const cmd = key.split(':');
if (cmd.length > 1) { if (cmd.length > 1) {
additionalCommands.push(cmd); additionalCommands.push(cmd);
@ -72,8 +72,8 @@ fs.readFile(bashFilePathIn, 'utf8', function (err, data) {
/\$main_commands\$/g, /\$main_commands\$/g,
'main_commands="' + mainCommandsStr + '"', 'main_commands="' + mainCommandsStr + '"',
); );
let subCommands = []; var subCommands = [];
let prevElement = additionalCommands[0][0]; var prevElement = additionalCommands[0][0];
additionalCommands.forEach(function (element) { additionalCommands.forEach(function (element) {
if (element[0] === prevElement) { if (element[0] === prevElement) {
subCommands.push(element[1]); subCommands.push(element[1]);
@ -134,8 +134,8 @@ fs.readFile(zshFilePathIn, 'utf8', function (err, data) {
/\$main_commands\$/g, /\$main_commands\$/g,
'main_commands=( ' + mainCommandsStr + ' )', 'main_commands=( ' + mainCommandsStr + ' )',
); );
let subCommands = []; var subCommands = [];
let prevElement = additionalCommands[0][0]; var prevElement = additionalCommands[0][0];
additionalCommands.forEach(function (element) { additionalCommands.forEach(function (element) {
if (element[0] === prevElement) { if (element[0] === prevElement) {
subCommands.push(element[1]); subCommands.push(element[1]);

View File

@ -14,7 +14,7 @@ $sub_cmds$
_arguments -C \ _arguments -C \
'(- 1 *)--version[show version and exit]' \ '(- 1 *)--version[show version and exit]' \
'(- 1 *)--help[show help options and exit]' \ '(- 1 *)'{-h,--help}'[show help options and exit]' \
'1:first command:_balena_main_cmds' \ '1:first command:_balena_main_cmds' \
'2:second command:_balena_sec_cmds' \ '2:second command:_balena_sec_cmds' \
&& ret=0 && ret=0

File diff suppressed because it is too large Load Diff

View File

@ -1,32 +0,0 @@
const { FlatCompat } = require('@eslint/eslintrc');
const compat = new FlatCompat({
baseDirectory: __dirname,
});
module.exports = [
...require('@balena/lint/config/eslint.config'),
...compat.config({
parserOptions: {
project: 'tsconfig.dev.json',
},
ignorePatterns: ['**/generate-completion.js', '**/bin/**/*'],
rules: {
ignoreDefinitionFiles: 0,
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-shadow': 'off',
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/no-require-imports': 'off',
'@typescript-eslint/no-unnecessary-type-assertion': 'off',
'@typescript-eslint/prefer-nullish-coalescing': 'warn',
'no-restricted-imports': ['error', {
paths: ['resin-cli-visuals', 'chalk', 'common-tags', 'resin-cli-form'],
}],
'@typescript-eslint/no-unused-vars': ['error', {
argsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
}],
},
}),
];

View File

@ -16,15 +16,14 @@
*/ */
import * as packageJSON from '../package.json'; import * as packageJSON from '../package.json';
import type { AppOptions } from './preparser';
import { import {
AppOptions,
checkDeletedCommand, checkDeletedCommand,
preparseArgs, preparseArgs,
unsupportedFlag, unsupportedFlag,
} from './preparser'; } from './preparser';
import { CliSettings } from './utils/bootstrap'; import { CliSettings } from './utils/bootstrap';
import { onceAsync } from './utils/lazy'; import { onceAsync } from './utils/lazy';
import { run as mainRun, settings } from '@oclif/core';
/** /**
* Sentry.io setup * Sentry.io setup
@ -101,9 +100,11 @@ async function init() {
/** Execute the oclif parser and the CLI command. */ /** Execute the oclif parser and the CLI command. */
async function oclifRun(command: string[], options: AppOptions) { async function oclifRun(command: string[], options: AppOptions) {
let deprecationPromise: Promise<void> | undefined; let deprecationPromise: Promise<void>;
// check and enforce the CLI's deprecation policy // check and enforce the CLI's deprecation policy
if (!(unsupportedFlag || process.env.BALENARC_UNSUPPORTED)) { if (unsupportedFlag || process.env.BALENARC_UNSUPPORTED) {
deprecationPromise = Promise.resolve();
} else {
const { DeprecationChecker } = await import('./deprecation'); const { DeprecationChecker } = await import('./deprecation');
const deprecationChecker = new DeprecationChecker(packageJSON.version); const deprecationChecker = new DeprecationChecker(packageJSON.version);
// warnAndAbortIfDeprecated uses previously cached data only // warnAndAbortIfDeprecated uses previously cached data only
@ -113,16 +114,10 @@ async function oclifRun(command: string[], options: AppOptions) {
} }
const runPromise = (async function (shouldFlush: boolean) { const runPromise = (async function (shouldFlush: boolean) {
const { CustomMain } = await import('./utils/oclif-utils');
let isEEXIT = false; let isEEXIT = false;
try { try {
if (options.development) { await CustomMain.run(command);
// In dev mode -> use ts-node and dev plugins
process.env.NODE_ENV = 'development';
settings.debug = true;
}
// For posteriority: We can't use default oclif 'execute' as
// We customize error handling and flushing
await mainRun(command, options.loadOptions ?? options.dir);
} catch (error) { } catch (error) {
// oclif sometimes exits with ExitError code EEXIT 0 (not an error), // oclif sometimes exits with ExitError code EEXIT 0 (not an error),
// for example the `balena help` command. // for example the `balena help` command.
@ -135,8 +130,7 @@ async function oclifRun(command: string[], options: AppOptions) {
} }
} }
if (shouldFlush) { if (shouldFlush) {
const { flush } = await import('@oclif/core'); await import('@oclif/command/flush');
await flush();
} }
// TODO: figure out why we need to call fast-boot stop() here, in // 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. // addition to calling it in the main `run()` function in this file.
@ -151,13 +145,13 @@ async function oclifRun(command: string[], options: AppOptions) {
} }
})(!options.noFlush); })(!options.noFlush);
const { trackPromise } = await import('./hooks/prerun'); const { trackPromise } = await import('./hooks/prerun/track');
await Promise.all([trackPromise, deprecationPromise, runPromise]); await Promise.all([trackPromise, deprecationPromise, runPromise]);
} }
/** CLI entrypoint. Called by the `bin/run.js` and `bin/dev.js` scripts. */ /** CLI entrypoint. Called by the `bin/balena` and `bin/balena-dev` scripts. */
export async function run(cliArgs = process.argv, options: AppOptions) { export async function run(cliArgs = process.argv, options: AppOptions = {}) {
try { try {
const { setOfflineModeEnvVars, normalizeEnvVars, pkgExec } = await import( const { setOfflineModeEnvVars, normalizeEnvVars, pkgExec } = await import(
'./utils/bootstrap' './utils/bootstrap'

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

174
lib/command.ts Normal file
View File

@ -0,0 +1,174 @@
/**
* @license
* Copyright 2020 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Command from '@oclif/command';
import {
InsufficientPrivilegesError,
NotAvailableInOfflineModeError,
} from './errors';
import { stripIndent } from './utils/lazy';
import * as output from './framework/output';
export default abstract class BalenaCommand extends Command {
/**
* When set to true, command will be listed in `help`,
* otherwise listed in `help --verbose` with secondary commands.
*/
public static primary = false;
/**
* Require elevated privileges to run.
* When set to true, command will exit with an error
* if executed without root on Mac/Linux
* or if executed by non-Administrator on Windows.
*/
public static root = false;
/**
* Require authentication to run.
* When set to true, command will exit with an error
* if user is not already logged in.
*/
public static authenticated = false;
/**
* Require an internet connection to run.
* When set to true, command will exit with an error
* if user is running in offline mode (BALENARC_OFFLINE_MODE).
*/
public static offlineCompatible = false;
/**
* Accept piped input.
* When set to true, command will read from stdin during init
* and make contents available on member `stdin`.
*/
public static readStdin = false;
public stdin: string;
/**
* Throw InsufficientPrivilegesError if not root on Mac/Linux
* or non-Administrator on Windows.
*
* Called automatically if `root=true`.
* Can be called explicitly by command implementation, if e.g.:
* - check should only be done conditionally
* - other code needs to execute before check
*/
protected static async checkElevatedPrivileges() {
const isElevated = await (await import('is-elevated'))();
if (!isElevated) {
throw new InsufficientPrivilegesError(
'You need root/admin privileges to run this command',
);
}
}
/**
* Throw NotLoggedInError if not logged in.
*
* Called automatically if `authenticated=true`.
* Can be called explicitly by command implementation, if e.g.:
* - check should only be done conditionally
* - other code needs to execute before check
*
* Note, currently public to allow use outside of derived commands
* (as some command implementations require this. Can be made protected
* if this changes).
*
* @throws {NotLoggedInError}
*/
public static async checkLoggedIn() {
await (await import('./utils/patterns')).checkLoggedIn();
}
/**
* Throw NotLoggedInError if not logged in when condition true.
*
* @param {boolean} doCheck - will check if true.
* @throws {NotLoggedInError}
*/
public static async checkLoggedInIf(doCheck: boolean) {
if (doCheck) {
await this.checkLoggedIn();
}
}
/**
* Throw NotAvailableInOfflineModeError if in offline mode.
*
* Called automatically if `onlineOnly=true`.
* Can be called explicitly by command implementation, if e.g.:
* - check should only be done conditionally
* - other code needs to execute before check
*
* Note, currently public to allow use outside of derived commands
* (as some command implementations require this. Can be made protected
* if this changes).
*
* @throws {NotAvailableInOfflineModeError}
*/
public static checkNotUsingOfflineMode() {
if (process.env.BALENARC_OFFLINE_MODE) {
throw new NotAvailableInOfflineModeError(stripIndent`
This command requires an internet connection, and cannot be used in offline mode.
To leave offline mode, unset the BALENARC_OFFLINE_MODE environment variable.
`);
}
}
/**
* Read stdin contents and make available to command.
*
* This approach could be improved in the future to automatically set argument
* values from stdin based in configuration, minimising command implementation.
*/
protected async getStdin() {
this.stdin = await (await import('get-stdin'))();
}
/**
* Get a logger instance.
*/
protected static async getLogger() {
return (await import('./utils/logger')).getLogger();
}
protected async init() {
const ctr = this.constructor as typeof BalenaCommand;
if (ctr.root) {
await BalenaCommand.checkElevatedPrivileges();
}
if (ctr.authenticated) {
await BalenaCommand.checkLoggedIn();
}
if (!ctr.offlineCompatible) {
BalenaCommand.checkNotUsingOfflineMode();
}
if (ctr.readStdin) {
await this.getStdin();
}
}
protected outputMessage = output.outputMessage;
protected outputData = output.outputData;
}

View File

@ -0,0 +1,85 @@
/**
* @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/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.
Generate a new balenaCloud API key for the current user, with the given
name. The key will be logged to the console.
This key can be used to log into the CLI using 'balena login --token <key>',
or to authenticate requests to the API with an 'Authorization: Bearer <key>' header.
`;
public static examples = ['$ balena api-key generate "Jenkins Key"'];
public static args = [
{
name: 'name',
description: 'the API key name',
required: true,
},
];
public static usage = 'api-key generate <name>';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(GenerateCmd);
let key;
try {
key = await getBalenaSdk().models.apiKey.create(params.name);
} catch (e) {
if (e.name === 'BalenaNotLoggedIn') {
throw new ExpectedError(stripIndent`
This command cannot be run when logged in with an API key.
Please login again with 'balena login' and select an alternative method.
`);
} else {
throw e;
}
}
console.log(stripIndent`
Registered api key '${params.name}':
${key}
This key will not be shown again, so please save it now.
`);
}
}

View File

@ -15,36 +15,31 @@
* limitations under the License. * limitations under the License.
*/ */
import { Args, Flags, Command } from '@oclif/core'; import { flags } from '@oclif/command';
import { getBalenaSdk } from '../../utils/lazy'; import Command from '../command';
import * as cf from '../../utils/common-flags'; import { getBalenaSdk } from '../utils/lazy';
import * as compose from '../../utils/compose'; import * as cf from '../utils/common-flags';
import type { import * as compose from '../utils/compose';
ApplicationType, import type { Application, ApplicationType, BalenaSDK } from 'balena-sdk';
BalenaSDK,
DeviceType,
PineOptions,
PineTypedResult,
} from 'balena-sdk';
import { import {
buildArgDeprecation, buildArgDeprecation,
dockerignoreHelp, dockerignoreHelp,
registrySecretsHelp, registrySecretsHelp,
} from '../../utils/messages'; } from '../utils/messages';
import type { ComposeCliFlags, ComposeOpts } from '../../utils/compose-types'; import type { ComposeCliFlags, ComposeOpts } from '../utils/compose-types';
import { buildProject, composeCliFlags } from '../../utils/compose_ts'; import { buildProject, composeCliFlags } from '../utils/compose_ts';
import type { BuildOpts, DockerCliFlags } from '../../utils/docker'; import type { BuildOpts, DockerCliFlags } from '../utils/docker';
import { dockerCliFlags } from '../../utils/docker'; import { dockerCliFlags } from '../utils/docker';
type ComposeGenerateOptsParam = Parameters<typeof compose.generateOpts>[0]; interface FlagsDef extends ComposeCliFlags, DockerCliFlags {
interface PrepareBuildOpts
extends ComposeCliFlags,
DockerCliFlags,
ComposeGenerateOptsParam {
arch?: string; arch?: string;
deviceType?: string; deviceType?: string;
fleet?: string; fleet?: string;
source?: string; // Not part of command profile - source param copied here.
help: void;
}
interface ArgsDef {
source?: string; source?: string;
} }
@ -73,73 +68,75 @@ ${dockerignoreHelp}
public static examples = [ public static examples = [
'$ balena build --fleet myFleet', '$ balena build --fleet myFleet',
'$ balena build ./source/ --fleet myorg/myfleet', '$ balena build ./source/ --fleet myorg/myfleet',
'$ balena build --deviceType raspberrypi3 --emulated',
'$ balena build --deviceType raspberrypi3 --arch armv7hf --emulated', '$ balena build --deviceType raspberrypi3 --arch armv7hf --emulated',
'$ balena build --docker /var/run/docker.sock --fleet myFleet # Linux, Mac', '$ balena build --docker /var/run/docker.sock --fleet myFleet # Linux, Mac',
'$ balena build --docker //./pipe/docker_engine --fleet myFleet # Windows', '$ balena build --docker //./pipe/docker_engine --fleet myFleet # Windows',
'$ balena build --dockerHost my.docker.host --dockerPort 2376 --ca ca.pem --key key.pem --cert cert.pem -f myFleet', '$ balena build --dockerHost my.docker.host --dockerPort 2376 --ca ca.pem --key key.pem --cert cert.pem -f myFleet',
]; ];
public static args = { public static args = [
source: Args.string({ description: 'path of project source directory' }), {
}; name: 'source',
description: 'path of project source directory',
},
];
public static flags = { public static usage = 'build [source]';
arch: Flags.string({
public static flags: flags.Input<FlagsDef> = {
arch: flags.string({
description: 'the architecture to build for', description: 'the architecture to build for',
char: 'A', char: 'A',
}), }),
deviceType: Flags.string({ deviceType: flags.string({
description: 'the type of device this build is for', description: 'the type of device this build is for',
char: 'd', char: 'd',
}), }),
fleet: cf.fleet, fleet: cf.fleet,
...composeCliFlags, ...composeCliFlags,
...dockerCliFlags, ...dockerCliFlags,
// NOTE: Not supporting -h for help, because of clash with -h in DockerCliFlags
// Revisit this in future release.
help: flags.help({}),
}; };
public static primary = true; public static primary = true;
public async run() { public async run() {
const { args: params, flags: options } = await this.parse(BuildCmd); const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
BuildCmd,
);
const Logger = await import('../../utils/logger'); await Command.checkLoggedInIf(!!options.fleet);
const { checkLoggedInIf } = await import('../../utils/patterns');
await checkLoggedInIf(!!options.fleet);
(await import('events')).defaultMaxListeners = 1000; (await import('events')).defaultMaxListeners = 1000;
const sdk = getBalenaSdk(); const sdk = getBalenaSdk();
const logger = Logger.getLogger(); const logger = await Command.getLogger();
logger.logDebug('Parsing input...'); logger.logDebug('Parsing input...');
const prepareBuildOpts = { // `build` accepts `source` as a parameter, but compose expects it as an option
...options, options.source = params.source;
source: params.source, delete params.source;
};
await this.resolveArchFromDeviceType(sdk, prepareBuildOpts); await this.validateOptions(options, sdk);
await this.validateOptions(prepareBuildOpts, sdk);
// Build args are under consideration for removal - warn user // Build args are under consideration for removal - warn user
if (prepareBuildOpts.buildArg) { if (options.buildArg) {
console.log(buildArgDeprecation); console.log(buildArgDeprecation);
} }
const app = await this.getAppAndResolveArch(prepareBuildOpts); const app = await this.getAppAndResolveArch(options);
const { docker, buildOpts, composeOpts } = const { docker, buildOpts, composeOpts } = await this.prepareBuild(options);
await this.prepareBuild(prepareBuildOpts);
try { try {
await this.buildProject(docker, logger, composeOpts, { await this.buildProject(docker, logger, composeOpts, {
appType: app?.application_type?.[0], app,
arch: prepareBuildOpts.arch!, arch: options.arch!,
deviceType: prepareBuildOpts.deviceType!, deviceType: options.deviceType!,
buildEmulated: prepareBuildOpts.emulated, buildEmulated: options.emulated,
buildOpts, buildOpts,
}); });
} catch (err) { } catch (err) {
@ -151,20 +148,20 @@ ${dockerignoreHelp}
logger.logSuccess('Build succeeded!'); logger.logSuccess('Build succeeded!');
} }
protected async validateOptions(opts: PrepareBuildOpts, sdk: BalenaSDK) { protected async validateOptions(opts: FlagsDef, sdk: BalenaSDK) {
// Validate option combinations // Validate option combinations
if ( if (
(opts.fleet == null && (opts.arch == null || opts.deviceType == null)) || (opts.fleet == null && (opts.arch == null || opts.deviceType == null)) ||
(opts.fleet != null && (opts.arch != null || opts.deviceType != null)) (opts.fleet != null && (opts.arch != null || opts.deviceType != null))
) { ) {
const { ExpectedError } = await import('../../errors'); const { ExpectedError } = await import('../errors');
throw new ExpectedError( throw new ExpectedError(
'You must specify either a fleet (-f), or the device type (-d) and optionally the architecture (-A)', 'You must specify either a fleet (-f), or the device type (-d) and architecture (-A)',
); );
} }
// Validate project directory // Validate project directory
const { validateProjectDirectory } = await import('../../utils/compose_ts'); const { validateProjectDirectory } = await import('../utils/compose_ts');
const { dockerfilePath, registrySecrets } = await validateProjectDirectory( const { dockerfilePath, registrySecrets } = await validateProjectDirectory(
sdk, sdk,
{ {
@ -179,45 +176,9 @@ ${dockerignoreHelp}
opts['registry-secrets'] = registrySecrets; opts['registry-secrets'] = registrySecrets;
} }
protected async resolveArchFromDeviceType( protected async getAppAndResolveArch(opts: FlagsDef) {
sdk: BalenaSDK,
opts: PrepareBuildOpts,
) {
if (opts.deviceType != null && opts.arch == null) {
try {
const deviceTypeOpts = {
$select: 'is_of__cpu_architecture',
$expand: {
is_of__cpu_architecture: {
$select: 'slug',
},
},
} satisfies PineOptions<DeviceType>;
opts.arch = (
(await sdk.models.deviceType.get(
opts.deviceType,
deviceTypeOpts,
)) as PineTypedResult<DeviceType, typeof deviceTypeOpts>
).is_of__cpu_architecture[0].slug;
} catch (err) {
const { ExpectedError } = await import('../../errors');
if (err instanceof sdk.errors.BalenaInvalidDeviceType) {
let message = err.message;
if (!(await sdk.auth.isLoggedIn())) {
message = `${message}. In case you are trying to use a private device type, please try to log in first.`;
}
throw new ExpectedError(message);
}
throw new ExpectedError(
'Failed to resolve the architecture of the provided device type. If you are in an air-gapped environment please also define the architecture (-A) parameter.',
);
}
}
}
protected async getAppAndResolveArch(opts: PrepareBuildOpts) {
if (opts.fleet) { if (opts.fleet) {
const { getAppWithArch } = await import('../../utils/helpers'); const { getAppWithArch } = await import('../utils/helpers');
const app = await getAppWithArch(opts.fleet); const app = await getAppWithArch(opts.fleet);
opts.arch = app.arch; opts.arch = app.arch;
opts.deviceType = app.is_for__device_type[0].slug; opts.deviceType = app.is_for__device_type[0].slug;
@ -225,8 +186,8 @@ ${dockerignoreHelp}
} }
} }
protected async prepareBuild(options: PrepareBuildOpts) { protected async prepareBuild(options: FlagsDef) {
const { getDocker, generateBuildOpts } = await import('../../utils/docker'); const { getDocker, generateBuildOpts } = await import('../utils/docker');
const [docker, buildOpts, composeOpts] = await Promise.all([ const [docker, buildOpts, composeOpts] = await Promise.all([
getDocker(options), getDocker(options),
generateBuildOpts(options), generateBuildOpts(options),
@ -247,24 +208,24 @@ ${dockerignoreHelp}
* buildEmulated * buildEmulated
* buildOpts: arguments to forward to docker build command * buildOpts: arguments to forward to docker build command
* *
* @param {Dockerode} docker * @param {DockerToolbelt} docker
* @param {Logger} logger * @param {Logger} logger
* @param {ComposeOpts} composeOpts * @param {ComposeOpts} composeOpts
* @param opts * @param opts
*/ */
protected async buildProject( protected async buildProject(
docker: import('dockerode'), docker: import('dockerode'),
logger: import('../../utils/logger'), logger: import('../utils/logger'),
composeOpts: ComposeOpts, composeOpts: ComposeOpts,
opts: { opts: {
appType?: Pick<ApplicationType, 'supports_multicontainer'>; app?: Application;
arch: string; arch: string;
deviceType: string; deviceType: string;
buildEmulated: boolean; buildEmulated: boolean;
buildOpts: BuildOpts; buildOpts: BuildOpts;
}, },
) { ) {
const { loadProject } = await import('../../utils/compose_ts'); const { loadProject } = await import('../utils/compose_ts');
const project = await loadProject( const project = await loadProject(
logger, logger,
@ -273,10 +234,11 @@ ${dockerignoreHelp}
opts.buildOpts.t, opts.buildOpts.t,
); );
const appType = (opts.app?.application_type as ApplicationType[])?.[0];
if ( if (
opts.appType != null && appType != null &&
project.descriptors.length > 1 && project.descriptors.length > 1 &&
!opts.appType.supports_multicontainer !appType.supports_multicontainer
) { ) {
logger.logWarn( logger.logWarn(
'Target fleet does not support multiple containers.\n' + 'Target fleet does not support multiple containers.\n' +

View File

@ -15,16 +15,31 @@
* limitations under the License. * limitations under the License.
*/ */
import { Flags, Command } from '@oclif/core'; import { flags } from '@oclif/command';
import type { Interfaces } from '@oclif/core'; import Command from '../../command';
import * as cf from '../../utils/common-flags'; import * as cf from '../../utils/common-flags';
import { getBalenaSdk, getCliForm, stripIndent } from '../../utils/lazy'; import { getBalenaSdk, getCliForm, stripIndent } from '../../utils/lazy';
import { import { applicationIdInfo, devModeInfo } from '../../utils/messages';
applicationIdInfo, import type { PineDeferred } from 'balena-sdk';
devModeInfo,
secureBootInfo, interface FlagsDef {
} from '../../utils/messages'; version: string; // OS version
import type { BalenaSDK, PineDeferred } from 'balena-sdk'; 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;
'provisioning-key-expiry-date'?: string;
help: void;
}
export default class ConfigGenerateCmd extends Command { export default class ConfigGenerateCmd extends Command {
public static description = stripIndent` public static description = stripIndent`
@ -36,8 +51,6 @@ export default class ConfigGenerateCmd extends Command {
${devModeInfo.split('\n').join('\n\t\t')} ${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 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. alongside the --deviceType option to specify the target device type.
@ -53,20 +66,20 @@ 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 --deviceApiKey <existingDeviceKey>',
'$ balena config generate --device 7cf02a6 --version 2.12.7 --output config.json', '$ 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 --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 --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 --output config.json',
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 15', '$ balena config generate --fleet myorg/fleet --version 2.12.7 --network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 15',
]; ];
public static flags = { public static usage = 'config generate';
version: Flags.string({
public static flags: flags.Input<FlagsDef> = {
version: flags.string({
description: 'a balenaOS version', description: 'a balenaOS version',
required: true, required: true,
}), }),
fleet: { ...cf.fleet, exclusive: ['device'] }, fleet: { ...cf.fleet, exclusive: ['device'] },
dev: cf.dev, dev: cf.dev,
secureBoot: cf.secureBoot,
device: { device: {
...cf.device, ...cf.device,
exclusive: [ exclusive: [
@ -75,71 +88,64 @@ export default class ConfigGenerateCmd extends Command {
'provisioning-key-expiry-date', 'provisioning-key-expiry-date',
], ],
}, },
deviceApiKey: Flags.string({ deviceApiKey: flags.string({
description: description:
'custom device key - note that this is only supported on balenaOS 2.0.3+', 'custom device key - note that this is only supported on balenaOS 2.0.3+',
char: 'k', char: 'k',
}), }),
deviceType: Flags.string({ deviceType: flags.string({
description: description:
"device type slug (run 'balena device-type list' for possible values)", "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', description: 'generate a fresh device key for the device',
}), }),
output: Flags.string({ output: flags.string({
description: 'path of output file', description: 'path of output file',
char: 'o', char: 'o',
}), }),
// Options for non-interactive configuration // Options for non-interactive configuration
network: Flags.string({ network: flags.string({
description: 'the network type to use: ethernet or wifi', description: 'the network type to use: ethernet or wifi',
options: ['ethernet', 'wifi'], options: ['ethernet', 'wifi'],
}), }),
wifiSsid: Flags.string({ wifiSsid: flags.string({
description: description:
'the wifi ssid to use (used only if --network is set to wifi)', 'the wifi ssid to use (used only if --network is set to wifi)',
}), }),
wifiKey: Flags.string({ wifiKey: flags.string({
description: description:
'the wifi key to use (used only if --network is set to wifi)', 'the wifi key to use (used only if --network is set to wifi)',
}), }),
appUpdatePollInterval: Flags.string({ appUpdatePollInterval: flags.string({
description: description:
'supervisor cloud polling interval in minutes (e.g. for device variables)', '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', description: 'custom key name assigned to generated provisioning api key',
exclusive: ['device'], exclusive: ['device'],
}), }),
'provisioning-key-expiry-date': Flags.string({ 'provisioning-key-expiry-date': flags.string({
description: description:
'expiry date assigned to generated provisioning api key (format: YYYY-MM-DD)', 'expiry date assigned to generated provisioning api key (format: YYYY-MM-DD)',
exclusive: ['device'], exclusive: ['device'],
}), }),
help: cf.help,
}; };
public static authenticated = true; 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() { 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(); const balena = getBalenaSdk();
await this.validateOptions(options); await this.validateOptions(options);
let resourceDeviceType: string; let resourceDeviceType: string;
let application: Awaited<ReturnType<typeof this.getApplication>> | null = let application: ApplicationWithDeviceType | null = null;
null;
let device: let device:
| (DeviceWithDeviceType & { belongs_to__application: PineDeferred }) | (DeviceWithDeviceType & { belongs_to__application: PineDeferred })
| null = null; | null = null;
@ -159,40 +165,36 @@ export default class ConfigGenerateCmd extends Command {
resourceDeviceType = device.is_of__device_type[0].slug; resourceDeviceType = device.is_of__device_type[0].slug;
} else { } else {
// Disambiguate application (if is a number, it could either be an ID or a numerical name) // 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; resourceDeviceType = application.is_for__device_type[0].slug;
} }
const deviceType = options.deviceType || resourceDeviceType; const deviceType = options.deviceType || resourceDeviceType;
const deviceManifest = await balena.models.device.getManifestBySlug(
deviceType,
);
// Check compatibility if application and deviceType provided // Check compatibility if application and deviceType provided
if (options.fleet && options.deviceType) { if (options.fleet && options.deviceType) {
const appDeviceManifest = await balena.models.device.getManifestBySlug(
resourceDeviceType,
);
const helpers = await import('../../utils/helpers'); const helpers = await import('../../utils/helpers');
if ( if (
!(await helpers.areDeviceTypesCompatible( !helpers.areDeviceTypesCompatible(appDeviceManifest, deviceManifest)
resourceDeviceType,
deviceType,
))
) { ) {
const { ExpectedError } = await import('../../errors'); throw new balena.errors.BalenaInvalidDeviceType(
throw new ExpectedError(
`Device type ${options.deviceType} is incompatible with fleet ${options.fleet}`, `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 // Prompt for values
// Pass params as an override: if there is any param with exactly the same name as a // 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) // required option, that value is used (and the corresponding question is not asked)
@ -201,7 +203,6 @@ export default class ConfigGenerateCmd extends Command {
}); });
answers.version = options.version; answers.version = options.version;
answers.developmentMode = options.dev; answers.developmentMode = options.dev;
answers.secureBoot = options.secureBoot;
answers.provisioningKeyName = options['provisioning-key-name']; answers.provisioningKeyName = options['provisioning-key-name'];
answers.provisioningKeyExpiryDate = options['provisioning-key-expiry-date']; answers.provisioningKeyExpiryDate = options['provisioning-key-expiry-date'];
@ -243,9 +244,7 @@ export default class ConfigGenerateCmd extends Command {
protected readonly deviceTypeNotAllowedMessage = protected readonly deviceTypeNotAllowedMessage =
'The --deviceType option can only be used alongside the --fleet option'; 'The --deviceType option can only be used alongside the --fleet option';
protected async validateOptions( protected async validateOptions(options: FlagsDef) {
options: Interfaces.InferredFlags<typeof ConfigGenerateCmd.flags>,
) {
const { ExpectedError } = await import('../../errors'); const { ExpectedError } = await import('../../errors');
if (options.device == null && options.fleet == null) { if (options.device == null && options.fleet == null) {
@ -255,8 +254,6 @@ export default class ConfigGenerateCmd extends Command {
if (!options.fleet && options.deviceType) { if (!options.fleet && options.deviceType) {
throw new ExpectedError(this.deviceTypeNotAllowedMessage); throw new ExpectedError(this.deviceTypeNotAllowedMessage);
} }
const { normalizeOsVersion } = await import('../../utils/normalization');
options.version = normalizeOsVersion(options.version);
const { validateDevOptionAndWarn } = await import('../../utils/config'); const { validateDevOptionAndWarn } = await import('../../utils/config');
await validateDevOptionAndWarn(options.dev, options.version); await validateDevOptionAndWarn(options.dev, options.version);
} }

View File

@ -15,10 +15,21 @@
* limitations under the License. * limitations under the License.
*/ */
import { Args, Command } from '@oclif/core'; import { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags'; import * as cf from '../../utils/common-flags';
import { getVisuals, stripIndent } from '../../utils/lazy'; import { getVisuals, stripIndent } from '../../utils/lazy';
interface FlagsDef {
type?: string;
drive?: string;
help: void;
}
interface ArgsDef {
file: string;
}
export default class ConfigInjectCmd extends Command { export default class ConfigInjectCmd extends Command {
public static description = stripIndent` public static description = stripIndent`
Inject a config.json file to a balenaOS image or attached media. Inject a config.json file to a balenaOS image or attached media.
@ -35,22 +46,28 @@ export default class ConfigInjectCmd extends Command {
'$ balena config inject my/config.json --drive /dev/disk2', '$ balena config inject my/config.json --drive /dev/disk2',
]; ];
public static args = { public static args = [
file: Args.string({ {
name: 'file',
description: 'the path to the config.json file to inject', description: 'the path to the config.json file to inject',
required: true, required: true,
}), },
}; ];
public static flags = { public static usage = 'config inject <file>';
public static flags: flags.Input<FlagsDef> = {
drive: cf.driveOrImg, drive: cf.driveOrImg,
help: cf.help,
}; };
public static root = true; public static root = true;
public static offlineCompatible = true; public static offlineCompatible = true;
public async run() { 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'); const { safeUmount } = await import('../../utils/umount');
@ -64,12 +81,7 @@ export default class ConfigInjectCmd extends Command {
); );
const config = await import('balena-config-json'); const config = await import('balena-config-json');
await config.write( await config.write(drive, '', configJSON);
drive,
// Will be removed in the next major of balena-config-json
undefined,
configJSON,
);
console.info('Done'); console.info('Done');
} }

View File

@ -15,10 +15,18 @@
* limitations under the License. * limitations under the License.
*/ */
import { Command } from '@oclif/core'; import { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags'; import * as cf from '../../utils/common-flags';
import { getVisuals, stripIndent } from '../../utils/lazy'; import { getVisuals, stripIndent } from '../../utils/lazy';
interface FlagsDef {
type?: string;
drive?: string;
help: void;
json: boolean;
}
export default class ConfigReadCmd extends Command { export default class ConfigReadCmd extends Command {
public static description = stripIndent` public static description = stripIndent`
Read the config.json file of a balenaOS image or attached media. Read the config.json file of a balenaOS image or attached media.
@ -36,8 +44,11 @@ export default class ConfigReadCmd extends Command {
'$ balena config read --drive balena.img', '$ balena config read --drive balena.img',
]; ];
public static flags = { public static usage = 'config read';
public static flags: flags.Input<FlagsDef> = {
drive: cf.driveOrImg, drive: cf.driveOrImg,
help: cf.help,
json: cf.json, json: cf.json,
}; };
@ -45,7 +56,7 @@ export default class ConfigReadCmd extends Command {
public static offlineCompatible = true; public static offlineCompatible = true;
public async run() { public async run() {
const { flags: options } = await this.parse(ConfigReadCmd); const { flags: options } = this.parse<FlagsDef, {}>(ConfigReadCmd);
const { safeUmount } = await import('../../utils/umount'); const { safeUmount } = await import('../../utils/umount');
@ -54,7 +65,7 @@ export default class ConfigReadCmd extends Command {
await safeUmount(drive); await safeUmount(drive);
const config = await import('balena-config-json'); const config = await import('balena-config-json');
const configJSON = await config.read(drive); const configJSON = await config.read(drive, '');
if (options.json) { if (options.json) {
console.log(JSON.stringify(configJSON, null, 4)); console.log(JSON.stringify(configJSON, null, 4));

View File

@ -15,10 +15,19 @@
* limitations under the License. * limitations under the License.
*/ */
import { Flags, Command } from '@oclif/core'; import { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags'; import * as cf from '../../utils/common-flags';
import { getVisuals, stripIndent } from '../../utils/lazy'; import { getVisuals, stripIndent } from '../../utils/lazy';
interface FlagsDef {
type?: string;
drive?: string;
advanced: boolean;
help: void;
version?: string;
}
export default class ConfigReconfigureCmd extends Command { export default class ConfigReconfigureCmd extends Command {
public static description = stripIndent` public static description = stripIndent`
Interactively reconfigure a balenaOS image file or attached media. Interactively reconfigure a balenaOS image file or attached media.
@ -38,13 +47,16 @@ export default class ConfigReconfigureCmd extends Command {
'$ balena config reconfigure --drive balena.img --advanced', '$ balena config reconfigure --drive balena.img --advanced',
]; ];
public static flags = { public static usage = 'config reconfigure';
public static flags: flags.Input<FlagsDef> = {
drive: cf.driveOrImg, drive: cf.driveOrImg,
advanced: Flags.boolean({ advanced: flags.boolean({
description: 'show advanced commands', description: 'show advanced commands',
char: 'v', char: 'v',
}), }),
version: Flags.string({ help: cf.help,
version: flags.string({
description: 'balenaOS version, for example "2.32.0" or "2.44.0+rev1"', description: 'balenaOS version, for example "2.32.0" or "2.44.0+rev1"',
}), }),
}; };
@ -53,7 +65,7 @@ export default class ConfigReconfigureCmd extends Command {
public static root = true; public static root = true;
public async run() { public async run() {
const { flags: options } = await this.parse(ConfigReconfigureCmd); const { flags: options } = this.parse<FlagsDef, {}>(ConfigReconfigureCmd);
const { safeUmount } = await import('../../utils/umount'); const { safeUmount } = await import('../../utils/umount');
@ -62,7 +74,7 @@ export default class ConfigReconfigureCmd extends Command {
await safeUmount(drive); await safeUmount(drive);
const config = await import('balena-config-json'); const config = await import('balena-config-json');
const { uuid } = await config.read(drive); const { uuid } = await config.read(drive, '');
await safeUmount(drive); await safeUmount(drive);
if (!uuid) { if (!uuid) {

View File

@ -15,10 +15,22 @@
* limitations under the License. * limitations under the License.
*/ */
import { Args, Command } from '@oclif/core'; import { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags'; import * as cf from '../../utils/common-flags';
import { getVisuals, stripIndent } from '../../utils/lazy'; 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 { export default class ConfigWriteCmd extends Command {
public static description = stripIndent` public static description = stripIndent`
Write a key-value pair to the config.json file of an OS image or attached media. Write a key-value pair to the config.json file of an OS image or attached media.
@ -36,26 +48,33 @@ export default class ConfigWriteCmd extends Command {
'$ balena config write --drive balena.img os.network.connectivity.interval 300', '$ balena config write --drive balena.img os.network.connectivity.interval 300',
]; ];
public static args = { public static args = [
key: Args.string({ {
name: 'key',
description: 'the key of the config parameter to write', description: 'the key of the config parameter to write',
required: true, required: true,
}), },
value: Args.string({ {
name: 'value',
description: 'the value of the config parameter to write', description: 'the value of the config parameter to write',
required: true, required: true,
}), },
}; ];
public static flags = { public static usage = 'config write <key> <value>';
public static flags: flags.Input<FlagsDef> = {
drive: cf.driveOrImg, drive: cf.driveOrImg,
help: cf.help,
}; };
public static root = true; public static root = true;
public static offlineCompatible = true; public static offlineCompatible = true;
public async run() { 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'); const { denyMount, safeUmount } = await import('../../utils/umount');
@ -64,19 +83,14 @@ export default class ConfigWriteCmd extends Command {
await safeUmount(drive); await safeUmount(drive);
const config = await import('balena-config-json'); const config = await import('balena-config-json');
const configJSON = await config.read(drive); const configJSON = await config.read(drive, '');
console.info(`Setting ${params.key} to ${params.value}`); console.info(`Setting ${params.key} to ${params.value}`);
ConfigWriteCmd.updateConfigJson(configJSON, params.key, params.value); ConfigWriteCmd.updateConfigJson(configJSON, params.key, params.value);
await denyMount(drive, async () => { await denyMount(drive, async () => {
await safeUmount(drive); await safeUmount(drive);
await config.write( await config.write(drive, '', configJSON);
drive,
// Will be removed in the next major of balena-config-json
undefined,
configJSON,
);
}); });
console.info('Done'); console.info('Done');

View File

@ -15,44 +15,45 @@
* limitations under the License. * limitations under the License.
*/ */
import { Args, Flags, Command } from '@oclif/core'; import { flags } from '@oclif/command';
import type { ImageDescriptor } from '@balena/compose/dist/parse'; import type { ImageDescriptor } from '@balena/compose/dist/parse';
import { ExpectedError } from '../../errors';
import { getBalenaSdk, getChalk, stripIndent } from '../../utils/lazy'; import Command from '../command';
import { ExpectedError } from '../errors';
import { getBalenaSdk, getChalk, stripIndent } from '../utils/lazy';
import { import {
dockerignoreHelp, dockerignoreHelp,
registrySecretsHelp, registrySecretsHelp,
buildArgDeprecation, buildArgDeprecation,
} from '../../utils/messages'; } from '../utils/messages';
import * as ca from '../../utils/common-args'; import * as ca from '../utils/common-args';
import * as compose from '../../utils/compose'; import * as compose from '../utils/compose';
import type { import type {
BuiltImage, BuiltImage,
ComposeCliFlags, ComposeCliFlags,
ComposeOpts, ComposeOpts,
Release as ComposeReleaseInfo, Release as ComposeReleaseInfo,
} from '../../utils/compose-types'; } from '../utils/compose-types';
import type { BuildOpts, DockerCliFlags } from '../../utils/docker'; import type { BuildOpts, DockerCliFlags } from '../utils/docker';
import { import {
applyReleaseTagKeysAndValues, applyReleaseTagKeysAndValues,
buildProject, buildProject,
composeCliFlags, composeCliFlags,
isBuildConfig, isBuildConfig,
parseReleaseTagKeysAndValues, parseReleaseTagKeysAndValues,
} from '../../utils/compose_ts'; } from '../utils/compose_ts';
import { dockerCliFlags } from '../../utils/docker'; import { dockerCliFlags } from '../utils/docker';
import type { ApplicationType, DeviceType, Release } from 'balena-sdk'; import type {
Application,
ApplicationType,
DeviceType,
Release,
} from 'balena-sdk';
interface ApplicationWithArch { interface ApplicationWithArch extends Application {
id: number;
arch: string; 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 { interface FlagsDef extends ComposeCliFlags, DockerCliFlags {
source?: string; source?: string;
build: boolean; build: boolean;
@ -60,6 +61,12 @@ interface FlagsDef extends ComposeCliFlags, DockerCliFlags {
'release-tag'?: string[]; 'release-tag'?: string[];
draft: boolean; draft: boolean;
note?: string; note?: string;
help: void;
}
interface ArgsDef {
fleet: string;
image?: string;
} }
export default class DeployCmd extends Command { export default class DeployCmd extends Command {
@ -100,26 +107,31 @@ ${dockerignoreHelp}
'$ balena deploy myFleet myRepo/myImage --release-tag key1 "" key2 "value2 with spaces"', '$ balena deploy myFleet myRepo/myImage --release-tag key1 "" key2 "value2 with spaces"',
]; ];
public static args = { public static args = [
fleet: ca.fleetRequired, ca.fleetRequired,
image: Args.string({ description: 'the image to deploy' }), {
}; name: 'image',
description: 'the image to deploy',
},
];
public static flags = { public static usage = 'deploy <fleet> [image]';
source: Flags.string({
public static flags: flags.Input<FlagsDef> = {
source: flags.string({
description: description:
'specify an alternate source directory; default is the working directory', 'specify an alternate source directory; default is the working directory',
char: 's', char: 's',
}), }),
build: Flags.boolean({ build: flags.boolean({
description: 'force a rebuild before deploy', description: 'force a rebuild before deploy',
char: 'b', char: 'b',
}), }),
nologupload: Flags.boolean({ nologupload: flags.boolean({
description: description:
"don't upload build logs to the dashboard with image (if building)", "don't upload build logs to the dashboard with image (if building)",
}), }),
'release-tag': Flags.string({ 'release-tag': flags.string({
description: stripIndent` description: stripIndent`
Set release tags if the image deployment is successful. Multiple Set release tags if the image deployment is successful. Multiple
arguments may be provided, alternating tag keys and values (see examples). arguments may be provided, alternating tag keys and values (see examples).
@ -127,7 +139,7 @@ ${dockerignoreHelp}
`, `,
multiple: true, multiple: true,
}), }),
draft: Flags.boolean({ draft: flags.boolean({
description: stripIndent` description: stripIndent`
Deploy the release as a draft. Draft releases are ignored Deploy the release as a draft. Draft releases are ignored
by the 'track latest' release policy but can be used through release pinning. by the 'track latest' release policy but can be used through release pinning.
@ -135,9 +147,12 @@ ${dockerignoreHelp}
as final by default unless this option is given.`, as final by default unless this option is given.`,
default: false, default: false,
}), }),
note: Flags.string({ description: 'The notes for this release' }), note: flags.string({ description: 'The notes for this release' }),
...composeCliFlags, ...composeCliFlags,
...dockerCliFlags, ...dockerCliFlags,
// NOTE: Not supporting -h for help, because of clash with -h in DockerCliFlags
// Revisit this in future release.
help: flags.help({}),
}; };
public static authenticated = true; public static authenticated = true;
@ -145,13 +160,13 @@ ${dockerignoreHelp}
public static primary = true; public static primary = true;
public async run() { 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; (await import('events')).defaultMaxListeners = 1000;
const Logger = await import('../../utils/logger'); const logger = await Command.getLogger();
const logger = Logger.getLogger();
logger.logDebug('Parsing input...'); logger.logDebug('Parsing input...');
const { fleet, image } = params; const { fleet, image } = params;
@ -169,7 +184,7 @@ ${dockerignoreHelp}
const sdk = getBalenaSdk(); const sdk = getBalenaSdk();
const { getRegistrySecrets, validateProjectDirectory } = await import( const { getRegistrySecrets, validateProjectDirectory } = await import(
'../../utils/compose_ts' '../utils/compose_ts'
); );
const { releaseTagKeys, releaseTagValues } = parseReleaseTagKeysAndValues( const { releaseTagKeys, releaseTagValues } = parseReleaseTagKeysAndValues(
@ -177,7 +192,7 @@ ${dockerignoreHelp}
); );
if (image) { if (image) {
(options as FlagsDef)['registry-secrets'] = await getRegistrySecrets( options['registry-secrets'] = await getRegistrySecrets(
sdk, sdk,
options['registry-secrets'], options['registry-secrets'],
); );
@ -190,16 +205,16 @@ ${dockerignoreHelp}
registrySecretsPath: options['registry-secrets'], registrySecretsPath: options['registry-secrets'],
}); });
options.dockerfile = dockerfilePath; options.dockerfile = dockerfilePath;
(options as FlagsDef)['registry-secrets'] = registrySecrets; options['registry-secrets'] = registrySecrets;
} }
const helpers = await import('../../utils/helpers'); const helpers = await import('../utils/helpers');
const app = await helpers.getAppWithArch(fleet); const app = await helpers.getAppWithArch(fleet);
const dockerUtils = await import('../../utils/docker'); const dockerUtils = await import('../utils/docker');
const [docker, buildOpts, composeOpts] = await Promise.all([ const [docker, buildOpts, composeOpts] = await Promise.all([
dockerUtils.getDocker(options), dockerUtils.getDocker(options),
dockerUtils.generateBuildOpts(options as FlagsDef), dockerUtils.generateBuildOpts(options),
compose.generateOpts(options), compose.generateOpts(options),
]); ]);
@ -220,13 +235,13 @@ ${dockerignoreHelp}
releaseTagValues, releaseTagValues,
); );
if (options.note) { if (options.note) {
await sdk.models.release.setNote(release.id, options.note); await sdk.models.release.note(release.id, options.note);
} }
} }
async deployProject( async deployProject(
docker: import('dockerode'), docker: import('dockerode'),
logger: import('../../utils/logger'), logger: import('../utils/logger'),
composeOpts: ComposeOpts, composeOpts: ComposeOpts,
opts: { opts: {
app: ApplicationWithArch; // the application instance to deploy to app: ApplicationWithArch; // the application instance to deploy to
@ -244,10 +259,10 @@ ${dockerignoreHelp}
const doodles = await import('resin-doodles'); const doodles = await import('resin-doodles');
const sdk = getBalenaSdk(); const sdk = getBalenaSdk();
const { deployProject: $deployProject, loadProject } = await import( const { deployProject: $deployProject, loadProject } = await import(
'../../utils/compose_ts' '../utils/compose_ts'
); );
const appType = opts.app.application_type[0]; const appType = (opts.app?.application_type as ApplicationType[])?.[0];
try { try {
const project = await loadProject( const project = await loadProject(
@ -304,7 +319,7 @@ ${dockerignoreHelp}
projectName: project.name, projectName: project.name,
composition: compositionToBuild, composition: compositionToBuild,
arch: opts.app.arch, 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, emulated: opts.buildEmulated,
buildOpts: opts.buildOpts, buildOpts: opts.buildOpts,
inlineLogs: composeOpts.inlineLogs, inlineLogs: composeOpts.inlineLogs,
@ -325,17 +340,17 @@ ${dockerignoreHelp}
); );
let release: Release | ComposeReleaseInfo['release']; 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 { deployLegacy } = require('../utils/deploy-legacy');
const msg = getChalk().yellow( const msg = getChalk().yellow(
'Target fleet requires legacy deploy method.', 'Target fleet requires legacy deploy method.',
); );
logger.logWarn(msg); logger.logWarn(msg);
const [token, { username }, url, options] = await Promise.all([ const [token, username, url, options] = await Promise.all([
sdk.auth.getToken(), sdk.auth.getToken(),
sdk.auth.getUserInfo(), sdk.auth.whoami(),
sdk.settings.get('balenaUrl'), sdk.settings.get('balenaUrl'),
{ {
// opts.appName may be prefixed by 'owner/', unlike opts.app.app_name // opts.appName may be prefixed by 'owner/', unlike opts.app.app_name
@ -358,17 +373,23 @@ ${dockerignoreHelp}
$select: ['commit'], $select: ['commit'],
}); });
} else { } else {
const [userId, auth, apiEndpoint] = await Promise.all([
sdk.auth.getUserId(),
sdk.auth.getToken(),
sdk.settings.get('apiUrl'),
]);
release = await $deployProject( release = await $deployProject(
docker, docker,
sdk,
logger, logger,
project.composition, project.composition,
images, images,
opts.app.id, opts.app.id,
userId,
`Bearer ${auth}`,
apiEndpoint,
!opts.shouldUploadLogs, !opts.shouldUploadLogs,
composeOpts.projectPath, composeOpts.projectPath,
opts.createAsDraft, opts.createAsDraft,
project.descriptors,
); );
} }

View File

@ -15,10 +15,21 @@
* limitations under the License. * limitations under the License.
*/ */
import { Args, Command } 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 cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy'; import { getBalenaSdk, stripIndent } from '../../utils/lazy';
interface FlagsDef {
yes: boolean;
help: void;
}
interface ArgsDef {
uuid: string;
}
export default class DeviceDeactivateCmd extends Command { export default class DeviceDeactivateCmd extends Command {
public static description = stripIndent` public static description = stripIndent`
Deactivate a device. Deactivate a device.
@ -33,22 +44,27 @@ export default class DeviceDeactivateCmd extends Command {
'$ balena device deactivate 7cf02a6 --yes', '$ balena device deactivate 7cf02a6 --yes',
]; ];
public static args = { public static args: Array<IArg<any>> = [
uuid: Args.string({ {
name: 'uuid',
description: 'the UUID of the device to be deactivated', description: 'the UUID of the device to be deactivated',
required: true, required: true,
}), },
}; ];
public static flags = { public static usage = 'device deactivate <uuid>';
public static flags: flags.Input<FlagsDef> = {
yes: cf.yes, yes: cf.yes,
help: cf.help,
}; };
public static authenticated = true; public static authenticated = true;
public async run() { public async run() {
const { args: params, flags: options } = const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
await this.parse(DeviceDeactivateCmd); DeviceDeactivateCmd,
);
const balena = getBalenaSdk(); const balena = getBalenaSdk();
const patterns = await import('../../utils/patterns'); const patterns = await import('../../utils/patterns');

View File

@ -15,10 +15,21 @@
* limitations under the License. * limitations under the License.
*/ */
import { Args, Command } 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 { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { ExpectedError } from '../../errors'; import { ExpectedError } from '../../errors';
interface FlagsDef {
help: void;
}
interface ArgsDef {
uuid: string;
}
export default class DeviceIdentifyCmd extends Command { export default class DeviceIdentifyCmd extends Command {
public static description = stripIndent` public static description = stripIndent`
Identify a device. Identify a device.
@ -27,17 +38,24 @@ export default class DeviceIdentifyCmd extends Command {
`; `;
public static examples = ['$ balena device identify 23c73a1']; public static examples = ['$ balena device identify 23c73a1'];
public static args = { public static args: Array<IArg<any>> = [
uuid: Args.string({ {
name: 'uuid',
description: 'the uuid of the device to identify', description: 'the uuid of the device to identify',
required: true, required: true,
}), },
];
public static usage = 'device identify <uuid>';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
}; };
public static authenticated = true; public static authenticated = true;
public async run() { public async run() {
const { args: params } = await this.parse(DeviceIdentifyCmd); const { args: params } = this.parse<FlagsDef, ArgsDef>(DeviceIdentifyCmd);
const balena = getBalenaSdk(); const balena = getBalenaSdk();

View File

@ -15,11 +15,12 @@
* limitations under the License. * limitations under the License.
*/ */
import { Flags, Args, Command } 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 * as cf from '../../utils/common-flags';
import { expandForAppName } from '../../utils/helpers'; import { expandForAppName } from '../../utils/helpers';
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy'; import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
import { jsonInfo } from '../../utils/messages';
import type { Application, Release } from 'balena-sdk'; import type { Application, Release } from 'balena-sdk';
@ -40,30 +41,39 @@ interface ExtendedDevice extends DeviceWithDeviceType {
undervoltage_detected?: boolean; undervoltage_detected?: boolean;
} }
interface FlagsDef {
help: void;
view: boolean;
}
interface ArgsDef {
uuid: string;
}
export default class DeviceCmd extends Command { export default class DeviceCmd extends Command {
public static description = stripIndent` public static description = stripIndent`
Show info about a single device. Show info about a single device.
Show information about a single device. Show information about a single device.
${jsonInfo.split('\n').join('\n\t\t')}
`; `;
public static examples = [ public static examples = [
'$ balena device 7cf02a6', '$ balena device 7cf02a6',
'$ balena device 7cf02a6 --view', '$ balena device 7cf02a6 --view',
'$ balena device 7cf02a6 --json',
]; ];
public static args = { public static args: Array<IArg<any>> = [
uuid: Args.string({ {
name: 'uuid',
description: 'the device uuid', description: 'the device uuid',
required: true, required: true,
}), },
}; ];
public static flags = { public static usage = 'device <uuid>';
json: cf.json,
view: Flags.boolean({ public static flags: flags.Input<FlagsDef> = {
help: cf.help,
view: flags.boolean({
default: false, default: false,
description: 'open device dashboard page', description: 'open device dashboard page',
}), }),
@ -73,63 +83,39 @@ export default class DeviceCmd extends Command {
public static primary = true; public static primary = true;
public async run() { 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(); const balena = getBalenaSdk();
let device: ExtendedDevice; const device = (await balena.models.device.get(params.uuid, {
if (options.json) { $select: [
const [deviceBase, deviceComputed] = await Promise.all([ 'device_name',
balena.models.device.get(params.uuid, { 'id',
$expand: { 'overall_status',
device_tag: { 'is_online',
$select: ['tag_key', 'value'], 'ip_address',
}, 'mac_address',
...expandForAppName.$expand, 'last_connectivity_event',
}, 'uuid',
}), 'supervisor_version',
balena.models.device.get(params.uuid, { 'is_web_accessible',
$select: [ 'note',
'overall_status', 'os_version',
'overall_progress', 'memory_usage',
'should_be_running__release', 'memory_total',
], 'public_address',
}), 'storage_block_device',
]); 'storage_usage',
'storage_total',
device = { 'cpu_usage',
...deviceBase, 'cpu_temp',
...deviceComputed, 'cpu_id',
} as ExtendedDevice; 'is_undervolted',
} else { ],
device = (await balena.models.device.get(params.uuid, { ...expandForAppName,
$select: [ })) as ExtendedDevice;
'device_name',
'id',
'overall_status',
'is_online',
'ip_address',
'mac_address',
'last_connectivity_event',
'uuid',
'supervisor_version',
'is_web_accessible',
'note',
'os_version',
'memory_usage',
'memory_total',
'public_address',
'storage_block_device',
'storage_usage',
'storage_total',
'cpu_usage',
'cpu_temp',
'cpu_id',
'is_undervolted',
],
...expandForAppName,
})) as ExtendedDevice;
}
if (options.view) { if (options.view) {
const open = await import('open'); const open = await import('open');
@ -193,11 +179,6 @@ export default class DeviceCmd extends Command {
); );
} }
if (options.json) {
console.log(JSON.stringify(device, null, 4));
return;
}
console.log( console.log(
getVisuals().table.vertical(device, [ getVisuals().table.vertical(device, [
`$${device.device_name}$`, `$${device.device_name}$`,

View File

@ -15,7 +15,8 @@
* limitations under the License. * limitations under the License.
*/ */
import { Flags, Command } from '@oclif/core'; import { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags'; import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy'; import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { applicationIdInfo } from '../../utils/messages'; import { applicationIdInfo } from '../../utils/messages';
@ -28,6 +29,7 @@ interface FlagsDef {
'os-version'?: string; 'os-version'?: string;
drive?: string; drive?: string;
config?: string; config?: string;
help: void;
'provisioning-key-name'?: string; 'provisioning-key-name'?: string;
'provisioning-key-expiry-date'?: string; 'provisioning-key-expiry-date'?: string;
} }
@ -50,7 +52,7 @@ export default class DeviceInitCmd extends Command {
Possible arguments for the '--fleet', '--os-version' and '--drive' options can Possible arguments for the '--fleet', '--os-version' and '--drive' options can
be listed respectively with the commands: be listed respectively with the commands:
'balena fleet list' 'balena fleets'
'balena os versions' 'balena os versions'
'balena util available-drives' 'balena util available-drives'
@ -72,14 +74,16 @@ export default class DeviceInitCmd extends Command {
'$ balena device init --fleet myFleet --os-version 2.83.21+rev1.prod --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 flags = { public static usage = 'device init';
public static flags: flags.Input<FlagsDef> = {
fleet: cf.fleet, fleet: cf.fleet,
yes: cf.yes, yes: cf.yes,
advanced: Flags.boolean({ advanced: flags.boolean({
char: 'v', char: 'v',
description: 'show advanced configuration options', description: 'show advanced configuration options',
}), }),
'os-version': Flags.string({ 'os-version': flags.string({
description: stripIndent` description: stripIndent`
exact version number, or a valid semver range, exact version number, or a valid semver range,
or 'latest' (includes pre-releases), or 'latest' (includes pre-releases),
@ -89,22 +93,23 @@ export default class DeviceInitCmd extends Command {
`, `,
}), }),
drive: cf.drive, drive: cf.drive,
config: Flags.string({ config: flags.string({
description: 'path to the config JSON file, see `balena os build-config`', 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', description: 'custom key name assigned to generated provisioning api key',
}), }),
'provisioning-key-expiry-date': Flags.string({ 'provisioning-key-expiry-date': flags.string({
description: description:
'expiry date assigned to generated provisioning api key (format: YYYY-MM-DD)', 'expiry date assigned to generated provisioning api key (format: YYYY-MM-DD)',
}), }),
help: cf.help,
}; };
public static authenticated = true; public static authenticated = true;
public async run() { public async run() {
const { flags: options } = await this.parse(DeviceInitCmd); const { flags: options } = this.parse<FlagsDef, {}>(DeviceInitCmd);
// Imports // Imports
const { promisify } = await import('util'); const { promisify } = await import('util');
@ -114,22 +119,25 @@ export default class DeviceInitCmd extends Command {
tmp.setGracefulCleanup(); tmp.setGracefulCleanup();
const { downloadOSImage } = await import('../../utils/cloud'); const { downloadOSImage } = await import('../../utils/cloud');
const { getApplication } = await import('../../utils/sdk'); const { getApplication } = await import('../../utils/sdk');
const Logger = await import('../../utils/logger');
const logger = Logger.getLogger(); const logger = await Command.getLogger();
const balena = getBalenaSdk(); const balena = getBalenaSdk();
// Get application and // Get application and
const application = options.fleet const application = (await getApplication(
? await getApplication(balena, options.fleet, { balena,
$select: ['id', 'slug'], options.fleet ||
$expand: { (
is_for__device_type: { await (await import('../../utils/patterns')).selectApplication()
$select: 'slug', ).slug,
}, {
$expand: {
is_for__device_type: {
$select: 'slug',
}, },
}) },
: await (await import('../../utils/patterns')).selectApplication(); },
)) as ApplicationWithDeviceType;
// Register new device // Register new device
const deviceUuid = balena.models.device.generateUniqueKey(); const deviceUuid = balena.models.device.generateUniqueKey();
@ -155,7 +163,7 @@ export default class DeviceInitCmd extends Command {
try { try {
logger.logDebug(`Process failed, removing device ${device.uuid}`); logger.logDebug(`Process failed, removing device ${device.uuid}`);
await balena.models.device.remove(device.uuid); await balena.models.device.remove(device.uuid);
} catch { } catch (e) {
// Ignore removal failures, and throw original error // Ignore removal failures, and throw original error
} }
throw e; throw e;

View File

@ -15,9 +15,23 @@
* limitations under the License. * limitations under the License.
*/ */
import { Flags, Args, Command } 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 { getBalenaSdk, stripIndent } from '../../utils/lazy';
interface FlagsDef {
enable: boolean;
disable: boolean;
status: boolean;
help?: void;
}
interface ArgsDef {
uuid: string | number;
}
export default class DeviceLocalModeCmd extends Command { export default class DeviceLocalModeCmd extends Command {
public static description = stripIndent` public static description = stripIndent`
Get or manage the local mode status for a device. Get or manage the local mode status for a device.
@ -33,33 +47,38 @@ export default class DeviceLocalModeCmd extends Command {
'$ balena device local-mode 23c73a1 --status', '$ balena device local-mode 23c73a1 --status',
]; ];
public static args = { public static args: Array<IArg<any>> = [
uuid: Args.string({ {
name: 'uuid',
description: 'the uuid of the device to manage', description: 'the uuid of the device to manage',
required: true, required: true,
}), },
}; ];
public static flags = { public static usage = 'device local-mode <uuid>';
enable: Flags.boolean({
public static flags: flags.Input<FlagsDef> = {
enable: flags.boolean({
description: 'enable local mode', description: 'enable local mode',
exclusive: ['disable', 'status'], exclusive: ['disable', 'status'],
}), }),
disable: Flags.boolean({ disable: flags.boolean({
description: 'disable local mode', description: 'disable local mode',
exclusive: ['enable', 'status'], exclusive: ['enable', 'status'],
}), }),
status: Flags.boolean({ status: flags.boolean({
description: 'output boolean indicating local mode status', description: 'output boolean indicating local mode status',
exclusive: ['enable', 'disable'], exclusive: ['enable', 'disable'],
}), }),
help: cf.help,
}; };
public static authenticated = true; public static authenticated = true;
public async run() { public async run() {
const { args: params, flags: options } = const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
await this.parse(DeviceLocalModeCmd); DeviceLocalModeCmd,
);
const balena = getBalenaSdk(); const balena = getBalenaSdk();

View File

@ -15,18 +15,36 @@
* limitations under the License. * limitations under the License.
*/ */
import { Args, Command } from '@oclif/core'; import type { flags } from '@oclif/command';
import type { IArg } from '@oclif/parser/lib/args';
import type { import type {
BalenaSDK, BalenaSDK,
Device, Device,
PineOptions, DeviceType,
PineTypedResult, PineTypedResult,
} from 'balena-sdk'; } from 'balena-sdk';
import Command from '../../command';
import * as cf from '../../utils/common-flags'; import * as cf from '../../utils/common-flags';
import { ExpectedError } from '../../errors'; import { ExpectedError } from '../../errors';
import { getBalenaSdk, stripIndent } from '../../utils/lazy'; import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { applicationIdInfo } from '../../utils/messages'; 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 { export default class DeviceMoveCmd extends Command {
public static description = stripIndent` public static description = stripIndent`
Move one or more devices to another fleet. Move one or more devices to another fleet.
@ -45,63 +63,61 @@ export default class DeviceMoveCmd extends Command {
'$ balena device move 7cf02a6 -f myorg/mynewfleet', '$ balena device move 7cf02a6 -f myorg/mynewfleet',
]; ];
public static args = { public static args: Array<IArg<any>> = [
uuid: Args.string({ {
name: 'uuid',
description: description:
'comma-separated list (no blank spaces) of device UUIDs to be moved', 'comma-separated list (no blank spaces) of device UUIDs to be moved',
required: true, required: true,
}), },
}; ];
public static flags = { public static usage = 'device move <uuid(s)>';
public static flags: flags.Input<FlagsDef> = {
fleet: cf.fleet, fleet: cf.fleet,
help: cf.help,
}; };
public static authenticated = true; 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() { 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(); const balena = getBalenaSdk();
const { expandForAppNameAndCpuArch } = await import('../../utils/helpers');
// Split uuids string into array of uuids // Split uuids string into array of uuids
const deviceUuids = params.uuid.split(','); const deviceUuids = params.uuid.split(',');
const devices = await this.getDevices(balena, deviceUuids); // Get devices
const devices = await Promise.all(
deviceUuids.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 // Disambiguate application
const { getApplication } = await import('../../utils/sdk'); const { getApplication } = await import('../../utils/sdk');
// Get destination application // Get destination application
const application = options.fleet const application = options.fleet
? await getApplication(balena, options.fleet, { $select: ['id', 'slug'] }) ? await getApplication(balena, options.fleet)
: await this.interactivelySelectApplication(balena, devices); : await this.interactivelySelectApplication(balena, devices);
// Move each device // Move each device
@ -118,8 +134,9 @@ export default class DeviceMoveCmd extends Command {
async interactivelySelectApplication( async interactivelySelectApplication(
balena: BalenaSDK, balena: BalenaSDK,
devices: Awaited<ReturnType<typeof this.getDevices>>, devices: ExtendedDevice[],
) { ) {
const { getExpandedProp } = await import('../../utils/pine');
// deduplicate the slugs // deduplicate the slugs
const deviceCpuArchs = Array.from( const deviceCpuArchs = Array.from(
new Set( new Set(
@ -129,44 +146,46 @@ export default class DeviceMoveCmd extends Command {
), ),
); );
const allCpuArches = await balena.pine.get({ const deviceTypeOptions = {
resource: 'cpu_architecture', $select: 'slug',
options: { $expand: {
$select: ['id', 'slug'], 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 const compatibleDeviceTypeSlugs = new Set(
.filter((cpuArch) => { deviceTypes
return deviceCpuArchs.every((deviceCpuArch) => .filter((deviceType) => {
balena.models.os.isArchitectureCompatibleWith( const deviceTypeArch = getExpandedProp(
deviceCpuArch, deviceType.is_of__cpu_architecture,
cpuArch.slug, 'slug',
), )!;
); return deviceCpuArchs.every((deviceCpuArch) =>
}) balena.models.os.isArchitectureCompatibleWith(
.map((deviceType) => deviceType.id); deviceCpuArch,
deviceTypeArch,
),
);
})
.map((deviceType) => deviceType.slug),
);
const patterns = await import('../../utils/patterns'); const patterns = await import('../../utils/patterns');
try { try {
const application = await patterns.selectApplication( const application = await patterns.selectApplication(
{ (app) =>
is_for__device_type: { compatibleDeviceTypeSlugs.has(app.is_for__device_type[0].slug) &&
$any: { devices.some((device) => device.application_name !== app.app_name),
$alias: 'dt',
$expr: {
dt: {
is_of__cpu_architecture: { $in: compatibleCpuArchIds },
},
},
},
},
},
true, true,
); );
return application; return application;
} catch (err) { } catch (err) {
if (!compatibleCpuArchIds.length) { if (!compatibleDeviceTypeSlugs.size) {
throw new ExpectedError( throw new ExpectedError(
`${err.message}\nDo all devices have a compatible architecture?`, `${err.message}\nDo all devices have a compatible architecture?`,
); );

View File

@ -15,12 +15,23 @@
* limitations under the License. * limitations under the License.
*/ */
import { Flags, Args, Command } 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 cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy'; import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy';
import type { Device } from 'balena-sdk'; import type { Device } from 'balena-sdk';
import { ExpectedError } from '../../errors'; import { ExpectedError } from '../../errors';
import { getExpandedProp } from '../../utils/pine';
interface FlagsDef {
version?: string;
yes: boolean;
help: void;
}
interface ArgsDef {
uuid: string;
}
export default class DeviceOsUpdateCmd extends Command { export default class DeviceOsUpdateCmd extends Command {
public static description = stripIndent` public static description = stripIndent`
@ -37,34 +48,32 @@ export default class DeviceOsUpdateCmd extends Command {
'$ balena device os-update 23c73a1', '$ balena device os-update 23c73a1',
'$ balena device os-update 23c73a1 --version 2.101.7', '$ balena device os-update 23c73a1 --version 2.101.7',
'$ balena device os-update 23c73a1 --version 2.31.0+rev1.prod', '$ balena device os-update 23c73a1 --version 2.31.0+rev1.prod',
'$ balena device os-update 23c73a1 --include-draft',
]; ];
public static args = { public static args: Array<IArg<any>> = [
uuid: Args.string({ {
name: 'uuid',
description: 'the uuid of the device to update', description: 'the uuid of the device to update',
required: true, required: true,
}), },
}; ];
public static flags = { public static usage = 'device os-update <uuid>';
version: Flags.string({
public static flags: flags.Input<FlagsDef> = {
version: flags.string({
description: 'a balenaOS version', description: 'a balenaOS version',
exclusive: ['include-draft'],
}),
'include-draft': Flags.boolean({
description: 'include pre-release balenaOS versions',
default: false,
exclusive: ['version'],
}), }),
yes: cf.yes, yes: cf.yes,
help: cf.help,
}; };
public static authenticated = true; public static authenticated = true;
public async run() { public async run() {
const { args: params, flags: options } = const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
await this.parse(DeviceOsUpdateCmd); DeviceOsUpdateCmd,
);
const sdk = getBalenaSdk(); const sdk = getBalenaSdk();
@ -90,25 +99,10 @@ export default class DeviceOsUpdateCmd extends Command {
); );
} }
let includeDraft = options['include-draft'];
if (!includeDraft && options.version != null) {
const bSemver = await import('balena-semver');
const parsedVersion = bSemver.parse(options.version);
// When the user provides a draft version, we need to pass `includeDraft`
// to the os.getSupportedOsUpdateVersions() since w/o it the results
// will for sure not include the user provided version and the command
// would return a "not in the Host OS update targets" error.
includeDraft =
parsedVersion != null && parsedVersion.prerelease.length > 0;
}
// Get supported OS update versions // Get supported OS update versions
const hupVersionInfo = await sdk.models.os.getSupportedOsUpdateVersions( const hupVersionInfo = await sdk.models.os.getSupportedOsUpdateVersions(
is_of__device_type[0].slug, is_of__device_type[0].slug,
currentOsVersion, currentOsVersion,
{
includeDraft,
},
); );
if (hupVersionInfo.versions.length === 0) { if (hupVersionInfo.versions.length === 0) {
throw new ExpectedError( throw new ExpectedError(
@ -119,72 +113,33 @@ export default class DeviceOsUpdateCmd extends Command {
// Get target OS version // Get target OS version
let targetOsVersion = options.version; let targetOsVersion = options.version;
if (targetOsVersion != null) { if (targetOsVersion != null) {
const { normalizeOsVersion } = await import('../../utils/normalization');
targetOsVersion = normalizeOsVersion(targetOsVersion);
if (!hupVersionInfo.versions.includes(targetOsVersion)) { if (!hupVersionInfo.versions.includes(targetOsVersion)) {
throw new ExpectedError( throw new ExpectedError(
`The provided version ${targetOsVersion} is not in the Host OS update targets for this device`, `The provided version ${targetOsVersion} is not in the Host OS update targets for this device`,
); );
} }
} else { } else {
const choices = await Promise.all(
hupVersionInfo.versions.map(async (version) => {
const takeoverRequired =
(await sdk.models.os.getOsUpdateType(
getExpandedProp(is_of__device_type, 'slug')!,
currentOsVersion,
version,
)) === 'takeover';
return {
name: `${version}${hupVersionInfo.recommended === version ? ' (recommended)' : ''}${takeoverRequired ? ' ADVANCED UPDATE: Requires disk re-partitioning with no rollback option' : ''}`,
value: version,
};
}),
);
targetOsVersion = await getCliForm().ask({ targetOsVersion = await getCliForm().ask({
message: 'Target OS version', message: 'Target OS version',
type: 'list', type: 'list',
choices, choices: hupVersionInfo.versions.map((version) => ({
name:
hupVersionInfo.recommended === version
? `${version} (recommended)`
: version,
value: version,
})),
}); });
} }
const takeoverRequired =
(await sdk.models.os.getOsUpdateType(
getExpandedProp(is_of__device_type, 'slug')!,
currentOsVersion,
targetOsVersion,
)) === 'takeover';
const patterns = await import('../../utils/patterns'); const patterns = await import('../../utils/patterns');
// Warn the user if the update requires a takeover
if (takeoverRequired) {
await patterns.confirm(
options.yes || false,
stripIndent`Before you proceed, note that this update process is different from a regular HostOS Update:
DATA LOSS: This update requires disk re-partitioning, which will erase all data stored on the device.
NO ROLLBACK: Unlike our HostOS update mechanism, this process does not allow reverting to a previous version in case of failure.
Make sure to back up all important data before continuing. For more details, check our documentation: https://docs.balena.io/reference/OS/updates/update-process/
`,
);
}
// Confirm and start update // Confirm and start update
await patterns.confirm( await patterns.confirm(
options.yes || false, options.yes || false,
'Host OS updates require a device restart when they complete. Are you sure you want to proceed?', 'Host OS updates require a device restart when they complete. Are you sure you want to proceed?',
); );
await sdk.models.device await sdk.models.device.startOsUpdate(uuid, targetOsVersion);
.startOsUpdate(uuid, targetOsVersion, { await patterns.awaitDeviceOsUpdate(uuid, targetOsVersion);
runDetached: true,
})
.then(() => {
console.log(
`The balena OS update has started. You can keep track of the progress via the dashboard.\n` +
`To open the dashboard page related to a device via the CLI, you can use \`balena device UUID --view\``,
);
})
.catch((error) => {
console.error(`Failed to start OS update for device ${uuid}:`, error);
});
} }
} }

View File

@ -15,10 +15,22 @@
* limitations under the License. * limitations under the License.
*/ */
import { Args, Command } 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 { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { getExpandedProp } from '../../utils/pine'; import { getExpandedProp } from '../../utils/pine';
interface FlagsDef {
help: void;
}
interface ArgsDef {
uuid: string;
releaseToPinTo?: string;
}
export default class DevicePinCmd extends Command { export default class DevicePinCmd extends Command {
public static description = stripIndent` public static description = stripIndent`
Pin a device to a release. Pin a device to a release.
@ -32,26 +44,34 @@ export default class DevicePinCmd extends Command {
'$ balena device pin 7cf02a6 91165e5', '$ balena device pin 7cf02a6 91165e5',
]; ];
public static args = { public static args: Array<IArg<any>> = [
uuid: Args.string({ {
name: 'uuid',
description: 'the uuid of the device to pin to a release', description: 'the uuid of the device to pin to a release',
required: true, required: true,
}), },
releaseToPinTo: Args.string({ {
name: 'releaseToPinTo',
description: 'the commit of the release for the device to get pinned to', description: 'the commit of the release for the device to get pinned to',
}), },
];
public static usage = 'device pin <uuid> [releaseToPinTo]';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
}; };
public static authenticated = true; public static authenticated = true;
public async run() { public async run() {
const { args: params } = await this.parse(DevicePinCmd); const { args: params } = this.parse<FlagsDef, ArgsDef>(DevicePinCmd);
const balena = getBalenaSdk(); const balena = getBalenaSdk();
const device = await balena.models.device.get(params.uuid, { const device = await balena.models.device.get(params.uuid, {
$expand: { $expand: {
is_pinned_on__release: { should_be_running__release: {
$select: 'commit', $select: 'commit',
}, },
belongs_to__application: { belongs_to__application: {
@ -61,7 +81,7 @@ export default class DevicePinCmd extends Command {
}); });
const pinnedRelease = getExpandedProp( const pinnedRelease = getExpandedProp(
device.is_pinned_on__release, device.should_be_running__release,
'commit', 'commit',
); );
const appSlug = getExpandedProp(device.belongs_to__application, 'slug'); const appSlug = getExpandedProp(device.belongs_to__application, 'slug');
@ -74,7 +94,7 @@ export default class DevicePinCmd extends Command {
pinnedRelease pinnedRelease
? `This device is currently pinned to ${pinnedRelease}.` ? `This device is currently pinned to ${pinnedRelease}.`
: 'This device is not currently pinned to any release.' : '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 release list ${appSlug}\`.`, } \n\nTo see a list of all releases this device can be pinned to, run \`balena releases ${appSlug}\`.`,
); );
} else { } else {
await balena.models.device.pinToRelease(params.uuid, releaseToPinTo); await balena.models.device.pinToRelease(params.uuid, releaseToPinTo);

View File

@ -15,10 +15,24 @@
* limitations under the License. * limitations under the License.
*/ */
import { Flags, Args, Command } 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 { ExpectedError } from '../../errors';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy'; import { getBalenaSdk, stripIndent } from '../../utils/lazy';
interface FlagsDef {
enable: boolean;
disable: boolean;
status: boolean;
help?: void;
}
interface ArgsDef {
uuid: string;
}
export default class DevicePublicUrlCmd extends Command { export default class DevicePublicUrlCmd extends Command {
public static description = stripIndent` public static description = stripIndent`
Get or manage the public URL for a device. Get or manage the public URL for a device.
@ -35,33 +49,38 @@ export default class DevicePublicUrlCmd extends Command {
'$ balena device public-url 23c73a1 --status', '$ balena device public-url 23c73a1 --status',
]; ];
public static args = { public static args: Array<IArg<any>> = [
uuid: Args.string({ {
name: 'uuid',
description: 'the uuid of the device to manage', description: 'the uuid of the device to manage',
required: true, required: true,
}), },
}; ];
public static flags = { public static usage = 'device public-url <uuid>';
enable: Flags.boolean({
public static flags: flags.Input<FlagsDef> = {
enable: flags.boolean({
description: 'enable the public URL', description: 'enable the public URL',
exclusive: ['disable', 'status'], exclusive: ['disable', 'status'],
}), }),
disable: Flags.boolean({ disable: flags.boolean({
description: 'disable the public URL', description: 'disable the public URL',
exclusive: ['enable', 'status'], exclusive: ['enable', 'status'],
}), }),
status: Flags.boolean({ status: flags.boolean({
description: 'determine if public URL is enabled', description: 'determine if public URL is enabled',
exclusive: ['enable', 'disable'], exclusive: ['enable', 'disable'],
}), }),
help: cf.help,
}; };
public static authenticated = true; public static authenticated = true;
public async run() { public async run() {
const { args: params, flags: options } = const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
await this.parse(DevicePublicUrlCmd); DevicePublicUrlCmd,
);
const balena = getBalenaSdk(); const balena = getBalenaSdk();

View File

@ -15,9 +15,20 @@
* limitations under the License. * limitations under the License.
*/ */
import { Args, Command } 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'; import { getBalenaSdk, getCliUx, stripIndent } from '../../utils/lazy';
interface FlagsDef {
help: void;
}
interface ArgsDef {
uuid: string;
}
export default class DevicePurgeCmd extends Command { export default class DevicePurgeCmd extends Command {
public static description = stripIndent` public static description = stripIndent`
Purge data from a device. Purge data from a device.
@ -33,17 +44,24 @@ export default class DevicePurgeCmd extends Command {
'$ balena device purge 55d43b3,23c73a1', '$ balena device purge 55d43b3,23c73a1',
]; ];
public static args = { public static usage = 'device purge <uuid>';
uuid: Args.string({
public static args: Array<IArg<any>> = [
{
name: 'uuid',
description: 'comma-separated list (no blank spaces) of device UUIDs', description: 'comma-separated list (no blank spaces) of device UUIDs',
required: true, required: true,
}), },
];
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
}; };
public static authenticated = true; public static authenticated = true;
public async run() { public async run() {
const { args: params } = await this.parse(DevicePurgeCmd); const { args: params } = this.parse<FlagsDef, ArgsDef>(DevicePurgeCmd);
const balena = getBalenaSdk(); const balena = getBalenaSdk();
const ux = getCliUx(); const ux = getCliUx();

View File

@ -15,10 +15,21 @@
* limitations under the License. * limitations under the License.
*/ */
import { Args, Command } 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 cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy'; import { getBalenaSdk, stripIndent } from '../../utils/lazy';
interface FlagsDef {
force: boolean;
help: void;
}
interface ArgsDef {
uuid: string;
}
export default class DeviceRebootCmd extends Command { export default class DeviceRebootCmd extends Command {
public static description = stripIndent` public static description = stripIndent`
Restart a device. Restart a device.
@ -27,21 +38,27 @@ export default class DeviceRebootCmd extends Command {
`; `;
public static examples = ['$ balena device reboot 23c73a1']; public static examples = ['$ balena device reboot 23c73a1'];
public static args = { public static args: Array<IArg<any>> = [
uuid: Args.string({ {
name: 'uuid',
description: 'the uuid of the device to reboot', description: 'the uuid of the device to reboot',
required: true, required: true,
}), },
}; ];
public static flags = { public static usage = 'device reboot <uuid>';
public static flags: flags.Input<FlagsDef> = {
force: cf.force, force: cf.force,
help: cf.help,
}; };
public static authenticated = true; public static authenticated = true;
public async run() { 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(); const balena = getBalenaSdk();

View File

@ -15,11 +15,24 @@
* limitations under the License. * limitations under the License.
*/ */
import { Flags, Command } 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 * as ca from '../../utils/common-args';
import { getBalenaSdk, stripIndent } from '../../utils/lazy'; import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { applicationIdInfo } from '../../utils/messages'; import { applicationIdInfo } from '../../utils/messages';
interface FlagsDef {
uuid?: string;
deviceType?: string;
help: void;
}
interface ArgsDef {
fleet: string;
}
export default class DeviceRegisterCmd extends Command { export default class DeviceRegisterCmd extends Command {
public static description = stripIndent` public static description = stripIndent`
Register a new device. Register a new device.
@ -38,34 +51,34 @@ export default class DeviceRegisterCmd extends Command {
'$ balena device register myorg/myfleet --uuid <uuid> --deviceType <deviceTypeSlug>', '$ balena device register myorg/myfleet --uuid <uuid> --deviceType <deviceTypeSlug>',
]; ];
public static args = { public static args: Array<IArg<any>> = [ca.fleetRequired];
fleet: ca.fleetRequired,
};
public static flags = { public static usage = 'device register <fleet>';
uuid: Flags.string({
public static flags: flags.Input<FlagsDef> = {
uuid: flags.string({
description: 'custom uuid', description: 'custom uuid',
char: 'u', char: 'u',
}), }),
deviceType: Flags.string({ deviceType: flags.string({
description: description:
"device type slug (run 'balena device-type list' for possible values)", "device type slug (run 'balena devices supported' for possible values)",
}), }),
help: cf.help,
}; };
public static authenticated = true; public static authenticated = true;
public async run() { public async run() {
const { args: params, flags: options } = const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
await this.parse(DeviceRegisterCmd); DeviceRegisterCmd,
);
const { getApplication } = await import('../../utils/sdk'); const { getApplication } = await import('../../utils/sdk');
const balena = getBalenaSdk(); const balena = getBalenaSdk();
const application = await getApplication(balena, params.fleet, { const application = await getApplication(balena, params.fleet);
$select: ['id', 'slug'],
});
const uuid = options.uuid ?? balena.models.device.generateUniqueKey(); const uuid = options.uuid ?? balena.models.device.generateUniqueKey();
console.info(`Registering to ${application.slug}: ${uuid}`); console.info(`Registering to ${application.slug}: ${uuid}`);
@ -76,6 +89,6 @@ export default class DeviceRegisterCmd extends Command {
options.deviceType, options.deviceType,
); );
return result.uuid; return result && result.uuid;
} }
} }

View File

@ -15,9 +15,21 @@
* limitations under the License. * limitations under the License.
*/ */
import { Args, Command } 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 { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy';
interface FlagsDef {
help: void;
}
interface ArgsDef {
uuid: string;
newName?: string;
}
export default class DeviceRenameCmd extends Command { export default class DeviceRenameCmd extends Command {
public static description = stripIndent` public static description = stripIndent`
Rename a device. Rename a device.
@ -31,20 +43,28 @@ export default class DeviceRenameCmd extends Command {
'$ balena device rename 7cf02a6 MyPi', '$ balena device rename 7cf02a6 MyPi',
]; ];
public static args = { public static args: Array<IArg<any>> = [
uuid: Args.string({ {
name: 'uuid',
description: 'the uuid of the device to rename', description: 'the uuid of the device to rename',
required: true, required: true,
}), },
newName: Args.string({ {
name: 'newName',
description: 'the new name for the device', description: 'the new name for the device',
}), },
];
public static usage = 'device rename <uuid> [newName]';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
}; };
public static authenticated = true; public static authenticated = true;
public async run() { public async run() {
const { args: params } = await this.parse(DeviceRenameCmd); const { args: params } = this.parse<FlagsDef, ArgsDef>(DeviceRenameCmd);
const balena = getBalenaSdk(); const balena = getBalenaSdk();

View File

@ -15,7 +15,10 @@
* limitations under the License. * limitations under the License.
*/ */
import { Flags, Args, Command } 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'; import { getBalenaSdk, getCliUx, stripIndent } from '../../utils/lazy';
import type { import type {
BalenaSDK, BalenaSDK,
@ -23,6 +26,15 @@ import type {
CurrentServiceWithCommit, CurrentServiceWithCommit,
} from 'balena-sdk'; } from 'balena-sdk';
interface FlagsDef {
help: void;
service?: string;
}
interface ArgsDef {
uuid: string;
}
export default class DeviceRestartCmd extends Command { export default class DeviceRestartCmd extends Command {
public static description = stripIndent` public static description = stripIndent`
Restart containers on a device. Restart containers on a device.
@ -43,26 +55,32 @@ export default class DeviceRestartCmd extends Command {
'$ balena device restart 23c73a1 -s myService1,myService2', '$ balena device restart 23c73a1 -s myService1,myService2',
]; ];
public static args = { public static args: Array<IArg<any>> = [
uuid: Args.string({ {
name: 'uuid',
description: description:
'comma-separated list (no blank spaces) of device UUIDs to restart', 'comma-separated list (no blank spaces) of device UUIDs to restart',
required: true, required: true,
}), },
}; ];
public static flags = { public static usage = 'device restart <uuid>';
service: Flags.string({
public static flags: flags.Input<FlagsDef> = {
service: flags.string({
description: description:
'comma-separated list (no blank spaces) of service names to restart', 'comma-separated list (no blank spaces) of service names to restart',
char: 's', char: 's',
}), }),
help: cf.help,
}; };
public static authenticated = true; public static authenticated = true;
public async run() { public async run() {
const { args: params, flags: options } = await this.parse(DeviceRestartCmd); const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
DeviceRestartCmd,
);
const balena = getBalenaSdk(); const balena = getBalenaSdk();
const ux = getCliUx(); const ux = getCliUx();
@ -154,7 +172,7 @@ export default class DeviceRestartCmd extends Command {
async restartAllServices(balena: BalenaSDK, deviceUuid: string) { async restartAllServices(balena: BalenaSDK, deviceUuid: string) {
// Note: device.restartApplication throws `BalenaDeviceNotFound: Device not found` if device not online. // Note: device.restartApplication throws `BalenaDeviceNotFound: Device not found` if device not online.
// Need to use device.get first to distinguish between non-existant and disconnected devices. // 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 // Remove this workaround when SDK issue resolved: https://github.com/balena-io/balena-sdk/issues/649
const { instanceOf, ExpectedError } = await import('../../errors'); const { instanceOf, ExpectedError } = await import('../../errors');
try { try {

View File

@ -15,10 +15,21 @@
* limitations under the License. * limitations under the License.
*/ */
import { Args, Command } 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 cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy'; import { getBalenaSdk, stripIndent } from '../../utils/lazy';
interface FlagsDef {
yes: boolean;
help: void;
}
interface ArgsDef {
uuid: string;
}
export default class DeviceRmCmd extends Command { export default class DeviceRmCmd extends Command {
public static description = stripIndent` public static description = stripIndent`
Remove one or more devices. Remove one or more devices.
@ -34,22 +45,28 @@ export default class DeviceRmCmd extends Command {
'$ balena device rm 7cf02a6 --yes', '$ balena device rm 7cf02a6 --yes',
]; ];
public static args = { public static args: Array<IArg<any>> = [
uuid: Args.string({ {
name: 'uuid',
description: description:
'comma-separated list (no blank spaces) of device UUIDs to be removed', 'comma-separated list (no blank spaces) of device UUIDs to be removed',
required: true, required: true,
}), },
}; ];
public static flags = { public static usage = 'device rm <uuid(s)>';
public static flags: flags.Input<FlagsDef> = {
yes: cf.yes, yes: cf.yes,
help: cf.help,
}; };
public static authenticated = true; public static authenticated = true;
public async run() { 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 balena = getBalenaSdk();
const patterns = await import('../../utils/patterns'); const patterns = await import('../../utils/patterns');

View File

@ -15,11 +15,22 @@
* limitations under the License. * limitations under the License.
*/ */
import { Args, Command } 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 cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy'; import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { ExpectedError } from '../../errors'; import { ExpectedError } from '../../errors';
interface FlagsDef {
force: boolean;
help: void;
}
interface ArgsDef {
uuid: string;
}
export default class DeviceShutdownCmd extends Command { export default class DeviceShutdownCmd extends Command {
public static description = stripIndent` public static description = stripIndent`
Shutdown a device. Shutdown a device.
@ -28,22 +39,27 @@ export default class DeviceShutdownCmd extends Command {
`; `;
public static examples = ['$ balena device shutdown 23c73a1']; public static examples = ['$ balena device shutdown 23c73a1'];
public static args = { public static args: Array<IArg<any>> = [
uuid: Args.string({ {
name: 'uuid',
description: 'the uuid of the device to shutdown', description: 'the uuid of the device to shutdown',
required: true, required: true,
}), },
}; ];
public static flags = { public static usage = 'device shutdown <uuid>';
public static flags: flags.Input<FlagsDef> = {
force: cf.force, force: cf.force,
help: cf.help,
}; };
public static authenticated = true; public static authenticated = true;
public async run() { public async run() {
const { args: params, flags: options } = const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
await this.parse(DeviceShutdownCmd); DeviceShutdownCmd,
);
const balena = getBalenaSdk(); const balena = getBalenaSdk();

View File

@ -15,9 +15,20 @@
* limitations under the License. * limitations under the License.
*/ */
import { Args, Command } 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 { getBalenaSdk, stripIndent } from '../../utils/lazy';
interface FlagsDef {
help: void;
}
interface ArgsDef {
uuid: string;
}
export default class DeviceTrackFleetCmd extends Command { export default class DeviceTrackFleetCmd extends Command {
public static description = stripIndent` public static description = stripIndent`
Make a device track the fleet's pinned release. Make a device track the fleet's pinned release.
@ -26,17 +37,24 @@ export default class DeviceTrackFleetCmd extends Command {
`; `;
public static examples = ['$ balena device track-fleet 7cf02a6']; public static examples = ['$ balena device track-fleet 7cf02a6'];
public static args = { public static args: Array<IArg<any>> = [
uuid: Args.string({ {
name: 'uuid',
description: "the uuid of the device to make track the fleet's release", description: "the uuid of the device to make track the fleet's release",
required: true, required: true,
}), },
];
public static usage = 'device track-fleet <uuid>';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
}; };
public static authenticated = true; public static authenticated = true;
public async run() { public async run() {
const { args: params } = await this.parse(DeviceTrackFleetCmd); const { args: params } = this.parse<FlagsDef, ArgsDef>(DeviceTrackFleetCmd);
const balena = getBalenaSdk(); const balena = getBalenaSdk();

View File

@ -15,30 +15,28 @@
* limitations under the License. * limitations under the License.
*/ */
import { Command } from '@oclif/core'; import { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags'; import * as cf from '../../utils/common-flags';
import { expandForAppName } from '../../utils/helpers'; import { expandForAppName } from '../../utils/helpers';
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy'; import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
import { applicationIdInfo, jsonInfo } from '../../utils/messages'; import { applicationIdInfo, jsonInfo } from '../../utils/messages';
import type { Device, PineOptions } from 'balena-sdk'; import type { Application } from 'balena-sdk';
const devicesSelectFields = { interface ExtendedDevice extends DeviceWithDeviceType {
$select: [ dashboard_url?: string;
'id', fleet?: string | null; // 'org/name' slug
'uuid', device_type?: string | null;
'device_name', }
'status',
'is_online',
'supervisor_version',
'os_version',
],
} satisfies PineOptions<Device>;
export default class DeviceListCmd extends Command { interface FlagsDef {
public static aliases = ['devices']; fleet?: string;
public static deprecateAliases = true; help: void;
json: boolean;
}
export default class DevicesCmd extends Command {
public static description = stripIndent` public static description = stripIndent`
List all devices. List all devices.
@ -51,14 +49,17 @@ export default class DeviceListCmd extends Command {
${jsonInfo.split('\n').join('\n\t\t')} ${jsonInfo.split('\n').join('\n\t\t')}
`; `;
public static examples = [ public static examples = [
'$ balena device list', '$ balena devices',
'$ balena device list --fleet MyFleet', '$ balena devices --fleet MyFleet',
'$ balena device list -f myorg/myfleet', '$ balena devices -f myorg/myfleet',
]; ];
public static flags = { public static usage = 'devices';
public static flags: flags.Input<FlagsDef> = {
fleet: cf.fleet, fleet: cf.fleet,
json: cf.json, json: cf.json,
help: cf.help,
}; };
public static primary = true; public static primary = true;
@ -66,42 +67,39 @@ export default class DeviceListCmd extends Command {
public static authenticated = true; public static authenticated = true;
public async run() { public async run() {
const { flags: options } = await this.parse(DeviceListCmd); const { flags: options } = this.parse<FlagsDef, {}>(DevicesCmd);
const balena = getBalenaSdk(); const balena = getBalenaSdk();
const devicesOptions = {
...devicesSelectFields,
...expandForAppName,
$orderby: { device_name: 'asc' },
} satisfies PineOptions<Device>;
const devices = ( let 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;
}
return await balena.pine.get({ if (options.fleet != null) {
resource: 'device', const { getApplication } = await import('../../utils/sdk');
options: devicesOptions, const application = await getApplication(balena, options.fleet);
}); devices = (await balena.models.device.getAllByApplication(
})() application.id,
).map((device) => ({ expandForAppName,
...device, )) as ExtendedDevice[];
dashboard_url: balena.models.device.getDashboardUrl(device.uuid), } else {
fleet: device.belongs_to__application?.[0]?.slug || null, devices = (await balena.models.device.getAll(
uuid: options.json ? device.uuid : device.uuid.slice(0, 7), expandForAppName,
device_type: device.is_of__device_type?.[0]?.slug || null, )) as ExtendedDevice[];
})); }
const fields: Array<keyof (typeof devices)[number]> = [ 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;
});
const fields = [
'id', 'id',
'uuid', 'uuid',
'device_name', 'device_name',

View File

@ -14,23 +14,24 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import { Flags, Command } from '@oclif/core'; import { flags } from '@oclif/command';
import type * as BalenaSdk from 'balena-sdk';
import * as _ from 'lodash'; import * as _ from 'lodash';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy'; import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
import { CommandHelp } from '../../utils/oclif-utils';
export default class DeviceTypeListCmd extends Command { interface FlagsDef {
public static aliases = ['devices supported']; help: void;
public static deprecateAliases = true; json?: boolean;
}
export default class DevicesSupportedCmd extends Command {
public static description = stripIndent` public static description = stripIndent`
List the device types supported by balena (like 'raspberrypi3' or 'intel-nuc'). List the supported device types (like 'raspberrypi3' or 'intel-nuc').
List the device types supported by balena (like 'raspberrypi3' or 'intel-nuc'). List the supported device types (like 'raspberrypi3' or 'intel-nuc').
By default, only actively supported device types are listed.
The --all option can be used to list all device types, including those that are
no longer supported by balena.
The --json option is recommended when scripting the output of this command, The --json option is recommended when scripting the output of this command,
because the JSON format is less likely to change and it better represents data because the JSON format is less likely to change and it better represents data
@ -39,58 +40,55 @@ export default class DeviceTypeListCmd extends Command {
(https://stedolan.github.io/jq/manual/). (https://stedolan.github.io/jq/manual/).
`; `;
public static examples = [ public static examples = [
'$ balena device-type list', '$ balena devices supported',
'$ balena device-type list --all', '$ balena devices supported --json',
'$ balena device-type list --json',
]; ];
public static flags = { public static usage = (
json: Flags.boolean({ 'devices supported ' +
new CommandHelp({ args: DevicesSupportedCmd.args }).defaultUsage()
).trim();
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
json: flags.boolean({
char: 'j', char: 'j',
description: 'produce JSON output instead of tabular output', description: 'produce JSON output instead of tabular output',
}), }),
all: Flags.boolean({
description: 'include device types no longer supported by balena',
default: false,
}),
}; };
public async run() { public async run() {
const { flags: options } = await this.parse(DeviceTypeListCmd); const { flags: options } = this.parse<FlagsDef, {}>(DevicesSupportedCmd);
const pineOptions = { const [dts, configDTs] = await Promise.all([
$select: ['slug', 'name'], getBalenaSdk().models.deviceType.getAllSupported({
$expand: { $expand: { is_of__cpu_architecture: { $select: 'slug' } },
is_of__cpu_architecture: { $select: 'slug' }, $select: ['slug', 'name'],
device_type_alias: { }),
$select: 'is_referenced_by__alias', getBalenaSdk().models.config.getDeviceTypes(),
$orderby: { is_referenced_by__alias: 'asc' }, ]);
}, const dtsBySlug = _.keyBy(dts, (dt) => dt.slug);
}, const configDTsBySlug = _.keyBy(configDTs, (dt) => dt.slug);
} satisfies BalenaSdk.PineOptions<BalenaSdk.DeviceType>;
const dts = (
options.all
? await getBalenaSdk().models.deviceType.getAll(pineOptions)
: await getBalenaSdk().models.deviceType.getAllSupported(pineOptions)
) as Array<
BalenaSdk.PineTypedResult<BalenaSdk.DeviceType, typeof pineOptions>
>;
interface DT { interface DT {
slug: string; slug: string;
aliases: string[]; aliases: string[];
arch: string; arch: string;
name: string; name: string;
} }
let deviceTypes = dts.map((dt): DT => { let deviceTypes: DT[] = [];
const aliases = dt.device_type_alias for (const slug of Object.keys(dtsBySlug)) {
.map((dta) => dta.is_referenced_by__alias) const configDT: Partial<typeof configDTs[0]> =
.filter((alias) => alias !== dt.slug); configDTsBySlug[slug] || {};
return { const aliases = (configDT.aliases || []).filter(
slug: dt.slug, (alias) => alias !== slug,
);
const dt: Partial<typeof dts[0]> = dtsBySlug[slug] || {};
deviceTypes.push({
slug,
aliases: options.json ? aliases : [aliases.join(', ')], aliases: options.json ? aliases : [aliases.join(', ')],
arch: dt.is_of__cpu_architecture[0]?.slug || 'n/a', arch: (dt.is_of__cpu_architecture as any)?.[0]?.slug || 'n/a',
name: dt.name || 'N/A', name: dt.name || 'N/A',
}; });
}); }
const fields = ['slug', 'aliases', 'arch', 'name']; const fields = ['slug', 'aliases', 'arch', 'name'];
deviceTypes = _.sortBy(deviceTypes, fields); deviceTypes = _.sortBy(deviceTypes, fields);
if (options.json) { if (options.json) {

View File

@ -15,8 +15,9 @@
* limitations under the License. * limitations under the License.
*/ */
import { Args, Command } from '@oclif/core'; import { flags } from '@oclif/command';
import type * as BalenaSdk from 'balena-sdk'; import type * as BalenaSdk from 'balena-sdk';
import Command from '../../command';
import { ExpectedError } from '../../errors'; import { ExpectedError } from '../../errors';
import * as cf from '../../utils/common-flags'; import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy'; import { getBalenaSdk, stripIndent } from '../../utils/lazy';
@ -25,6 +26,7 @@ import { applicationIdInfo } from '../../utils/messages';
interface FlagsDef { interface FlagsDef {
fleet?: string; fleet?: string;
device?: string; // device UUID device?: string; // device UUID
help: void;
quiet: boolean; quiet: boolean;
service?: string; // service name service?: string; // service name
} }
@ -34,14 +36,11 @@ interface ArgsDef {
value?: string; value?: string;
} }
export default class EnvSetCmd extends Command { export default class EnvAddCmd extends Command {
public static aliases = ['env add'];
public static deprecateAliases = true;
public static description = stripIndent` public static description = stripIndent`
Add or update env or config variable to fleets, devices or services. Add env or config variable to fleets, devices or services.
Add or update an environment or config variable to one or more fleets, devices or Add an environment or config variable to one or more fleets, devices or
services, as selected by the respective command-line options. Either the services, as selected by the respective command-line options. Either the
--fleet or the --device option must be provided, and either may be be --fleet or the --device option must be provided, and either may be be
used alongside the --service option to define a service-specific variable. used alongside the --service option to define a service-specific variable.
@ -68,41 +67,45 @@ export default class EnvSetCmd extends Command {
`; `;
public static examples = [ public static examples = [
'$ balena env set TERM --fleet MyFleet', '$ balena env add TERM --fleet MyFleet',
'$ balena env set EDITOR vim -f myorg/myfleet', '$ balena env add EDITOR vim -f myorg/myfleet',
'$ balena env set EDITOR vim --fleet MyFleet,MyFleet2', '$ balena env add EDITOR vim --fleet MyFleet,MyFleet2',
'$ balena env set EDITOR vim --fleet MyFleet --service MyService', '$ balena env add EDITOR vim --fleet MyFleet --service MyService',
'$ balena env set EDITOR vim --fleet MyFleet,MyFleet2 --service MyService,MyService2', '$ balena env add EDITOR vim --fleet MyFleet,MyFleet2 --service MyService,MyService2',
'$ balena env set EDITOR vim --device 7cf02a6', '$ balena env add EDITOR vim --device 7cf02a6',
'$ balena env set EDITOR vim --device 7cf02a6,d6f1433', '$ balena env add EDITOR vim --device 7cf02a6,d6f1433',
'$ balena env set EDITOR vim --device 7cf02a6 --service MyService', '$ balena env add EDITOR vim --device 7cf02a6 --service MyService',
'$ balena env set EDITOR vim --device 7cf02a6,d6f1433 --service MyService,MyService2', '$ balena env add EDITOR vim --device 7cf02a6,d6f1433 --service MyService,MyService2',
]; ];
public static args = { public static args = [
name: Args.string({ {
name: 'name',
required: true, required: true,
description: 'environment or config variable name', description: 'environment or config variable name',
}), },
value: Args.string({ {
name: 'value',
required: false, required: false,
description: description:
"variable value; if omitted, use value from this process' environment", "variable value; if omitted, use value from this process' environment",
}), },
}; ];
// Required for supporting empty string ('') `value` args. public static usage = 'env add <name> [value]';
public static strict = false;
public static flags = { public static flags: flags.Input<FlagsDef> = {
fleet: { ...cf.fleet, exclusive: ['device'] }, fleet: { ...cf.fleet, exclusive: ['device'] },
device: { ...cf.device, exclusive: ['fleet'] }, device: { ...cf.device, exclusive: ['fleet'] },
help: cf.help,
quiet: cf.quiet, quiet: cf.quiet,
service: cf.service, service: cf.service,
}; };
public async run() { public async run() {
const { args: params, flags: options } = await this.parse(EnvSetCmd); const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
EnvAddCmd,
);
const cmd = this; const cmd = this;
if (!options.fleet && !options.device) { if (!options.fleet && !options.device) {
@ -111,9 +114,7 @@ export default class EnvSetCmd extends Command {
); );
} }
const { checkLoggedIn } = await import('../../utils/patterns'); await Command.checkLoggedIn();
await checkLoggedIn();
if (params.value == null) { if (params.value == null) {
params.value = process.env[params.name]; params.value = process.env[params.name];
@ -150,15 +151,16 @@ export default class EnvSetCmd extends Command {
const varType = isConfigVar ? 'configVar' : 'envVar'; const varType = isConfigVar ? 'configVar' : 'envVar';
if (options.fleet) { 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 { try {
await balena.models.application[varType].set( await balena.models.application[varType].set(
appSlug, await getFleetSlug(balena, app),
params.name, params.name,
params.value, params.value,
); );
} catch (err) { } catch (err) {
console.error(`${err.message}, fleet: ${appSlug}`); console.error(`${err.message}, fleet: ${app}`);
process.exitCode = 1; process.exitCode = 1;
} }
} }
@ -179,25 +181,6 @@ export default class EnvSetCmd 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. * Add service variables for a device or fleet.
*/ */
@ -207,17 +190,17 @@ async function setServiceVars(
options: FlagsDef, options: FlagsDef,
) { ) {
if (options.fleet) { 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(',')) { for (const service of options.service!.split(',')) {
try { try {
const serviceId = await getServiceIdForApp(sdk, appSlug, service); const serviceId = await getServiceIdForApp(sdk, app, service);
await sdk.models.service.var.set( await sdk.models.service.var.set(
serviceId, serviceId,
params.name, params.name,
params.value!, params.value!,
); );
} catch (err) { } catch (err) {
console.error(`${err.message}, fleet: ${appSlug}`); console.error(`${err.message}, fleet: ${app}`);
process.exitCode = 1; process.exitCode = 1;
} }
} }
@ -262,12 +245,11 @@ async function setServiceVars(
*/ */
async function getServiceIdForApp( async function getServiceIdForApp(
sdk: BalenaSdk.BalenaSDK, sdk: BalenaSdk.BalenaSDK,
appSlug: string, appName: string,
serviceName: string, serviceName: string,
): Promise<number> { ): Promise<number> {
let serviceId: number | undefined; let serviceId: number | undefined;
const services = await sdk.models.service.getAllByApplication(appSlug, { const services = await sdk.models.service.getAllByApplication(appName, {
$select: 'id',
$filter: { service_name: serviceName }, $filter: { service_name: serviceName },
}); });
if (services.length > 0) { if (services.length > 0) {
@ -275,7 +257,7 @@ async function getServiceIdForApp(
} }
if (serviceId === undefined) { if (serviceId === undefined) {
throw new ExpectedError( throw new ExpectedError(
`Cannot find service ${serviceName} for fleet ${appSlug}`, `Cannot find service ${serviceName} for fleet ${appName}`,
); );
} }
return serviceId; return serviceId;

View File

@ -14,11 +14,28 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import { Args, Command } from '@oclif/core'; import { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import * as ec from '../../utils/env-common'; import * as ec from '../../utils/env-common';
import { getBalenaSdk, stripIndent } from '../../utils/lazy'; import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { parseAsInteger } from '../../utils/validation'; 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 { export default class EnvRenameCmd extends Command {
public static description = stripIndent` public static description = stripIndent`
Change the value of a config or env var for a fleet, device or service. Change the value of a config or env var for a fleet, device or service.
@ -37,31 +54,36 @@ export default class EnvRenameCmd extends Command {
'$ balena env rename 678678 1 --device --config', '$ balena env rename 678678 1 --device --config',
]; ];
public static args = { public static args: Array<IArg<any>> = [
id: Args.integer({ {
name: 'id',
required: true, required: true,
description: "variable's numeric database ID", description: "variable's numeric database ID",
parse: (input) => parseAsInteger(input, 'id'), parse: (input) => parseAsInteger(input, 'id'),
}), },
value: Args.string({ {
name: 'value',
required: true, required: true,
description: description:
"variable value; if omitted, use value from this process' environment", "variable value; if omitted, use value from this process' environment",
}), },
}; ];
public static flags = { public static usage = 'env rename <id> <value>';
public static flags: flags.Input<FlagsDef> = {
config: ec.booleanConfig, config: ec.booleanConfig,
device: ec.booleanDevice, device: ec.booleanDevice,
service: ec.booleanService, service: ec.booleanService,
help: cf.help,
}; };
public async run() { public async run() {
const { args: params, flags: opt } = await this.parse(EnvRenameCmd); const { args: params, flags: opt } = this.parse<FlagsDef, ArgsDef>(
EnvRenameCmd,
);
const { checkLoggedIn } = await import('../../utils/patterns'); await Command.checkLoggedIn();
await checkLoggedIn();
await getBalenaSdk().pine.patch({ await getBalenaSdk().pine.patch({
resource: ec.getVarResourceName(opt.config, opt.device, opt.service), resource: ec.getVarResourceName(opt.config, opt.device, opt.service),

View File

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

View File

@ -14,16 +14,23 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import { Flags, Command } from '@oclif/core'; import { flags } from '@oclif/command';
import type { Interfaces } from '@oclif/core';
import type * as SDK from 'balena-sdk'; import type * as SDK from 'balena-sdk';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { ExpectedError } from '../../errors'; import Command from '../command';
import * as cf from '../../utils/common-flags'; import { ExpectedError } from '../errors';
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy'; import * as cf from '../utils/common-flags';
import { applicationIdInfo } from '../../utils/messages'; import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
import { applicationIdInfo } from '../utils/messages';
type FlagsDef = Interfaces.InferredFlags<typeof EnvListCmd.flags>; interface FlagsDef {
fleet?: string;
config: boolean;
device?: string; // device UUID
json: boolean;
help: void;
service?: string; // service name
}
interface EnvironmentVariableInfo extends SDK.EnvironmentVariableBase { interface EnvironmentVariableInfo extends SDK.EnvironmentVariableBase {
fleet?: string | null; // fleet slug fleet?: string | null; // fleet slug
@ -45,10 +52,7 @@ interface ServiceEnvironmentVariableInfo
serviceName?: string; // service name serviceName?: string; // service name
} }
export default class EnvListCmd extends Command { export default class EnvsCmd extends Command {
public static aliases = ['envs'];
public static deprecateAliases = true;
public static description = stripIndent` public static description = stripIndent`
List the environment or config variables of a fleet, device or service. List the environment or config variables of a fleet, device or service.
@ -86,37 +90,39 @@ export default class EnvListCmd extends Command {
`; `;
public static examples = [ public static examples = [
'$ balena env list --fleet myorg/myfleet', '$ balena envs --fleet myorg/myfleet',
'$ balena env list --fleet MyFleet --json', '$ balena envs --fleet MyFleet --json',
'$ balena env list --fleet MyFleet --service MyService', '$ balena envs --fleet MyFleet --service MyService',
'$ balena env list --fleet MyFleet --config', '$ balena envs --fleet MyFleet --service MyService',
'$ balena env list --device 7cf02a6', '$ balena envs --fleet MyFleet --config',
'$ balena env list --device 7cf02a6 --json', '$ balena envs --device 7cf02a6',
'$ balena env list --device 7cf02a6 --config --json', '$ balena envs --device 7cf02a6 --json',
'$ balena env list --device 7cf02a6 --service MyService', '$ balena envs --device 7cf02a6 --config --json',
'$ balena envs --device 7cf02a6 --service MyService',
]; ];
public static flags = { public static usage = 'envs';
public static flags: flags.Input<FlagsDef> = {
fleet: { ...cf.fleet, exclusive: ['device'] }, fleet: { ...cf.fleet, exclusive: ['device'] },
config: Flags.boolean({ config: flags.boolean({
default: false, default: false,
char: 'c', char: 'c',
description: 'show configuration variables only', description: 'show configuration variables only',
exclusive: ['service'], exclusive: ['service'],
}), }),
device: { ...cf.device, exclusive: ['fleet'] }, device: { ...cf.device, exclusive: ['fleet'] },
help: cf.help,
json: cf.json, json: cf.json,
service: { ...cf.service, exclusive: ['config'] }, service: { ...cf.service, exclusive: ['config'] },
}; };
public async run() { public async run() {
const { flags: options } = await this.parse(EnvListCmd); const { flags: options } = this.parse<FlagsDef, {}>(EnvsCmd);
const variables: EnvironmentVariableInfo[] = []; const variables: EnvironmentVariableInfo[] = [];
const { checkLoggedIn } = await import('../../utils/patterns'); await Command.checkLoggedIn();
await checkLoggedIn();
if (!options.fleet && !options.device) { if (!options.fleet && !options.device) {
throw new ExpectedError('Missing --fleet or --device option'); throw new ExpectedError('Missing --fleet or --device option');
@ -125,16 +131,12 @@ export default class EnvListCmd extends Command {
const balena = getBalenaSdk(); const balena = getBalenaSdk();
let fleetSlug: string | undefined = options.fleet let fleetSlug: string | undefined = options.fleet
? await ( ? await (await import('../utils/sdk')).getFleetSlug(balena, options.fleet)
await import('../../utils/sdk')
).getFleetSlug(balena, options.fleet)
: undefined; : undefined;
let fullUUID: string | undefined; // as oppposed to the short, 7-char UUID let fullUUID: string | undefined; // as oppposed to the short, 7-char UUID
if (options.device) { if (options.device) {
const { getDeviceAndMaybeAppFromUUID } = await import( const { getDeviceAndMaybeAppFromUUID } = await import('../utils/cloud');
'../../utils/cloud'
);
const [device, app] = await getDeviceAndMaybeAppFromUUID( const [device, app] = await getDeviceAndMaybeAppFromUUID(
balena, balena,
options.device, options.device,
@ -187,7 +189,7 @@ export default class EnvListCmd extends Command {
} }
if (options.json) { if (options.json) {
const { pickAndRename } = await import('../../utils/helpers'); const { pickAndRename } = await import('../utils/helpers');
const mapped = varArray.map((o) => pickAndRename(o, fields)); const mapped = varArray.map((o) => pickAndRename(o, fields));
this.log(JSON.stringify(mapped, null, 4)); this.log(JSON.stringify(mapped, null, 4));
} else { } else {
@ -207,7 +209,6 @@ async function validateServiceName(
fleetSlug: string, fleetSlug: string,
) { ) {
const services = await sdk.models.service.getAllByApplication(fleetSlug, { const services = await sdk.models.service.getAllByApplication(fleetSlug, {
$select: 'id',
$filter: { service_name: serviceName }, $filter: { service_name: serviceName },
}); });
if (services.length === 0) { if (services.length === 0) {
@ -233,10 +234,9 @@ async function getAppVars(
if (!fleetSlug) { if (!fleetSlug) {
return appVars; return appVars;
} }
const vars = const vars = await sdk.models.application[
await sdk.models.application[ options.config ? 'configVar' : 'envVar'
options.config ? 'configVar' : 'envVar' ].getAllByApplication(fleetSlug);
].getAllByApplication(fleetSlug);
fillInInfoFields(vars, fleetSlug); fillInInfoFields(vars, fleetSlug);
appVars.push(...vars); appVars.push(...vars);
if (!options.config) { if (!options.config) {
@ -275,8 +275,9 @@ async function getDeviceVars(
const printedUUID = options.json ? fullUUID : options.device!; const printedUUID = options.json ? fullUUID : options.device!;
const deviceVars: EnvironmentVariableInfo[] = []; const deviceVars: EnvironmentVariableInfo[] = [];
if (options.config) { if (options.config) {
const deviceConfigVars = const deviceConfigVars = await sdk.models.device.configVar.getAllByDevice(
await sdk.models.device.configVar.getAllByDevice(fullUUID); fullUUID,
);
fillInInfoFields(deviceConfigVars, fleetSlug, printedUUID); fillInInfoFields(deviceConfigVars, fleetSlug, printedUUID);
deviceVars.push(...deviceConfigVars); deviceVars.push(...deviceConfigVars);
} else { } else {
@ -301,8 +302,9 @@ async function getDeviceVars(
fillInInfoFields(deviceServiceVars, fleetSlug, printedUUID); fillInInfoFields(deviceServiceVars, fleetSlug, printedUUID);
deviceVars.push(...deviceServiceVars); deviceVars.push(...deviceServiceVars);
const deviceEnvVars = const deviceEnvVars = await sdk.models.device.envVar.getAllByDevice(
await sdk.models.device.envVar.getAllByDevice(fullUUID); fullUUID,
);
fillInInfoFields(deviceEnvVars, fleetSlug, printedUUID); fillInInfoFields(deviceEnvVars, fleetSlug, printedUUID);
deviceVars.push(...deviceEnvVars); deviceVars.push(...deviceEnvVars);
} }

View File

@ -0,0 +1,149 @@
/**
* @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 } 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 { 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`
Create a fleet.
Create a new balena fleet.
You can specify the organization the fleet 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 fleet'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 fleet create MyFleet',
'$ balena fleet create MyFleet --organization mmyorg',
'$ balena fleet create MyFleet -o myorg --type raspberry-pi',
];
public static args = [
{
name: 'name',
description: 'fleet name',
required: true,
},
];
public static usage = 'fleet create <name>';
public static flags: flags.Input<FlagsDef> = {
organization: flags.string({
char: 'o',
description: 'handle of the organization the fleet should belong to',
}),
type: flags.string({
char: 't',
description:
'fleet device type (Check available types with `balena devices supported`)',
}),
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
FleetCreateCmd,
);
// 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,11 +15,25 @@
* limitations under the License. * limitations under the License.
*/ */
import { Flags, Command } from '@oclif/core'; import type { flags as flagsType } from '@oclif/command';
import { flags } from '@oclif/command';
import type { Release } from 'balena-sdk';
import Command from '../../command';
import * as cf from '../../utils/common-flags'; import * as cf from '../../utils/common-flags';
import * as ca from '../../utils/common-args'; import * as ca from '../../utils/common-args';
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy'; import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { applicationIdInfo } from '../../utils/messages'; import { applicationIdInfo } from '../../utils/messages';
import type { DataOutputOptions } from '../../framework';
interface FlagsDef extends DataOutputOptions {
help: void;
view: boolean;
}
interface ArgsDef {
fleet: string;
}
export default class FleetCmd extends Command { export default class FleetCmd extends Command {
public static description = stripIndent` public static description = stripIndent`
@ -35,34 +49,42 @@ export default class FleetCmd extends Command {
'$ balena fleet myorg/myfleet --view', '$ balena fleet myorg/myfleet --view',
]; ];
public static args = { public static args = [ca.fleetRequired];
fleet: ca.fleetRequired,
};
public static flags = { public static usage = 'fleet <fleet>';
view: Flags.boolean({
public static flags: flagsType.Input<FlagsDef> = {
help: cf.help,
view: flags.boolean({
default: false, default: false,
description: 'open fleet dashboard page', description: 'open fleet dashboard page',
}), }),
json: cf.json, ...cf.dataOutputFlags,
}; };
public static authenticated = true; public static authenticated = true;
public static primary = true; public static primary = true;
public async run() { 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 { getApplication } = await import('../../utils/sdk');
const balena = getBalenaSdk(); const balena = getBalenaSdk();
const application = await getApplication(balena, params.fleet, { const application = (await getApplication(balena, params.fleet, {
$expand: { $expand: {
is_for__device_type: { $select: 'slug' }, is_for__device_type: { $select: 'slug' },
should_be_running__release: { $select: 'commit' }, should_be_running__release: { $select: 'commit' },
}, },
}); })) as ApplicationWithDeviceType & {
should_be_running__release: [Release?];
// For display purposes:
device_type: string;
commit?: string;
};
if (options.view) { if (options.view) {
const open = await import('open'); const open = await import('open');
@ -73,28 +95,13 @@ export default class FleetCmd extends Command {
return; return;
} }
const applicationToDisplay = { application.device_type = application.is_for__device_type[0].slug;
id: application.id, application.commit = application.should_be_running__release[0]?.commit;
app_name: application.app_name,
slug: application.slug,
device_type: application.is_for__device_type[0].slug,
commit: application.should_be_running__release[0]?.commit,
};
if (options.json) { await this.outputData(
console.log(JSON.stringify(applicationToDisplay, null, 4)); application,
return; ['app_name', 'id', 'device_type', 'slug', 'commit'],
} options,
// Emulate table.vertical title output, but avoid uppercasing and inserting spaces
console.log(`== ${applicationToDisplay.app_name}`);
console.log(
getVisuals().table.vertical(applicationToDisplay, [
'id',
'device_type',
'slug',
'commit',
]),
); );
} }
} }

View File

@ -15,10 +15,22 @@
* limitations under the License. * limitations under the License.
*/ */
import { Args, Command } 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 { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { getExpandedProp } from '../../utils/pine'; import { getExpandedProp } from '../../utils/pine';
interface FlagsDef {
help: void;
}
interface ArgsDef {
slug: string;
releaseToPinTo?: string;
}
export default class FleetPinCmd extends Command { export default class FleetPinCmd extends Command {
public static description = stripIndent` public static description = stripIndent`
Pin a fleet to a release. Pin a fleet to a release.
@ -32,20 +44,28 @@ export default class FleetPinCmd extends Command {
'$ balena fleet pin myorg/myfleet 91165e5', '$ balena fleet pin myorg/myfleet 91165e5',
]; ];
public static args = { public static args: Array<IArg<any>> = [
slug: Args.string({ {
name: 'slug',
description: 'the slug of the fleet to pin to a release', description: 'the slug of the fleet to pin to a release',
required: true, required: true,
}), },
releaseToPinTo: Args.string({ {
name: 'releaseToPinTo',
description: 'the commit of the release for the fleet to get pinned to', description: 'the commit of the release for the fleet to get pinned to',
}), },
];
public static usage = 'fleet pin <slug> [releaseToPinTo]';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
}; };
public static authenticated = true; public static authenticated = true;
public async run() { public async run() {
const { args: params } = await this.parse(FleetPinCmd); const { args: params } = this.parse<FlagsDef, ArgsDef>(FleetPinCmd);
const balena = getBalenaSdk(); const balena = getBalenaSdk();
@ -71,7 +91,7 @@ export default class FleetPinCmd extends Command {
pinnedRelease pinnedRelease
? `This fleet is currently pinned to ${pinnedRelease}.` ? `This fleet is currently pinned to ${pinnedRelease}.`
: 'This fleet is not currently pinned to any release.' : '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 release list ${slug}\`.`, } \n\nTo see a list of all releases this fleet can be pinned to, run \`balena releases ${slug}\`.`,
); );
} else { } else {
await balena.models.application.pinToRelease(slug, releaseToPinTo); await balena.models.application.pinToRelease(slug, releaseToPinTo);

View File

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

View File

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

View File

@ -15,11 +15,22 @@
* limitations under the License. * limitations under the License.
*/ */
import { Command } from '@oclif/core'; 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 * as ca from '../../utils/common-args';
import { getBalenaSdk, stripIndent } from '../../utils/lazy'; import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { applicationIdInfo } from '../../utils/messages'; import { applicationIdInfo } from '../../utils/messages';
interface FlagsDef {
help: void;
}
interface ArgsDef {
fleet: string;
}
export default class FleetRestartCmd extends Command { export default class FleetRestartCmd extends Command {
public static description = stripIndent` public static description = stripIndent`
Restart a fleet. Restart a fleet.
@ -34,23 +45,25 @@ export default class FleetRestartCmd extends Command {
'$ balena fleet restart myorg/myfleet', '$ balena fleet restart myorg/myfleet',
]; ];
public static args = { public static args = [ca.fleetRequired];
fleet: ca.fleetRequired,
public static usage = 'fleet restart <fleet>';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
}; };
public static authenticated = true; public static authenticated = true;
public async run() { 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 { getApplication } = await import('../../utils/sdk');
const balena = getBalenaSdk(); const balena = getBalenaSdk();
// Disambiguate application // Disambiguate application
const application = await getApplication(balena, params.fleet, { const application = await getApplication(balena, params.fleet);
$select: 'slug',
});
await balena.models.application.restart(application.slug); await balena.models.application.restart(application.slug);
} }

View File

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

View File

@ -15,9 +15,20 @@
* limitations under the License. * limitations under the License.
*/ */
import { Args, Command } 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 { getBalenaSdk, stripIndent } from '../../utils/lazy';
interface FlagsDef {
help: void;
}
interface ArgsDef {
slug: string;
}
export default class FleetTrackLatestCmd extends Command { export default class FleetTrackLatestCmd extends Command {
public static description = stripIndent` public static description = stripIndent`
Make this fleet track the latest release. Make this fleet track the latest release.
@ -29,17 +40,24 @@ export default class FleetTrackLatestCmd extends Command {
'$ balena fleet track-latest myfleet', '$ balena fleet track-latest myfleet',
]; ];
public static args = { public static args: Array<IArg<any>> = [
slug: Args.string({ {
name: 'slug',
description: 'the slug of the fleet to make track the latest release', description: 'the slug of the fleet to make track the latest release',
required: true, required: true,
}), },
];
public static usage = 'fleet track-latest <slug>';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
}; };
public static authenticated = true; public static authenticated = true;
public async run() { public async run() {
const { args: params } = await this.parse(FleetTrackLatestCmd); const { args: params } = this.parse<FlagsDef, ArgsDef>(FleetTrackLatestCmd);
const balena = getBalenaSdk(); const balena = getBalenaSdk();

94
lib/commands/fleets.ts Normal file
View File

@ -0,0 +1,94 @@
/**
* @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 } from '@oclif/command';
import Command from '../command';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../utils/lazy';
import type { DataSetOutputOptions } from '../framework';
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.
List all your balena fleets.
For detailed information on a particular fleet, use
\`balena fleet <fleet>\`
`;
public static examples = ['$ balena fleets'];
public static usage = 'fleets';
public static flags: flags.Input<FlagsDef> = {
...cf.dataSetOutputFlags,
help: cf.help,
};
public static authenticated = true;
public static primary = true;
public async run() {
const { flags: options } = this.parse<FlagsDef, {}>(FleetsCmd);
const balena = getBalenaSdk();
// Get applications
const applications =
(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) => {
application.device_count = application.owns__device?.length ?? 0;
application.online_devices =
application.owns__device?.filter((d) => d.is_online).length || 0;
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,
);
}
}

View File

@ -15,8 +15,9 @@
* limitations under the License. * limitations under the License.
*/ */
import { Args, Command } from '@oclif/core'; import Command from '../../command';
import { stripIndent } from '../../utils/lazy'; import { stripIndent } from '../../utils/lazy';
import { CommandHelp } from '../../utils/oclif-utils';
// 'Internal' commands are called during the execution of other commands. // 'Internal' commands are called during the execution of other commands.
// `osinit` is called during `os initialize` // `osinit` is called during `os initialize`
@ -26,6 +27,12 @@ import { stripIndent } from '../../utils/lazy';
// - https://github.com/balena-io/balena-cli/pull/1455#discussion_r334308357 // - https://github.com/balena-io/balena-cli/pull/1455#discussion_r334308357
// - https://github.com/balena-io/balena-cli/pull/1455#discussion_r334308526 // - 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 { export default class OsinitCmd extends Command {
public static description = stripIndent` public static description = stripIndent`
Do actual init of the device with the preconfigured os image. Do actual init of the device with the preconfigured os image.
@ -34,24 +41,32 @@ export default class OsinitCmd extends Command {
Use \`balena os initialize <image>\` instead. Use \`balena os initialize <image>\` instead.
`; `;
public static args = { public static args = [
image: Args.string({ {
name: 'image',
required: true, required: true,
}), },
type: Args.string({ {
name: 'type',
required: true, required: true,
}), },
config: Args.string({ {
name: 'config',
required: true, required: true,
}), },
}; ];
public static usage = (
'internal osinit ' +
new CommandHelp({ args: OsinitCmd.args }).defaultUsage()
).trim();
public static hidden = true; public static hidden = true;
public static root = true; public static root = true;
public static offlineCompatible = true; public static offlineCompatible = true;
public async run() { public async run() {
const { args: params } = await this.parse(OsinitCmd); const { args: params } = this.parse<{}, ArgsDef>(OsinitCmd);
const config = JSON.parse(params.config); const config = JSON.parse(params.config);

View File

@ -15,11 +15,22 @@
* limitations under the License. * limitations under the License.
*/ */
import { Args, Flags, Command } from '@oclif/core'; import { flags } from '@oclif/command';
import * as cf from '../../utils/common-flags'; import Command from '../command';
import { getBalenaSdk, stripIndent } from '../../utils/lazy'; import * as cf from '../utils/common-flags';
import { applicationIdInfo } from '../../utils/messages'; import { getBalenaSdk, stripIndent } from '../utils/lazy';
import { parseAsLocalHostnameOrIp } from '../../utils/validation'; 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 { export default class JoinCmd extends Command {
public static description = stripIndent` public static description = stripIndent`
@ -52,31 +63,37 @@ export default class JoinCmd extends Command {
'$ balena join 192.168.1.25 --fleet MyFleet', '$ balena join 192.168.1.25 --fleet MyFleet',
]; ];
public static args = { public static args = [
deviceIpOrHostname: Args.string({ {
name: 'deviceIpOrHostname',
description: 'the IP or hostname of device', description: 'the IP or hostname of device',
parse: parseAsLocalHostnameOrIp, parse: parseAsLocalHostnameOrIp,
}), },
}; ];
public static flags = { // Hardcoded to preserve camelcase
public static usage = 'join [deviceIpOrHostname]';
public static flags: flags.Input<FlagsDef> = {
fleet: cf.fleet, fleet: cf.fleet,
pollInterval: Flags.integer({ pollInterval: flags.integer({
description: 'the interval in minutes to check for updates', description: 'the interval in minutes to check for updates',
char: 'i', char: 'i',
}), }),
help: cf.help,
}; };
public static authenticated = true; public static authenticated = true;
public static primary = true; public static primary = true;
public async run() { 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 promote = await import('../utils/promote');
const sdk = getBalenaSdk(); const sdk = getBalenaSdk();
const Logger = await import('../../utils/logger'); const logger = await Command.getLogger();
const logger = Logger.getLogger();
return promote.join( return promote.join(
logger, logger,
sdk, sdk,

View File

@ -15,13 +15,22 @@
* limitations under the License. * limitations under the License.
*/ */
import { Args, Command } 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'; import { getBalenaSdk, stripIndent } from '../../utils/lazy';
export default class SSHKeyAddCmd extends Command { interface FlagsDef {
public static aliases = ['key add']; help: void;
public static deprecateAliases = true; }
interface ArgsDef {
name: string;
path: string;
}
export default class KeyAddCmd extends Command {
public static description = stripIndent` public static description = stripIndent`
Add an SSH key to balenaCloud. Add an SSH key to balenaCloud.
@ -45,34 +54,45 @@ export default class SSHKeyAddCmd extends Command {
`; `;
public static examples = [ public static examples = [
'$ balena ssh-key add Main ~/.ssh/id_rsa.pub', '$ balena key add Main ~/.ssh/id_rsa.pub',
'$ cat ~/.ssh/id_rsa.pub | balena ssh-key add Main', '$ cat ~/.ssh/id_rsa.pub | balena key add Main',
'# Windows 10 (cmd.exe prompt) example', '# Windows 10 (cmd.exe prompt) example',
'$ balena ssh-key add Main %userprofile%.sshid_rsa.pub', '$ balena key add Main %userprofile%.sshid_rsa.pub',
]; ];
public static args = { public static args = [
name: Args.string({ {
name: 'name',
description: 'the SSH key name', description: 'the SSH key name',
required: true, required: true,
}), },
path: Args.string({ {
name: `path`,
description: `the path to the public key file`, description: `the path to the public key file`,
required: true, },
}), ];
public static usage = 'key add <name> [path]';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
}; };
public static authenticated = true; public static authenticated = true;
public async run() { public static readStdin = true;
const { args: params } = await this.parse(SSHKeyAddCmd);
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(KeyAddCmd);
const { readFile } = (await import('fs')).promises;
let key: string; let key: string;
try { if (params.path != null) {
const { readFile } = (await import('fs')).promises;
key = await readFile(params.path, 'utf8'); key = await readFile(params.path, 'utf8');
} catch { } else if (this.stdin.length > 0) {
key = params.path; key = this.stdin;
} else {
throw new ExpectedError('No public key file or path provided.');
} }
await getBalenaSdk().models.key.create(params.name, key); await getBalenaSdk().models.key.create(params.name, key);

View File

@ -15,34 +15,50 @@
* limitations under the License. * limitations under the License.
*/ */
import { Args, Command } 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 { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
import { parseAsInteger } from '../../utils/validation'; import { parseAsInteger } from '../../utils/validation';
export default class SSHKeyCmd extends Command { type IArg<T> = import('@oclif/parser').args.IArg<T>;
public static aliases = ['key'];
public static deprecateAliases = true;
interface FlagsDef {
help: void;
}
interface ArgsDef {
id: number;
}
export default class KeyCmd extends Command {
public static description = stripIndent` public static description = stripIndent`
Display an SSH key. Display an SSH key.
Display a single SSH key registered in balenaCloud for the logged in user. Display a single SSH key registered in balenaCloud for the logged in user.
`; `;
public static examples = ['$ balena ssh-key 17']; public static examples = ['$ balena key 17'];
public static args = { public static args: Array<IArg<any>> = [
id: Args.integer({ {
name: 'id',
description: 'balenaCloud ID for the SSH key', description: 'balenaCloud ID for the SSH key',
parse: (x) => parseAsInteger(x, 'id'), parse: (x) => parseAsInteger(x, 'id'),
required: true, required: true,
}), },
];
public static usage = 'key <id>';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
}; };
public static authenticated = true; public static authenticated = true;
public async run() { public async run() {
const { args: params } = await this.parse(SSHKeyCmd); const { args: params } = this.parse<{}, ArgsDef>(KeyCmd);
const key = await getBalenaSdk().models.key.get(params.id); const key = await getBalenaSdk().models.key.get(params.id);

View File

@ -15,15 +15,24 @@
* limitations under the License. * limitations under the License.
*/ */
import { Args, Command } from '@oclif/core'; import { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags'; import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy'; import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { parseAsInteger } from '../../utils/validation'; import { parseAsInteger } from '../../utils/validation';
export default class SSHKeyRmCmd extends Command { type IArg<T> = import('@oclif/parser').args.IArg<T>;
public static aliases = ['key rm'];
public static deprecateAliases = true;
interface FlagsDef {
yes: boolean;
help: void;
}
interface ArgsDef {
id: number;
}
export default class KeyRmCmd extends Command {
public static description = stripIndent` public static description = stripIndent`
Remove an SSH key from balenaCloud. Remove an SSH key from balenaCloud.
@ -32,27 +41,30 @@ export default class SSHKeyRmCmd extends Command {
The --yes option may be used to avoid interactive confirmation. The --yes option may be used to avoid interactive confirmation.
`; `;
public static examples = [ public static examples = ['$ balena key rm 17', '$ balena key rm 17 --yes'];
'$ balena ssh-key rm 17',
'$ balena ssh-key rm 17 --yes',
];
public static args = { public static args: Array<IArg<any>> = [
id: Args.integer({ {
name: 'id',
description: 'balenaCloud ID for the SSH key', description: 'balenaCloud ID for the SSH key',
parse: (x) => parseAsInteger(x, 'id'), parse: (x) => parseAsInteger(x, 'id'),
required: true, required: true,
}), },
}; ];
public static flags = { public static usage = 'key rm <id>';
public static flags: flags.Input<FlagsDef> = {
yes: cf.yes, yes: cf.yes,
help: cf.help,
}; };
public static authenticated = true; public static authenticated = true;
public async run() { public async run() {
const { args: params, flags: options } = await this.parse(SSHKeyRmCmd); const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
KeyRmCmd,
);
const patterns = await import('../../utils/patterns'); const patterns = await import('../../utils/patterns');

View File

@ -15,24 +15,33 @@
* limitations under the License. * limitations under the License.
*/ */
import { Command } from '@oclif/core'; import { flags } from '@oclif/command';
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy'; import Command from '../command';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
export default class SSHKeyListCmd extends Command { interface FlagsDef {
public static aliases = ['keys', 'key list']; help: void;
public static deprecateAliases = true; }
export default class KeysCmd extends Command {
public static description = stripIndent` public static description = stripIndent`
List the SSH keys in balenaCloud. List the SSH keys in balenaCloud.
List all SSH keys registered in balenaCloud for the logged in user. List all SSH keys registered in balenaCloud for the logged in user.
`; `;
public static examples = ['$ balena ssh-key list']; public static examples = ['$ balena keys'];
public static usage = 'keys';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static authenticated = true; public static authenticated = true;
public async run() { public async run() {
await this.parse(SSHKeyListCmd); this.parse<FlagsDef, {}>(KeysCmd);
const keys = await getBalenaSdk().models.key.getAll(); const keys = await getBalenaSdk().models.key.getAll();

View File

@ -15,9 +15,19 @@
* limitations under the License. * limitations under the License.
*/ */
import { Args, Command } from '@oclif/core'; import { flags } from '@oclif/command';
import { stripIndent } from '../../utils/lazy'; import Command from '../command';
import { parseAsLocalHostnameOrIp } from '../../utils/validation'; 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 { export default class LeaveCmd extends Command {
public static description = stripIndent` public static description = stripIndent`
@ -41,22 +51,28 @@ export default class LeaveCmd extends Command {
'$ balena leave 192.168.1.25', '$ balena leave 192.168.1.25',
]; ];
public static args = { public static args = [
deviceIpOrHostname: Args.string({ {
name: 'deviceIpOrHostname',
description: 'the device IP or hostname', description: 'the device IP or hostname',
parse: parseAsLocalHostnameOrIp, parse: parseAsLocalHostnameOrIp,
}), },
];
public static usage = 'leave [deviceIpOrHostname]';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
}; };
public static authenticated = true; public static authenticated = true;
public static primary = true; public static primary = true;
public async run() { 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 promote = await import('../utils/promote');
const Logger = await import('../../utils/logger'); const logger = await Command.getLogger();
const logger = Logger.getLogger();
return promote.leave(logger, params.deviceIpOrHostname); return promote.leave(logger, params.deviceIpOrHostname);
} }
} }

View File

@ -15,9 +15,20 @@
* limitations under the License. * limitations under the License.
*/ */
import { Args, Command } 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'; import { stripIndent } from '../../utils/lazy';
interface FlagsDef {
help: void;
}
interface ArgsDef {
target: string;
}
export default class LocalConfigureCmd extends Command { export default class LocalConfigureCmd extends Command {
public static description = stripIndent` public static description = stripIndent`
(Re)configure a balenaOS drive or image. (Re)configure a balenaOS drive or image.
@ -30,18 +41,25 @@ export default class LocalConfigureCmd extends Command {
'$ balena local configure path/to/image.img', '$ balena local configure path/to/image.img',
]; ];
public static args = { public static args = [
target: Args.string({ {
name: 'target',
description: 'path of drive or image to configure', description: 'path of drive or image to configure',
required: true, required: true,
}), },
];
public static usage = 'local configure <target>';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
}; };
public static root = true; public static root = true;
public static offlineCompatible = true; public static offlineCompatible = true;
public async run() { public async run() {
const { args: params } = await this.parse(LocalConfigureCmd); const { args: params } = this.parse<FlagsDef, ArgsDef>(LocalConfigureCmd);
const reconfix = await import('reconfix'); const reconfix = await import('reconfix');
const { denyMount, safeUmount } = await import('../../utils/umount'); const { denyMount, safeUmount } = await import('../../utils/umount');
@ -236,7 +254,7 @@ export default class LocalConfigureCmd extends Command {
const bootPartition = await getBootPartition(target); const bootPartition = await getBootPartition(target);
const files = await imagefs.interact(target, bootPartition, async (_fs) => { const files = await imagefs.interact(target, bootPartition, async (_fs) => {
return await _fs.promises.readdir(this.CONNECTIONS_FOLDER); return await promisify(_fs.readdir)(this.CONNECTIONS_FOLDER);
}); });
let connectionFileName; let connectionFileName;
@ -245,11 +263,13 @@ export default class LocalConfigureCmd extends Command {
} else if (_.includes(files, 'resin-sample.ignore')) { } else if (_.includes(files, 'resin-sample.ignore')) {
// Fresh image, new mode, accoding to https://github.com/balena-os/meta-balena/pull/770/files // Fresh image, new mode, accoding to https://github.com/balena-os/meta-balena/pull/770/files
await imagefs.interact(target, bootPartition, async (_fs) => { await imagefs.interact(target, bootPartition, async (_fs) => {
const contents = await _fs.promises.readFile( const readFileAsync = promisify(_fs.readFile);
const writeFileAsync = promisify(_fs.writeFile);
const contents = await readFileAsync(
`${this.CONNECTIONS_FOLDER}/resin-sample.ignore`, `${this.CONNECTIONS_FOLDER}/resin-sample.ignore`,
{ encoding: 'utf8' }, { encoding: 'utf8' },
); );
await _fs.promises.writeFile( return await writeFileAsync(
`${this.CONNECTIONS_FOLDER}/resin-wifi`, `${this.CONNECTIONS_FOLDER}/resin-wifi`,
contents, contents,
); );
@ -266,13 +286,13 @@ export default class LocalConfigureCmd extends Command {
} else { } else {
// In case there's no file at all (shouldn't happen normally, but the file might have been removed) // In case there's no file at all (shouldn't happen normally, but the file might have been removed)
await imagefs.interact(target, bootPartition, async (_fs) => { await imagefs.interact(target, bootPartition, async (_fs) => {
await _fs.promises.writeFile( return await promisify(_fs.writeFile)(
`${this.CONNECTIONS_FOLDER}/resin-wifi`, `${this.CONNECTIONS_FOLDER}/resin-wifi`,
this.CONNECTION_FILE, this.CONNECTION_FILE,
); );
}); });
} }
return this.getConfigurationSchema(bootPartition, connectionFileName); return await this.getConfigurationSchema(bootPartition, connectionFileName);
} }
async removeHostname(schema: any) { async removeHostname(schema: any) {

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