Compare commits

..

1 Commits

Author SHA1 Message Date
625f5c8dcc Revert download-artifact action to v4.1.3
* attempts to correct empty GH releases being published

change-type: patch
2024-07-01 11:48:35 -07:00
272 changed files with 20151 additions and 18201 deletions

2
.eslintignore Normal file
View File

@ -0,0 +1,2 @@
/completion/*
/bin/*

21
.eslintrc.js Normal file
View File

@ -0,0 +1,21 @@
module.exports = {
extends: ['./node_modules/@balena/lint/config/.eslintrc.js'],
parserOptions: {
project: 'tsconfig.dev.json',
},
root: true,
rules: {
ignoreDefinitionFiles: 0,
// to avoid the `warning Forbidden non-null assertion @typescript-eslint/no-non-null-assertion`
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-shadow': 'off',
'@typescript-eslint/no-var-requires': 'off',
'no-restricted-imports': [
'error',
{
paths: ['resin-cli-visuals', 'chalk', 'common-tags', 'resin-cli-form'],
},
],
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
},
};

View File

@ -67,7 +67,7 @@ fixed it.
- **Cloud backend: openBalena or balenaCloud?** If unsure, it will be balenaCloud
- **Operating system version:** e.g. Windows 10, Ubuntu 18.04, macOS 10.14.5
- **32/64 bit OS and processor:** e.g. 32-bit Windows on 64-bit Intel processor
- **Install method:** npm or standalone package or executable installer
- **Install method:** npm or zip package or executable installer
- **If npm install, Node.js and npm version:** e.g. Node v8.16.0 and npm v6.4.1
# Additional References

View File

@ -18,7 +18,7 @@ inputs:
default: 'accounts+apple@balena.io'
NODE_VERSION:
type: string
default: '22.x'
default: '20.x'
VERBOSE:
type: string
default: 'true'
@ -28,7 +28,7 @@ runs:
using: 'composite'
steps:
- name: Download custom source artifact
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9
uses: actions/download-artifact@87c55149d96e628cc2ef7e6fc2aab372015aec85 # v4.1.3
with:
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}-${{ runner.arch }}
path: ${{ runner.temp }}
@ -39,7 +39,7 @@ runs:
run: tar -xf ${{ runner.temp }}/custom.tgz
- name: Setup Node.js
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3
with:
node-version: ${{ inputs.NODE_VERSION }}
cache: npm
@ -48,7 +48,7 @@ runs:
if: runner.os == 'macOS'
uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4
with:
python-version: '3.11'
python-version: "3.11"
- name: Install additional tools
if: runner.os == 'Windows'
@ -66,7 +66,7 @@ runs:
# 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
uses: apple-actions/import-codesign-certs@253ddeeac23f2bdad1646faac5c8c2832e800071 # v1
with:
p12-file-base64: ${{ fromJSON(inputs.secrets).APPLE_SIGNING }}
p12-password: ${{ fromJSON(inputs.secrets).APPLE_SIGNING_PASSWORD }}
@ -94,7 +94,7 @@ runs:
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_KEY_PASSWORD=${{ fromJSON(inputs.secrets).APPLE_SIGNING_PASSWORD }}
CSC_KEYCHAIN=signing_temp
CSC_LINK=${{ fromJSON(inputs.secrets).APPLE_SIGNING }}
@ -112,8 +112,8 @@ runs:
PATH="/c/Program Files/DigiCert/DigiCert One Signing Manager Tools:${PATH}"
smksp_registrar.exe list
smctl.exe keypair ls
smctl.exe windows certsync
/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}"
@ -135,11 +135,9 @@ runs:
XCODE_APP_LOADER_TEAM_ID: ${{ inputs.XCODE_APP_LOADER_TEAM_ID }}
- name: Upload artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4
with:
name: gh-release-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ strategy.job-index }}
path: |
dist
!dist/balena
path: dist
retention-days: 1
if-no-files-found: error

View File

@ -15,7 +15,7 @@ inputs:
# --- custom environment
NODE_VERSION:
type: string
default: '22.x'
default: '20.x'
VERBOSE:
type: string
default: "true"
@ -26,7 +26,7 @@ runs:
steps:
# https://github.com/actions/setup-node#caching-global-packages-data
- name: Setup Node.js
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3
with:
node-version: ${{ inputs.NODE_VERSION }}
cache: npm
@ -58,7 +58,7 @@ runs:
run: tar --exclude-vcs -acf ${{ runner.temp }}/custom.tgz .
- name: Upload custom artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4
with:
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}-${{ runner.arch }}
path: ${{ runner.temp }}/custom.tgz

View File

@ -24,9 +24,9 @@ jobs:
custom_test_matrix: >
{
"os": [
["self-hosted", "X64"],
["self-hosted", "ARM64"],
["macos-13"],
["actuated-4cpu-8gb"],
["actuated-arm64-4cpu-8gb"],
["macos-12"],
["windows-2019"],
["macos-latest-xlarge"]
]
@ -34,9 +34,9 @@ jobs:
custom_publish_matrix: >
{
"os": [
["self-hosted", "X64"],
["self-hosted", "ARM64"],
["macos-13"],
["actuated-4cpu-8gb"],
["actuated-arm64-4cpu-8gb"],
["macos-12"],
["windows-2019"],
["macos-latest-xlarge"]
]

View File

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

View File

@ -2,7 +2,7 @@ module.exports = {
reporter: 'spec',
require: 'ts-node/register/transpile-only',
file: './tests/config-tests',
timeout: 48000,
timeout: 12000,
// To test only, say, 'push.spec.ts', do it as follows so that
// requests are authenticated:
// spec: ['tests/auth/*.spec.ts', 'tests/**/deploy.spec.ts'],

File diff suppressed because it is too large Load Diff

View File

@ -4,983 +4,6 @@ All notable changes to this project will be documented in this file
automatically by Versionist. DO NOT EDIT THIS FILE MANUALLY!
This project adheres to [Semantic Versioning](http://semver.org/).
## 22.1.1 - 2025-06-19
* Deploy: Limit the submitted error_message of images that fail to build to 1000 characters [Thodoris Greasidis]
## 22.1.0 - 2025-06-09
* Add support for node 22 [Otavio Jacobi]
<details>
<summary> Bump etcher-sdk to v10.0.0 [Otavio Jacobi] </summary>
> ### etcher-sdk-10.0.0 - 2025-06-02
>
> * Drop support to node18 and add support to node 22 & 24 [Otavio Jacobi]
>
</details>
## 22.0.6 - 2025-06-02
* Remove `request` dependency [myarmolinsky]
* Replace `request` usage with `got` [myarmolinsky]
## 22.0.5 - 2025-05-29
<details>
<summary> Bump etcher-sdk to v9.1.4 [Otavio Jacobi] </summary>
> ### etcher-sdk-9.1.4 - 2025-05-29
>
> * Run `npm audit fix` which should only do non-breaking changes [Otavio Jacobi]
>
> ### etcher-sdk-9.1.3 - 2025-02-17
>
> * Embed config.json with a fixed timestamp to enable consistent checksums [Pagan Gazzard]
>
> ### etcher-sdk-9.1.2 - 2024-10-09
>
> * Update dependency unzip-stream to v0.3.2 [SECURITY] [Self-hosted Renovate Bot]
>
> ### etcher-sdk-9.1.1 - 2024-10-09
>
> * patch: add EXLOCK flag for windows [Talha Can Havadar]
>
</details>
## 22.0.4 - 2025-05-29
* tests: Replace request with got [Otavio Jacobi]
* deploy-legacy: Replace request with got [Otavio Jacobi]
## 22.0.3 - 2025-05-29
* Bump sentry to v9 [Otavio Jacobi]
## 22.0.2 - 2025-05-28
* Fix balena build to work with --nologs [Otavio Jacobi]
## 22.0.1 - 2025-05-28
* DeviceAPI: Move away from `request` in favor of BalenaSdk request [myarmolinsky]
* Update `nock` to 14.0.4 [myarmolinsky]
## 22.0.0 - 2025-05-26
* Add migration guide to v22 [Otavio Jacobi]
* Build standalone without pkg [Otavio Jacobi]
## 21.1.14 - 2025-05-21
<details>
<summary> Bump balena-preload to 18.0.4 [Otavio Jacobi] </summary>
> ### balena-preload-18.0.4 - 2025-05-21
>
> * Fix balena-preload pip deps [Otavio Jacobi]
>
</details>
## 21.1.13 - 2025-05-21
<details>
<summary> Bump balena-preload to 18.0.3 [Otavio Jacobi] </summary>
> ### balena-preload-18.0.3 - 2025-03-19
>
> * Update dependency sh to v1.14.3 [balena-renovate[bot]]
>
> ### balena-preload-18.0.2 - 2025-03-19
>
> * Update alpine Docker tag to v3.21 [balena-renovate[bot]]
>
</details>
## 21.1.12 - 2025-05-16
* Update @balena/compose dependency to fix error with build secrets. [Ken Bannister]
## 21.1.11 - 2025-05-06
* Fix typos in `os configure` and `config generate` help messages [myarmolinsky]
## 21.1.10 - 2025-05-05
* patch: fix windows signing [Edwin Joassart]
## 21.1.9 - 2025-04-07
<details>
<summary> Update balena-config-json to rely on the balena-image-fs helpers [Thodoris Greasidis] </summary>
> ### balena-config-json-4.2.7 - 2025-04-02
>
> * Fix getBootPartition always warning that the boot partitions were not found [Thodoris Greasidis]
>
> ### balena-config-json-4.2.6 - 2025-04-01
>
> * write: Allow undefined as a value for the deprecated type parameter [Thodoris Greasidis]
>
> <details>
> <summary> Use the balena-image-fs findPartition() helper to find the boot partition [Thodoris Greasidis] </summary>
>
>> #### balena-image-fs-7.5.0 - 2025-03-26
>>
>> * Add function to find a partition by name/label [Ken Bannister]
>>
>> #### balena-image-fs-7.4.1 - 2025-02-21
>>
>> * bump ext2fs to 4.2.4 [Ryan Cooke]
>>
>
> </details>
>
>
> ### balena-config-json-4.2.5 - 2025-03-26
>
> * Update @balena/lint to 9.1.4 [Thodoris Greasidis]
>
> ### balena-config-json-4.2.4 - 2025-03-26
>
> * Switch use ts-mocha instead of manually compiling the tests [Thodoris Greasidis]
> * Update TypeScript to 5.8.2 [Thodoris Greasidis]
>
> ### balena-config-json-4.2.3 - 2025-03-26
>
> * Update chai to v5 [balena-renovate[bot]]
>
> ### balena-image-fs-7.5.2 - 2025-04-02
>
> * Update dependency jsdoc-to-markdown to v9 [balena-renovate[bot]]
>
> ### balena-image-fs-7.5.1 - 2025-03-26
>
> * Update @balena/lint to v9 [Thodoris Greasidis]
> * Remove the no longer needed gpt detection helper [Thodoris Greasidis]
> * Update TypeScript to 5.8.2 [Thodoris Greasidis]
> * Drop the package-lock.json [Thodoris Greasidis]
>
</details>
## 21.1.8 - 2025-04-03
* Update actions/download-artifact action to v4.1.9 [balena-renovate[bot]]
## 21.1.7 - 2025-04-03
* Update actions/upload-artifact digest to ea165f8 [balena-renovate[bot]]
## 21.1.6 - 2025-04-03
* Update actions/setup-node digest to cdca736 [balena-renovate[bot]]
## 21.1.5 - 2025-04-03
* Update dockerode/docker-modem dependencies for fixes [Ken Bannister]
## 21.1.4 - 2025-04-02
* Add comment with secure boot signature file example for preload [Ken Bannister]
## 21.1.3 - 2025-03-28
* Fix device detail for open balena [Otavio Jacobi]
## 21.1.2 - 2025-03-27
* Deny preload for an image with secure boot enabled [Ken Bannister]
## 21.1.1 - 2025-03-26
<details>
<summary> Bump balena-sdk to 21.3.0 [Otavio Jacobi] </summary>
> ### balena-sdk-21.3.0 - 2025-03-26
>
> * device: add `changed_api_heartbeat_state_on__date` to typings [Otavio Jacobi]
>
> ### balena-sdk-21.2.2 - 2025-03-26
>
> * fix linting [Otavio Jacobi]
>
</details>
## 21.1.0 - 2025-03-12
* Add support for new requirement labels feature [Felipe Lalanne]
## 21.0.0 - 2025-03-11
* Drop support for OS versions <2.14.0 [myarmolinsky]
* api-key generate: Add required argument `expiryDate` [myarmolinsky]
* Update `balena-preload` to 18.0.1 [myarmolinsky]
* Add dependency `date-fns` [myarmolinsky]
* Update `balena-sdk` to 21.2.1 [myarmolinsky]
## 20.2.10 - 2025-03-10
* Update TypeScript to 5.8.2 [Thodoris Greasidis]
## 20.2.9 - 2025-02-26
* Fix CORS issue with X-Balena-Client header [Thodoris Greasidis]
## 20.2.8 - 2025-02-26
* Update balena-config-json dependency and fix test [Ken Bannister]
## 20.2.7 - 2025-02-25
* Use the CLI version in the X-Balena-Client header [Thodoris Greasidis]
## 20.2.6 - 2025-02-25
* Update actions/upload-artifact digest to 4cec3d8 [balena-renovate[bot]]
## 20.2.5 - 2025-02-25
* Update actions/setup-node digest to 1d0ff46 [balena-renovate[bot]]
## 20.2.4 - 2025-02-25
* Pin docker-modem and dockerode to avoid regression [Ken Bannister]
## 20.2.3 - 2025-01-15
* Remove unused old eslint version files [Otavio Jacobi]
## 20.2.2 - 2025-01-12
* Use the promises namespace of balena-image-fs [Thodoris Greasidis]
<details>
<summary> Update balena-device-init to 8.1.3 & balena-image-fs to 7.3.0 [Thodoris Greasidis] </summary>
> ### balena-image-fs-7.3.0 - 2025-01-06
>
> * Drop Bluebird from devDependencies [Thodoris Greasidis]
> * flowzone: Remove empty with clause [Thodoris Greasidis]
> * Add the promises namespace as part of the exposed fs [Thodoris Greasidis]
>
> ### balena-image-fs-7.2.2 - 2024-01-02
>
> * Update dependency @types/node to v20 [Self-hosted Renovate Bot]
>
> ### balena-image-fs-7.2.1 - 2023-12-19
>
> * Remove repo config from flowzone.yml [Kyle Harding]
>
> ### balena-image-fs-7.2.0 - 2023-01-20
>
> * Add support for Node 18 [Akis Kesoglou]
>
> ### balena-image-fs-7.1.2 - 2023-01-05
>
> * Update dependencies [ab77]
>
> ### balena-image-fs-7.1.1 - 2022-12-20
>
> * Update dependency jsdoc-to-markdown to 8.0.0 [Renovate Bot]
>
> ### balena-image-fs-7.1.0 - 2022-12-13
>
> * update dependencies [Zane Hitchcox]
>
> ### balena-device-init-8.1.3 - 2025-01-09
>
> * README: Remove the travisci badge [Thodoris Greasidis]
>
> ### balena-device-init-8.1.2 - 2025-01-09
>
>
> <details>
> <summary> Use the promises namespace of balena-image-fs [Thodoris Greasidis] </summary>
>
>> #### balena-image-fs-7.3.0 - 2025-01-06
>>
>> * Drop Bluebird from devDependencies [Thodoris Greasidis]
>> * flowzone: Remove empty with clause [Thodoris Greasidis]
>> * Add the promises namespace as part of the exposed fs [Thodoris Greasidis]
>>
>> #### balena-image-fs-7.2.2 - 2024-01-02
>>
>> * Update dependency @types/node to v20 [Self-hosted Renovate Bot]
>>
>> #### balena-image-fs-7.2.1 - 2023-12-19
>>
>> * Remove repo config from flowzone.yml [Kyle Harding]
>>
>> #### balena-image-fs-7.2.0 - 2023-01-20
>>
>> * Add support for Node 18 [Akis Kesoglou]
>>
>> #### balena-image-fs-7.1.2 - 2023-01-05
>>
>> * Update dependencies [ab77]
>>
>> #### balena-image-fs-7.1.1 - 2022-12-20
>>
>> * Update dependency jsdoc-to-markdown to 8.0.0 [Renovate Bot]
>>
>> #### balena-image-fs-7.1.0 - 2022-12-13
>>
>> * update dependencies [Zane Hitchcox]
>>
>
> </details>
>
>
> ### balena-device-init-8.1.1 - 2025-01-06
>
> * Convert some parts to async await and simplify [Thodoris Greasidis]
> * Avoid unnecessary destructuring [Thodoris Greasidis]
>
</details>
## 20.2.1 - 2025-01-01
<details>
<summary> Update balena-preload to 17.0.0 [Thodoris Greasidis] </summary>
> ### balena-preload-17.0.0 - 2024-10-21
>
> * Improve typings [Thodoris Greasidis]
> * Stop returning Bluebird promises & drop it from the dependencies [Thodoris Greasidis]
>
</details>
## 20.2.0 - 2024-12-31
<details>
<summary> os configure: Give precedence to the boot partition located in the image over the device-type.json contents [Thodoris Greasidis] </summary>
> ### balena-device-init-8.1.0 - Invalid date
>
> * Try to find the boot partition by inspecting the image [Thodoris Greasidis]
>
> ### balena-device-init-8.0.1 - 2024-12-19
>
> * Drop the unnecessary eslint.config.js [Thodoris Greasidis]
> * packacke.json: Explicitly set type commonjs [Thodoris Greasidis]
>
</details>
## 20.1.6 - 2024-12-30
* Add more realistic os configure tests [Thodoris Greasidis]
## 20.1.5 - 2024-12-20
* Update shrinkwrapped express to v4.21.2 [Oskar Williams]
## 20.1.4 - 2024-12-20
<details>
<summary> Update balena-device-init to 8.0.0 [Thodoris Greasidis] </summary>
> ### balena-device-init-8.0.0 - 2024-12-18
>
> * Avoid running linting in the custom tests [Thodoris Greasidis]
> * Stop returning Bluebird promises [Thodoris Greasidis]
> * package: Publish only the build & typings folders [Thodoris Greasidis]
> * Convert to TypeScript with es6 module notation [Thodoris Greasidis]
> * Replace gulp, coffeelint & mochainon with tsc, @balena/lint, mocha, chai & sinon [Thodoris Greasidis]
> * Drop support for node <20.6.0 [Thodoris Greasidis]
>
> ### balena-device-init-7.0.2 - 2024-12-17
>
> * flowzone: Update runner versions [Thodoris Greasidis]
> * Pin etcher-sdk to 9.0.8 to match resin-device-operations and fix tests [Thodoris Greasidis]
>
</details>
## 20.1.3 - 2024-12-20
* Update oclif to 4.17.0 and @oclif/core 4.1.0 [Otavio Jacobi]
## 20.1.2 - 2024-12-17
* Remove unnecessary `Promise.resolve` and `Promise.reject` [Pagan Gazzard]
## 20.1.1 - 2024-12-16
* Update @balena/lint to v9.1.3 [Otavio Jacobi]
## 20.1.0 - 2024-12-12
* `device os-update`: Add handling for updates that require takeover [myarmolinsky]
* Update `balena-sdk` [myarmolinsky]
* Update `@balena/compose` [myarmolinsky]
## 20.0.9 - 2024-12-05
* Update shrinkwrapped express to v4.21.1 [Oskar Williams]
## 20.0.8 - 2024-12-04
* Run test and publish with macos-13 [Otavio Jacobi]
## 20.0.7 - 2024-11-23
* Update TypeScript to 5.7.2 [Thodoris Greasidis]
## 20.0.6 - 2024-11-08
* Refactor balena build for clarity [Thodoris Greasidis]
## 20.0.5 - 2024-11-05
* Update actions/upload-artifact digest to b4b15b8 [balena-renovate[bot]]
## 20.0.4 - 2024-11-05
* Update actions/setup-node digest to 39370e3 [balena-renovate[bot]]
## 20.0.3 - 2024-11-05
* api-key generate: Display a descriptive error when the generation fails due to a stale JWT [Thodoris Greasidis]
## 20.0.2 - 2024-10-29
* Restore ability to cat key into `ssh-key add` [myarmolinsky]
## 20.0.1 - 2024-10-29
* Fix sending input to some aliases not working [myarmolinsky]
## 20.0.0 - 2024-10-25
* `device update`: Use detached HUP for os updates [jaomaloy]
* Drop `-h` flag for help and stop manually adding `help` per command in favor of oclif automatically adding it [myarmolinsky]
* Stop checking for very old, long-removed commands [myarmolinsky]
* Deprecate `devices supported` command in favor of `device-type list` [myarmolinsky]
* Deprecate `notes` command in favor of `device note` [myarmolinsky]
* Deprecate `tunnel` command in favor of `device tunnel` [myarmolinsky]
* Deprecate `env add` in favor of `env set` [myarmolinsky]
* Deprecate `ssh` command in favor of `device ssh` [myarmolinsky]
* Deprecate `logs` command in favor of `device logs` [myarmolinsky]
* Deprecate `scan` command in favor of `device detect` [myarmolinsky]
* Deprecate `orgs` command in favor of `organization list` [myarmolinsky]
* Deprecate `tags` command in favor of `tag list` [myarmolinsky]
* Deprecate `envs` command in favor of `env list` [myarmolinsky]
* Deprecate `key` commands in favor of `ssh-key` [myarmolinsky]
* Deprecate `keys` command in favor of `key list` [myarmolinsky]
* Deprecate `releases` command in favor of `release list` [myarmolinsky]
* Deprecate `fleets` command in favor of `fleet list` [myarmolinsky]
* Deprecate `api-keys` command in favor of `api-key list` [myarmolinsky]
* Deprecate `devices` command in favor of `device list` [myarmolinsky]
* Docs: Show whether an alias is deprecated [myarmolinsky]
* Update `balena-preload` to 16.0.0 [myarmolinsky]
* Tests: Drop unused `my_application` resource mock [myarmolinsky]
* Rename `device.overall_status` values: `IDLE` to `OPERATIONAL`, `OFFLINE` to `DISCONNECTED`; add `REDUCED_FUNCTIONALITY` [myarmolinsky]
* Update `@balena/compose` to 5.0.0 [myarmolinsky]
* Bump balena-sdk to 20.3.0 [myarmolinsky]
## 19.16.0 - 2024-10-23
* device-type list: Add `--all` flag for including no longer supported device types in the list [myarmolinsky]
* Add alias `device-type list` for command `devices supported` [myarmolinsky]
## 19.15.0 - 2024-10-23
* Add alias `device note` for command `notes` [myarmolinsky]
## 19.14.0 - 2024-10-22
* Add alias `device tunnel` for command `tunnel` [myarmolinsky]
## 19.13.1 - 2024-10-21
* Update dependency chalk to v4 [Self-hosted Renovate Bot]
## 19.13.0 - 2024-10-21
* Add alias `env set` for command `env add` [myarmolinsky]
## 19.12.1 - 2024-10-21
* Update `@oclif/core` [myarmolinsky]
## 19.12.0 - 2024-10-21
* Add alias `device ssh` for `ssh` command [myarmolinsky]
## 19.11.1 - 2024-10-21
* Update dependency sinon to v19 [Self-hosted Renovate Bot]
## 19.11.0 - 2024-10-21
* Add alias `device logs` for `logs` [myarmolinsky]
* git mv `logs/index` to `device/logs` [myarmolinsky]
## 19.10.0 - 2024-10-21
* Add alias `device detect` for `scan` [myarmolinsky]
## 19.9.0 - 2024-10-18
* Add alias `organization list` for `orgs` command [myarmolinsky]
## 19.8.0 - 2024-10-18
* Add alias `tag list` for `tags` command [myarmolinsky]
## 19.7.0 - 2024-10-18
* Add alias `env list` for command `envs` [myarmolinsky]
## 19.6.0 - 2024-10-18
* Add `ssh-key` as aliases for all `key` commands [myarmolinsky]
## 19.5.0 - 2024-10-17
* Add `key list` alias for `keys` command [myarmolinsky]
## 19.4.0 - 2024-10-16
* Add `release list` alias for `releases` command [myarmolinsky]
## 19.3.0 - 2024-10-16
* Add alias `fleet list` for `fleets` command [myarmolinsky]
## 19.2.0 - 2024-10-16
* Add alias `api-key list` for command `api-keys` [myarmolinsky]
## 19.1.3 - 2024-10-16
* Remove custom sorting of OS commands in docs in favor of alphabetizing [myarmolinsky]
## 19.1.2 - 2024-10-16
* Remove no longer needed dependency `get-stdin` [myarmolinsky]
* Remove custom override of oclif Command class in favor of `prerun` hook [myarmolinsky]
## 19.1.1 - 2024-10-14
* Deduplicate dependencies [myarmolinsky]
* Fix changelog entry for v19.1.0 [myarmolinsky]
## 19.1.0 - 2024-10-11
* Docs: Show aliases for commands [myarmolinsky]
* Add alias `device list` for `devices` command [myarmolinsky]
* Deduplicate dependencies [myarmolinsky]
## 19.0.20 - 2024-10-11
* Docs: Generate CLI command references from file names instead of usage [myarmolinsky]
* Use default oclif `USAGE` message for all commands [myarmolinsky]
## 19.0.19 - 2024-10-11
* Deduplicate dependencies [myarmolinsky]
* Fix update notification release notes link [myarmolinsky]
## 19.0.18 - 2024-10-08
* Contributing: No longer request separate folders for plural commands [myarmolinsky]
## 19.0.17 - 2024-10-08
* Remove dev dependency `parse-link-header` [myarmolinsky]
* Remove dev dependency @octokit/rest [myarmolinsky]
* Remove dev dependency @octokit/plugin-throttling [myarmolinsky]
* Remove no longer needed references and tests for mixpanel [myarmolinsky]
* Remove dev dependency `@types/mixpanel` [myarmolinsky]
## 19.0.16 - 2024-10-08
* compose: Reduce the properties updated to only the necessary [Thodoris Greasidis]
## 19.0.15 - 2024-10-08
* Remove unused `mockery` dev dependency [myarmolinsky]
## 19.0.14 - 2024-10-08
* Temporarily skip broken image-manager tests on Windows and Mac [myarmolinsky]
## 19.0.13 - 2024-09-23
* Remove extra line from recent changelog entry [myarmolinsky]
## 19.0.12 - 2024-09-20
* Add `image-manager` tests [myarmolinsky]
* Remove `balena-image-manager` dependency [myarmolinsky]
* Embed `balena-image-manager` instead of having it as a dependency [myarmolinsky]
## 19.0.11 - 2024-09-18
* Remove Bluebird as a direct dependency [Thodoris Greasidis]
## 19.0.10 - 2024-09-12
* Remove package `@resin.io/valid-email` [myarmolinsky]
## 19.0.9 - 2024-09-12
* Update actions/download-artifact action to v4.1.8 [Self-hosted Renovate Bot]
## 19.0.8 - 2024-09-12
* Update actions/upload-artifact digest to 5076954 [Self-hosted Renovate Bot]
## 19.0.7 - 2024-09-12
* Update actions/setup-node digest to 1e60f62 [Self-hosted Renovate Bot]
## 19.0.6 - 2024-09-12
* Remove moment and moment-duration-format in favor of native time parsing [Otavio Jacobi]
## 19.0.5 - 2024-09-10
* Update apple-actions/import-codesign-certs action to v2 [Self-hosted Renovate Bot]
## 19.0.4 - 2024-09-10
* Update TypeScript to 5.6.2 [Thodoris Greasidis]
## 19.0.3 - 2024-09-05
* Reduce use of CJS require() on automation files [Otavio Jacobi]
* Remove the use of CJS require() on test files [Otavio Jacobi]
* Remove not necessary 'import = require' syntax for js-yaml [Otavio Jacobi]
## 19.0.2 - 2024-09-03
* Update devDependency patch-package to v8.0.0 [Otavio Jacobi]
* Update devDependency mkdirp to v3.0.1 [Otavio Jacobi]
* Update devDependency fs-extra(to v11) and @types/fs-extra(to v11) [Otavio Jacobi]
* Update devDependency @types/parse-link-header to v2.0.3 [Otavio Jacobi]
* Remove unused devDependency @types/nock [Otavio Jacobi]
* Update devDependency klaw(to v4) and @types/klaw(to v3.0.6) [Otavio Jacobi]
* Update husky to v9.1.5 [Otavio Jacobi]
* Update devDependency @types/jsonwebtoken to v9.0.6 [Otavio Jacobi]
* Update devDependency archiver(to v7) and @types/archiver(to v6) [Otavio Jacobi]
* Removes unused devDependency @types/net-keepalive [Otavio Jacobi]
* Update devDependency rewire(to v7) and @types/rewire(to v2.5.30) [Otavio Jacobi]
* Update devDependency sinon(to v18) and @types/sinon(to v17) [Otavio Jacobi]
## 19.0.1 - 2024-09-02
* Bump @oclif/core from 3.27.0 to 4.0.18 [Otavio Jacobi]
## 19.0.0 - 2024-08-22
* Update all references of lib to src [Otavio Jacobi]
* Rename the lib folder to src [Otavio Jacobi]
* Update @balena/compose to 4.0.1 Update @balena/compose from 3.2.1 to 4.0.1 [Otavio Jacobi]
* Use standard visuals table component for fleet/s [Otavio Jacobi]
## 18.2.34 - 2024-07-29
* Run npm dedupe commands [Kyle Harding]
* Switch to self-hosted [Anton Belodedenko]
## 18.2.33 - 2024-07-17
* Improve discover balena os across different networks [Otavio Jacobi]
## 18.2.32 - 2024-07-16
* Remove unused code [Otavio Jacobi]
## 18.2.31 - 2024-07-15
* deploy: Use the sdk's pine instance with balena-compose [Thodoris Greasidis]
<details>
<summary> Update balena-sdk to 19.7.3 [Thodoris Greasidis] </summary>
> ### balena-sdk-19.7.3 - 2024-07-12
>
> * pinejs-client-core: Add some missing methods to the custom typings [Thodoris Greasidis]
>
</details>
## 18.2.30 - 2024-07-15
* Omit unicode control character escapes from test logs [Thodoris Greasidis]
## 18.2.29 - 2024-07-12
* Update balena-preload from 15.0.5 to 15.0.6 [Otavio Jacobi]
## 18.2.28 - 2024-07-12
* Downgrade pinejs-client-request to 7.4.2 to unblock the sdk update [Thodoris Greasidis]
<details>
<summary> Update balena-sdk to 19.7.2 [Thodoris Greasidis] </summary>
> ### balena-sdk-19.7.2 - 2024-07-12
>
>
> <details>
> <summary> Update balena-request from 13.3.1 to 13.3.2 [Thodoris Greasidis] </summary>
>
>> #### balena-request-13.3.2 - 2024-07-12
>>
>> * Fix always following redirects when followRedirect = false [Thodoris Greasidis]
>>
>
> </details>
>
>
> ### balena-sdk-19.7.1 - 2024-07-08
>
>
> <details>
> <summary> Limit pinejs-client-core to ~6.14.0, to fix errors in older TypeScript [Thodoris Greasidis] </summary>
>
>> #### pinejs-client-js-6.14.0 - 2023-12-05
>>
>> * Respect the Retry-After header when clients define the getRetryAfterHeader option [Thodoris Greasidis]
>>
>> #### pinejs-client-js-6.13.0 - 2023-07-11
>>
>> * Add support for $duration [Thodoris Greasidis]
>>
>> #### pinejs-client-js-6.12.4 - 2023-05-09
>>
>> * Avoid an unnecessary function creation on each get() call [Thodoris Greasidis]
>>
>> #### pinejs-client-js-6.12.3 - 2022-12-28
>>
>> * CI: Convert tests to TypeScript [Josh Bowling]
>>
>> #### pinejs-client-js-6.12.2 - 2022-11-18
>>
>> * Fix `$orderby: { a: { $count: ... }, $dir: 'asc' }` typings [Thodoris Greasidis]
>>
>> #### pinejs-client-js-6.12.1 - 2022-11-15
>>
>> * Update TypeScript to 4.9.3 [Thodoris Greasidis]
>>
>
> </details>
>
> * Fix the TypeScript incompatibility test [Thodoris Greasidis]
>
> ### balena-sdk-19.7.0 - 2024-07-05
>
> * Add identity provider & saml account model typing [Otavio Jacobi]
>
> ### balena-sdk-19.6.1 - 2024-06-20
>
> * Update TypeScript to 5.5.2 [Thodoris Greasidis]
>
> ### balena-sdk-19.6.0 - 2024-06-20
>
> * Add the application.getAllByOrganization() method [Thodoris Greasidis]
> * Deprecate the application.getAppByOwner() method [Thodoris Greasidis]
>
> ### balena-sdk-19.5.11 - 2024-05-28
>
> * tests: Make the cleanups more precise [Thodoris Greasidis]
>
> ### balena-sdk-19.5.10 - 2024-03-29
>
> * Drop the toWritable helper in favor of TypeScript's satisfies [Thodoris Greasidis]
>
> ### balena-sdk-19.5.9 - 2024-03-29
>
> * os: Update the comments on why we still need to be using the release_tags [Thodoris Greasidis]
>
> ### balena-sdk-19.5.8 - 2024-03-18
>
> * Fix `application.create` method being wrongly marked as deprecated [myarmolinsky]
>
> ### balena-sdk-19.5.7 - 2024-03-08
>
> * Fix missing underscore to describes__device property [Andrea Rosci]
>
> ### balena-sdk-19.5.6 - 2024-03-07
>
> * Update TypeScript to 5.4.2 [Thodoris Greasidis]
> * device-type.getInstructions: Convert etcher link to HTTPS [Vipul Gupta (@vipulgupta2048)]
>
</details>
## 18.2.27 - 2024-07-12
<details>
<summary> Update balena-sdk to 19.5.5 [Thodoris Greasidis] </summary>
> ### balena-sdk-19.5.5 - 2024-02-26
>
>
> <details>
> <summary> Update balena-auth to 6.0.1 [Thodoris Greasidis] </summary>
>
>> #### balena-auth-6.0.1 - 2024-02-23
>>
>> * Update jwt-decode to v3 [Thodoris Greasidis]
>>
>> #### balena-auth-6.0.0 - 2024-02-23
>>
>> * Update typescript to 5.3.3 [Thodoris Greasidis]
>> * Move the sources from lib to src [Thodoris Greasidis]
>> * Update @balena/lint to v7 [Thodoris Greasidis]
>> * Stop publishing the lib folder [Thodoris Greasidis]
>> * Drop support for nodejs < 18 [Thodoris Greasidis]
>> * Drop no longer used appveyor.yml [Thodoris Greasidis]
>>
>> #### balena-register-device-9.0.2 - 2024-02-23
>>
>> * Update @balena/lint to v7 [Thodoris Greasidis]
>> * Update balena-request to 13.3.0 [Thodoris Greasidis]
>>
>> #### balena-request-13.3.1 - 2024-02-23
>>
>> * Update balena-auth to 6.0.1 [Thodoris Greasidis]
>>
>
> </details>
>
>
> ### balena-sdk-19.5.4 - 2024-02-14
>
> * Bump balena-request Update balena-request from 13.2.0 to 13.3.0 [Otávio Jacobi]
>
> ### balena-sdk-19.5.3 - 2024-02-14
>
> * Replace deprecated flowzone input tests_run_on [Kyle Harding]
>
> ### balena-sdk-19.5.2 - 2024-02-13
>
> * tests: Reformat describe & it calls to have curly braces [Thodoris Greasidis]
>
> ### balena-sdk-19.5.1 - 2024-02-02
>
> * Update @balena/lint to 7.3.0 [Thodoris Greasidis]
>
> ### balena-sdk-19.5.0 - 2024-01-26
>
> * types: Add the `Organization.is_using__billing_version` property [Thodoris Greasidis]
>
</details>
## 18.2.26 - 2024-07-12
* Drop unused dependencies [Otavio Jacobi]
* Move dependencies that should be dev only as devDependencies [Otavio Jacobi]
## 18.2.25 - 2024-07-11
* Fix complete generation intermitency [Otavio Jacobi]
* Bump oclif to v4 [Otavio Jacobi]
## 18.2.24 - 2024-07-10
* Update mocha from 8.4.0 to 10.6.0 [Otavio Jacobi]
* Override inline-source-cli with non-vulnerable dependency [Otavio Jacobi]
## 18.2.23 - 2024-07-10
* Replace resin-discoverable-services with bonjour-service [Otavio Jacobi]
## 18.2.22 - 2024-07-10
* Remove unused dependency minimatch [Otavio Jacobi]
## 18.2.21 - 2024-07-09
* Bump resin-discoverable-services from 2.0.4 to 2.0.5 [Otavio Jacobi]
## 18.2.20 - 2024-07-05
* Audit fix dependencies [Otavio Jacobi]
## 18.2.19 - 2024-07-05
* Remove unused package `publish-release` [myarmolinsky]
## 18.2.18 - 2024-07-04
* Update actions/setup-node action to v4 [Self-hosted Renovate Bot]
## 18.2.17 - 2024-07-02
<details>
<summary> Update dependency etcher-sdk to v9.1.0 [Self-hosted Renovate Bot] </summary>
> ### etcher-sdk-9.1.0 - 2024-06-13
>
> * patch: etcher-sdk is not yet compatible with node22 [JOASSART Edwin]
> * minor: allow passing custom assets to start SB protected CM4 [Edwin Joassart]
>
</details>
## 18.2.16 - 2024-07-02
<details>
<summary> Update dependency etcher-sdk to v9.0.11 [Self-hosted Renovate Bot] </summary>
> ### etcher-sdk-9.0.11 - 2024-04-26
>
> * patch: use http2 to fix issues with url source [Edwin Joassart]
>
> ### etcher-sdk-9.0.10 - 2024-04-26
>
> * patch: remove CI workaround [Edwin Joassart]
>
> ### etcher-sdk-9.0.9 - 2024-04-24
>
> * patch: add option to allow listing virtual drive on Mac [JOASSART Edwin]
>
</details>
## 18.2.15 - 2024-07-02
* Update dependency event-stream to v3.3.5 [Self-hosted Renovate Bot]
## 18.2.14 - 2024-07-02
* Update dependency jsonwebtoken to v9 [SECURITY] [Self-hosted Renovate Bot]
## 18.2.13 - 2024-07-02
* Update dependency @types/prettyjson to ^0.0.33 [Self-hosted Renovate Bot]
## 18.2.12 - 2024-07-02
* Deduplicate dependencies [Thodoris Greasidis]
## 18.2.11 - 2024-07-01
* Update dependency @types/fast-levenshtein to v0.0.4 [Self-hosted Renovate Bot]

View File

@ -14,7 +14,7 @@ The balena CLI is an open source project and your contribution is welcome!
In order to ease development:
* `npm run build:fast` skips some of the build steps for interactive testing, or
* `npm run test:source` skips testing the standalone packages (which is rather slow)
* `npm run test:source` skips testing the standalone zip packages (which is rather slow)
* `./bin/balena-dev` uses `ts-node/register` to transpile on the fly.
Before opening a PR, test your changes with `npm test`. Keep compatibility in mind, as the CLI is
@ -115,9 +115,9 @@ The content sources for the auto generation of `docs/balena-cli.md` are:
* [Selected
sections](https://github.com/balena-io/balena-cli/blob/v12.23.0/automation/capitanodoc/capitanodoc.ts#L199-L204)
of the README file.
* The CLI's command documentation in source code (`src/commands/` folder), for example:
* `src/commands/push.ts`
* `src/commands/env/add.ts`
* The CLI's command documentation in source code (`lib/commands/` folder), for example:
* `lib/commands/push.ts`
* `lib/commands/env/add.ts`
The README file is manually edited, but subsections are automatically extracted for inclusion in
`docs/balena-cli.md` by the `getCapitanoDoc()` function in
@ -133,6 +133,7 @@ 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
4. Resources with plural names needs to have 2 sections if they have commands like: "fleet, fleets" or "device, devices" or "tag, tags"
Once added, run the command `npm run build` to generate the documentation
@ -223,7 +224,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
[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
be automatically invalidated if `npm link` is used or if manual modifications are made to the
`node_modules` folder. In this situation:

View File

@ -8,8 +8,8 @@ There are 3 options to choose from to install balena's CLI:
* [Executable Installer](#executable-installer): the easiest method on Windows and macOS, using the
traditional graphical desktop application installers.
* [Standalone tar.gz Package](#standalone-targz-package): these are plain tar.gz files with the balena CLI
bundled within. Available for all platforms: Linux, Windows, macOS.
* [Standalone Zip Package](#standalone-zip-package): these are plain zip files with the balena CLI
executable in them: extract and run. Available for all platforms: Linux, Windows, macOS.
Recommended also for scripted installation in CI (continuous integration) environments.
* [NPM Installation](#npm-installation): recommended for Node.js developers who may be interested
in integrating the balena CLI in their existing projects or workflow.
@ -30,9 +30,9 @@ instructions:
> If you would like to use WSL, follow the [installations instructions for
> Linux](./INSTALL-LINUX.md) rather than Windows, as WSL consists of a Linux environment.
If you had previously installed the CLI using a standalone tar package, it may be a good idea to
If you had previously installed the CLI using a standalone zip package, it may be a good idea to
check your system's `PATH` environment variable for duplicate entries, as the terminal will use the
entry that comes first. Check the [Standalone tar.gz Package](#standalone-targz-package) instructions
entry that comes first. Check the [Standalone Zip Package](#standalone-zip-package) instructions
for how to modify the PATH variable.
By default, the CLI is installed to the following folders:
@ -40,19 +40,20 @@ By default, the CLI is installed to the following folders:
OS | Folders
--- | ---
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 tar.gz Package
## Standalone Zip Package
1. Download the latest tar.gz file from the [releases page](https://github.com/balena-io/balena-cli/releases).
1. Download the latest zip file from the [releases page](https://github.com/balena-io/balena-cli/releases).
Look for a file name that ends with the word "standalone", for example:
`balena-cli-vX.Y.Z-linux-x64-standalone.tar.gz`_also for the Windows Subsystem for Linux_
`balena-cli-vX.Y.Z-macOS-x64-standalone.tar.gz`
`balena-cli-vX.Y.Z-windows-x64-standalone.tar.gz`
`balena-cli-vX.Y.Z-linux-x64-standalone.zip`_also for the Windows Subsystem for Linux_
`balena-cli-vX.Y.Z-macOS-x64-standalone.zip`
`balena-cli-vX.Y.Z-windows-x64-standalone.zip`
2. Extract the tar.gz file contents to any folder you choose. The extracted contents will be a `balena` folder containing a `bin` subdirectory.
2. Extract the zip file contents to any folder you choose. The extracted contents will include a
`balena-cli` folder.
3. Add the `balena/bin` folder to the system's `PATH` environment variable.
3. Add the `balena-cli` folder to the system's `PATH` environment variable.
See instructions for:
[Linux](https://stackoverflow.com/questions/14637979/how-to-permanently-set-path-on-linux-unix) |
[macOS](https://www.architectryan.com/2012/10/02/add-to-the-path-on-mac-os-x-mountain-lion/#.Uydjga1dXDg) |
@ -60,14 +61,14 @@ macOS: | `/usr/local/src/balena-cli/` <br> `/usr/local/bin/balena`
> * If you are using macOS 10.15 or later (Catalina, Big Sur), [check this known issue and
> workaround](https://github.com/balena-io/balena-cli/issues/2244).
> * **Linux Alpine** and **Busybox:** the standalone tar.gz package is not currently compatible with
> * **Linux Alpine** and **Busybox:** the standalone zip package is not currently compatible with
> these "compact" Linux distributions, because of the alternative C libraries they ship with.
> For these, consider the [NPM Installation](#npm-installation) option.
> * Note that moving the `balena/bin/balena` executable out of the extracted `balena` folder on its own
> * Note that moving the `balena` executable out of the extracted `balena-cli` folder on its own
> (e.g. moving it to `/usr/local/bin/balena`) will **not** work, as it depends on the other
> folders and files also present in the `balena` folder.
> folders and files also present in the `balena-cli` folder.
To update the CLI to a new version, download a new release tar.gz file and replace the previous
To update the CLI to a new version, download a new release zip file and replace the previous
installation folder. To uninstall, simply delete the folder and edit the PATH environment variable
as described above.
@ -77,8 +78,8 @@ If you are a Node.js developer, you may wish to install the balena CLI via [npm]
The npm installation involves building native (platform-specific) binary modules, which require
some development tools to be installed first, as follows.
> **The balena CLI currently requires Node.js version >=20.6.0**
> **Versions 23 and later are not yet fully supported.**
> **The balena CLI currently requires Node.js version ^20.6.0**
> **Versions 21 and later are not yet fully supported.**
### Install development tools
@ -88,7 +89,7 @@ some development tools to be installed first, as follows.
$ sudo apt-get update && sudo apt-get -y install curl python3 git make g++
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
$ . ~/.bashrc
$ nvm install 22
$ nvm install 20
```
The `curl` command line above uses
@ -105,7 +106,7 @@ recommended.
```sh
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
$ . ~/.bashrc
$ nvm install 22
$ nvm install 20
```
#### **Windows** (not WSL)
@ -113,7 +114,7 @@ $ nvm install 22
Install:
* If you'd like the ability to switch between Node.js versions, install
- Node.js v22 from the [Nodejs.org releases page](https://nodejs.org/en/download/releases/).
- 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)
instead.
* The [MSYS2 shell](https://www.msys2.org/), which provides `git`, `make`, `g++` and more:
@ -144,7 +145,7 @@ container) in order to allow npm scripts like `postinstall` to be executed.
## 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
system:

View File

@ -8,15 +8,15 @@ method.
Selected operating system: **Linux**
1. Download the latest tar.gz file from the [latest release
1. Download the latest zip file from the [latest release
page](https://github.com/balena-io/balena-cli/releases/latest). Look for a file name that ends
with "-standalone.tar.gz", for example:
`balena-cli-vX.Y.Z-linux-x64-standalone.tar.gz`
with "-standalone.zip", for example:
`balena-cli-vX.Y.Z-linux-x64-standalone.zip`
2. Extract the tar.gz file contents to any folder you choose, for example `/home/james`.
The extracted contents will include a `balena/bin` folder.
2. Extract the zip file contents to any folder you choose, for example `/home/james`.
The extracted contents will include a `balena-cli` folder.
3. Add that folder (e.g. `/home/james/balena/bin`) to the `PATH` environment variable.
3. Add that folder (e.g. `/home/james/balena-cli`) to the `PATH` environment variable.
Check this [StackOverflow
post](https://stackoverflow.com/questions/14637979/how-to-permanently-set-path-on-linux-unix)
for instructions. Close and reopen the terminal window so that the changes to `PATH`
@ -27,13 +27,13 @@ Selected operating system: **Linux**
* `balena version` - should print the CLI's version
* `balena help` - should print a list of available commands
To update the balena CLI to a new version, download a new release tar.gz file and replace the previous
To update the balena CLI to a new version, download a new release zip file and replace the previous
installation folder. To uninstall, simply delete the folder and edit the PATH environment variable
as described above.
## 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`
setting, run `sudo visudo` to edit it and add the balena CLI's installation folder to
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
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`
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*`
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
desktop Linux distributions. Otherwise, on Debian or Ubuntu, the installation command would be
`sudo apt-get install avahi-daemon`.

View File

@ -7,8 +7,8 @@ Selected operating system: **macOS**
1. Download the installer from the [latest release
page](https://github.com/balena-io/balena-cli/releases/latest).
Look for a file name that ends with "-installer.pkg":
`balena-cli-vX.Y.Z-macOS-x64-installer.pkg`
Look for a file name that ends with "-installer.pkg":
`balena-cli-vX.Y.Z-macOS-x64-installer.pkg`
2. Double click on the downloaded file to run the installer and follow the installer's
instructions.
@ -19,7 +19,7 @@ Selected operating system: **macOS**
- On the terminal prompt, type `balena version` and hit Enter. It should display
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
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:
```text
sudo /usr/local/src/balena-cli/bin/uninstall
sudo /usr/local/lib/balena-cli/bin/uninstall
```
## 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
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
include:
@ -52,7 +52,7 @@ include:
Components → Command Line Tools → Install.
* 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*`
command set can also be used to list and manage SSH keys: see `balena help -v`.

View File

@ -8,7 +8,7 @@ Selected operating system: **Windows**
1. Download the installer from the [latest release
page](https://github.com/balena-io/balena-cli/releases/latest).
Look for a file name that ends with "-installer.exe":
`balena-cli-vX.Y.Z-windows-x64-installer.exe`
`balena-cli-vX.Y.Z-windows-x64-installer.exe`
2. Double click on the downloaded file to run the installer and follow the installer's
instructions.
@ -19,7 +19,7 @@ Selected operating system: **Windows**
- On the command prompt, type `balena version` and hit Enter. It should display
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
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
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
Update. To check whether it is installed, run `ssh` on a Windows Command Prompt or PowerShell. It
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
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*`
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
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

View File

@ -1,45 +0,0 @@
## Migrating to balena CLI v22
This guide outlines the changes introduced in balena CLI v22 and provides instructions for when and how to migrate.
---
### For Installer Users (Windows .exe, macOS .pkg)
If you are using the Windows executable (.exe) or macOS package (.pkg) installers, **no changes** are required for this update. You can continue to use the installers as before.
---
### For npm Installation Users
If you installed balena CLI via npm, **no changes** are required for this update. Your existing installation and update process remains the same.
---
### For Standalone Installation Users
Users of the standalone balena CLI will need to make the following adjustments:
1. **Archive Format Change**: The distribution archive format has changed from `.zip` to `.tar.gz`. You will need to use the `tar` command instead of `unzip` to extract the CLI.
* **Previous command (v21.x.x and older):**
```bash
unzip balena-cli-v21.1.12-linux-x64-standalone.zip
```
* **New command (v22.0.0 and newer):**
```bash
tar -xzf balena-cli-v22.0.0-linux-x64-standalone.tar.gz
```
2. **Executable Path Change**: The path to the balena CLI executable within the extracted folder has been updated.
* **Previous path (v21.x.x and older):**
```
balena-cli/balena
```
* **New path (v22.0.0 and newer):**
```
balena/bin/balena
```
Please update your scripts and any aliases to reflect these changes if you are using the standalone version.

View File

@ -20,8 +20,6 @@ GitHub](https://github.com/balena-io/balena-cli/), and your contribution is also
Check the [balena CLI installation instructions on
GitHub](https://github.com/balena-io/balena-cli/blob/master/INSTALL.md).
Note for v22 Migration: If you are upgrading to balena CLI v22 from a previous standalone installation, please [see the v22 Migration Guide](https://github.com/balena-io/balena-cli/blob/master/MIGRATIONS.md) for installation changes.
## Choosing a shell (command prompt/terminal)
On **Windows,** the standard Command Prompt (`cmd.exe`) and
@ -90,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
`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` 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
@ -112,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
> option](https://github.com/balena-io/balena-cli/blob/master/INSTALL.md), it also requires
> 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.
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).
## 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.
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
```
## 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
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
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`
@ -115,7 +115,7 @@ If nothing seems to help, consider also using a different client-side terminal a
## "Docker seems to be unavailable" error when using Windows Subsystem for Linux (WSL)
When running on WSL, the recommendation is to install a CLI release for Linux, like the standalone
tar.gz package for Linux. However, commands like "balena build" will, by default, attempt to reach the
zip package for Linux. However, commands like "balena build" will, by default, attempt to reach the
Docker daemon at the Unix socket path `/var/run/docker.sock`, while Docker Desktop for Windows uses
a Windows named pipe at `//./pipe/docker_engine` (which the Linux CLI on WSL cannot use). A
solution is:

View File

@ -15,21 +15,33 @@
* limitations under the License.
*/
import type { JsonVersions } from '../lib/commands/version/index';
import { run as oclifRun } from '@oclif/core';
import * as archiver from 'archiver';
import * as Bluebird from 'bluebird';
import { exec, execFile } from 'child_process';
import * as filehound from 'filehound';
import type { Stats } from 'fs';
import * as fs from 'fs-extra';
import * as klaw from 'klaw';
import * as path from 'path';
import * as rimraf from 'rimraf';
import * as semver from 'semver';
import { promisify } from 'util';
import { notarize } from '@electron/notarize';
import { loadPackageJson, ROOT, whichSpawn } from './utils';
import { stripIndent } from '../build/utils/lazy';
import {
diffLines,
loadPackageJson,
ROOT,
StdOutTap,
whichSpawn,
} from './utils';
const execFileAsync = promisify(execFile);
const execAsync = promisify(exec);
const rimrafAsync = promisify(rimraf);
export const packageJSON = loadPackageJson();
export const version = 'v' + packageJSON.version;
@ -43,6 +55,12 @@ interface PathByPlatform {
[platform: string]: string;
}
const standaloneZips: PathByPlatform = {
linux: dPath(`balena-cli-${version}-linux-${arch}-standalone.zip`),
darwin: dPath(`balena-cli-${version}-macOS-${arch}-standalone.zip`),
win32: dPath(`balena-cli-${version}-windows-${arch}-standalone.zip`),
};
const getOclifInstallersOriginalNames = async (): Promise<PathByPlatform> => {
const { stdout } = await execAsync('git rev-parse --short HEAD');
const sha = stdout.trim();
@ -57,28 +75,260 @@ const renamedOclifInstallers: PathByPlatform = {
win32: dPath(`balena-cli-${version}-windows-${arch}-installer.exe`),
};
const getOclifStandaloneOriginalNames = async (): Promise<PathByPlatform> => {
const { stdout } = await execAsync('git rev-parse --short HEAD');
const sha = stdout.trim();
return {
linux: dPath(`balena-${version}-${sha}-linux-${arch}.tar.gz`),
darwin: dPath(`balena-${version}-${sha}-darwin-${arch}.tar.gz`),
win32: dPath(`balena-${version}-${sha}-win32-${arch}.tar.gz`),
};
export const finalReleaseAssets: { [platform: string]: string[] } = {
win32: [standaloneZips['win32'], renamedOclifInstallers['win32']],
darwin: [standaloneZips['darwin'], renamedOclifInstallers['darwin']],
linux: [standaloneZips['linux']],
};
const renamedOclifStandalone: PathByPlatform = {
linux: dPath(`balena-cli-${version}-linux-${arch}-standalone.tar.gz`),
darwin: dPath(`balena-cli-${version}-macOS-${arch}-standalone.tar.gz`),
win32: dPath(`balena-cli-${version}-windows-${arch}-standalone.tar.gz`),
};
/**
* Given the output of `pkg` as a string (containing warning messages),
* diff it against previously saved output of known "safe" warnings.
* Throw an error if the diff is not empty.
*/
async function diffPkgOutput(pkgOut: string) {
const { monochrome } = await import('../tests/helpers');
const relSavedPath = path.join(
'tests',
'test-data',
'pkg',
`expected-warnings-${process.platform}-${arch}.txt`,
);
const absSavedPath = path.join(ROOT, relSavedPath);
const ignoreStartsWith = [
'> pkg@',
'> Fetching base Node.js binaries',
' fetched-',
'prebuild-install WARN install No prebuilt binaries found',
];
const modulesRE =
process.platform === 'win32'
? /(?<=[ '])([A-Z]:)?\\.+?\\node_modules(?=\\)/
: /(?<=[ '])\/.+?\/node_modules(?=\/)/;
const buildRE =
process.platform === 'win32'
? /(?<=[ '])([A-Z]:)?\\.+\\build(?=\\)/
: /(?<=[ '])\/.+\/build(?=\/)/;
const cleanLines = (chunks: string | string[]) => {
const lines = typeof chunks === 'string' ? chunks.split('\n') : chunks;
return lines
.map((line: string) => monochrome(line)) // remove ASCII colors
.filter((line: string) => !/^\s*$/.test(line)) // blank lines
.filter((line: string) =>
ignoreStartsWith.every((i) => !line.startsWith(i)),
)
.map((line: string) => {
// replace absolute paths with relative paths
let replaced = line.replace(modulesRE, 'node_modules');
if (replaced === line) {
replaced = line.replace(buildRE, 'build');
}
return replaced;
});
};
pkgOut = cleanLines(pkgOut).join('\n');
const { readFile } = (await import('fs')).promises;
const expectedOut = cleanLines(await readFile(absSavedPath, 'utf8')).join(
'\n',
);
if (expectedOut !== pkgOut) {
const sep =
'================================================================================';
const diff = diffLines(expectedOut, pkgOut);
const msg = `pkg output does not match expected output from "${relSavedPath}"
Diff:
${sep}
${diff}
${sep}
Check whether the new or changed pkg warnings are safe to ignore, then update
"${relSavedPath}"
and share the result of your investigation as comments on the pull request.
Hint: the fix is often a matter of updating the 'pkg.scripts' or 'pkg.assets'
sections in the CLI's 'package.json' file, or a matter of updating the
'buildPkg' function in 'automation/build-bin.ts'. Sometimes it requires
patching dependencies: See for example 'patches/all/open+7.0.2.patch'.
${sep}
`;
throw new Error(msg);
}
}
/**
* Call `pkg.exec` to generate the standalone zip file, capturing its warning
* messages (stdout and stderr) in order to call diffPkgOutput().
*/
async function execPkg(...args: any[]) {
const { exec: pkgExec } = await import('@yao-pkg/pkg');
const outTap = new StdOutTap(true);
try {
outTap.tap();
await (pkgExec as any)(...args);
} catch (err) {
outTap.untap();
console.log(outTap.stdoutBuf.join(''));
console.error(outTap.stderrBuf.join(''));
throw err;
}
outTap.untap();
await diffPkgOutput(outTap.allBuf.join(''));
}
/**
* Use the 'pkg' module to create a single large executable file with
* the contents of 'node_modules' and the CLI's javascript code.
* Also copy a number of native modules (binary '.node' files) that are
* compiled during 'npm install' to the 'build-bin' folder, alongside
* the single large executable file created by pkg. (This is necessary
* because of a pkg limitation that does not allow binary executables
* to be directly executed from inside another binary executable.)
*/
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 = [
'--targets',
targets,
'--output',
'build-bin/balena',
'package.json',
];
console.log('=======================================================');
console.log(`execPkg ${args.join(' ')}`);
console.log(`cwd="${process.cwd()}" ROOT="${ROOT}"`);
console.log('=======================================================');
await execPkg(args);
const paths: Array<[string, string[], string[]]> = [
// [platform, [source path], [destination path]]
['*', ['open', 'xdg-open'], ['xdg-open']],
['darwin', ['denymount', 'bin', 'denymount'], ['denymount']],
];
await Promise.all(
paths.map(([platform, source, dest]) => {
if (platform === '*' || platform === process.platform) {
// eg copy from node_modules/open/xdg-open to build-bin/xdg-open
return fs.copy(
path.join(ROOT, 'node_modules', ...source),
path.join(ROOT, 'build-bin', ...dest),
);
}
}),
);
const nativeExtensionPaths: string[] = await filehound
.create()
.paths(path.join(ROOT, 'node_modules'))
.ext(['node', 'dll'])
.find();
console.log(`\nCopying to build-bin:\n${nativeExtensionPaths.join('\n')}`);
await Promise.all(
nativeExtensionPaths.map((extPath) =>
fs.copy(
extPath,
extPath.replace(
path.join(ROOT, 'node_modules'),
path.join(ROOT, 'build-bin'),
),
),
),
);
}
/**
* Run some basic tests on the built pkg executable.
* TODO: test more than just `balena version -j`; integrate with the
* existing mocha/chai CLI command testing.
*/
async function testPkg() {
const pkgBalenaPath = path.join(
ROOT,
'build-bin',
process.platform === 'win32' ? 'balena.exe' : 'balena',
);
console.log(`Testing standalone package "${pkgBalenaPath}"...`);
// Run `balena version -j`, parse its stdout as JSON, and check that the
// reported Node.js major version matches semver.major(process.version)
let { stdout, stderr } = await execFileAsync(pkgBalenaPath, [
'version',
'-j',
]);
const { filterCliOutputForTests } = await import('../tests/helpers');
const filtered = filterCliOutputForTests({
err: stderr.split(/\r?\n/),
out: stdout.split(/\r?\n/),
});
stdout = filtered.out.join('\n');
stderr = filtered.err.join('\n');
let pkgNodeVersion = '';
let pkgNodeMajorVersion = 0;
try {
const balenaVersions: JsonVersions = JSON.parse(stdout);
pkgNodeVersion = balenaVersions['Node.js'];
pkgNodeMajorVersion = semver.major(pkgNodeVersion);
} catch (err) {
throw new Error(stripIndent`
Error parsing JSON output of "balena version -j": ${err}
Original output: "${stdout}"`);
}
if (semver.major(process.version) !== pkgNodeMajorVersion) {
throw new Error(
`Mismatched major version: built-in pkg Node version="${pkgNodeVersion}" vs process.version="${process.version}"`,
);
}
if (filtered.err.length > 0) {
const err = filtered.err.join('\n');
throw new Error(`"${pkgBalenaPath}": non-empty stderr "${err}"`);
}
console.log('Success! (standalone package test successful)');
}
/**
* Create the zip file for the standalone 'pkg' bundle previously created
* by the buildPkg() function in 'build-bin.ts'.
*/
async function zipPkg() {
const outputFile = standaloneZips[process.platform];
if (!outputFile) {
throw new Error(
`Standalone installer unavailable for platform "${process.platform}"`,
);
}
await fs.mkdirp(path.dirname(outputFile));
await new Promise((resolve, reject) => {
console.log(`Zipping standalone package to "${outputFile}"...`);
const archive = archiver('zip', {
zlib: { level: 7 },
});
archive.directory(path.join(ROOT, 'build-bin'), 'balena-cli');
const outputStream = fs.createWriteStream(outputFile);
outputStream.on('close', resolve);
outputStream.on('error', reject);
archive.on('error', reject);
archive.on('warning', console.warn);
archive.pipe(outputStream);
archive.finalize().catch(reject);
});
}
export async function signFilesForNotarization() {
console.log('Signing files for notarization');
// If signFilesForNotarization is called on the test CI environment (which will not set CSC_LINK)
// then we skip the signing process.
if (process.platform !== 'darwin' || !process.env.CSC_LINK) {
console.log('Skipping signing for notarization');
if (process.platform !== 'darwin') {
return;
}
console.log('Deleting unneeded zip files...');
@ -166,39 +416,20 @@ export async function signFilesForNotarization() {
]);
}
export async function buildStandalone() {
console.log(`Building standalone tarball for CLI ${version}`);
fs.rmSync('./tmp', { recursive: true, force: true });
fs.rmSync('./dist', { recursive: true, force: true });
fs.mkdirSync('./dist');
export async function buildStandaloneZip() {
console.log(`Building standalone zip package for CLI ${version}`);
try {
let packOpts = ['-r', ROOT, '--no-xz'];
if (process.platform === 'darwin') {
packOpts = packOpts.concat('--targets', `darwin-${arch}`);
} else if (process.platform === 'win32') {
packOpts = packOpts.concat('--targets', 'win32-x64');
} else if (process.platform === 'linux') {
packOpts = packOpts.concat('--targets', `linux-${arch}`);
}
console.log(`Building oclif installer for CLI ${version}`);
const packCmd = `pack:tarballs`;
console.log('=======================================================');
console.log(`oclif ${packCmd} ${packOpts.join(' ')}`);
console.log(`cwd="${process.cwd()}" ROOT="${ROOT}"`);
console.log('=======================================================');
const oclifPath = path.join(ROOT, 'node_modules', 'oclif');
await oclifRun([packCmd].concat(...packOpts), oclifPath);
await renameStandalone();
console.log(`Standalone tarball package build completed`);
await buildPkg();
await testPkg();
await zipPkg();
console.log(`Standalone zip package build completed`);
} catch (error) {
console.error(`Error creating or testing standalone tarball package`);
console.error(`Error creating or testing standalone zip package`);
throw error;
}
}
async function renameInstallers() {
async function renameInstallerFiles() {
const oclifInstallers = await getOclifInstallersOriginalNames();
if (await fs.pathExists(oclifInstallers[process.platform])) {
await fs.rename(
@ -208,16 +439,6 @@ async function renameInstallers() {
}
}
async function renameStandalone() {
const oclifStandalone = await getOclifStandaloneOriginalNames();
if (await fs.pathExists(oclifStandalone[process.platform])) {
await fs.rename(
oclifStandalone[process.platform],
renamedOclifStandalone[process.platform],
);
}
}
/**
* If the CSC_LINK and CSC_KEY_PASSWORD env vars are set, digitally sign the
* executable installer using Microsoft SignTool.exe (Sign Tool)
@ -225,7 +446,7 @@ async function renameStandalone() {
*/
async function signWindowsInstaller() {
if (process.env.SM_CODE_SIGNING_CERT_SHA1_HASH) {
const exeName = (await getOclifInstallersOriginalNames())[process.platform];
const exeName = renamedOclifInstallers[process.platform];
console.log(`Signing installer "${exeName}"`);
// trust ...
await execFileAsync('signtool.exe', [
@ -259,14 +480,12 @@ async function notarizeMacInstaller(): Promise<void> {
const appleId =
process.env.XCODE_APP_LOADER_EMAIL || 'accounts+apple@balena.io';
const appleIdPassword = process.env.XCODE_APP_LOADER_PASSWORD;
const appPath = (await getOclifInstallersOriginalNames())[process.platform];
console.log(`Notarizing file "${appPath}"`);
if (appleIdPassword && teamId) {
await notarize({
tool: 'notarytool',
teamId,
appPath,
appPath: renamedOclifInstallers.darwin,
appleId,
appleIdPassword,
});
@ -298,7 +517,7 @@ export async function buildOclifInstaller() {
}
for (const dir of dirs) {
console.log(`rimraf(${dir})`);
await rimrafAsync(dir);
await Bluebird.fromCallback((cb) => rimraf(dir, cb));
}
console.log('=======================================================');
console.log(`oclif ${packCmd} ${packOpts.join(' ')}`);
@ -306,6 +525,7 @@ export async function buildOclifInstaller() {
console.log('=======================================================');
const oclifPath = path.join(ROOT, 'node_modules', 'oclif');
await oclifRun([packCmd].concat(...packOpts), oclifPath);
await renameInstallerFiles();
// The Windows installer is explicitly signed here (oclif doesn't do it).
// The macOS installer is automatically signed by oclif (which runs the
// `pkgbuild` tool), using the certificate name given in package.json
@ -317,7 +537,6 @@ export async function buildOclifInstaller() {
await notarizeMacInstaller(); // Notarize
console.log('Package notarized.');
}
await renameInstallers();
console.log(`oclif installer build completed`);
}
}
@ -353,5 +572,4 @@ export async function testShrinkwrap(): Promise<void> {
if (process.platform !== 'win32') {
await whichSpawn(path.resolve(__dirname, 'test-lock-deduplicated.sh'));
}
await Promise.resolve();
}

View File

@ -28,7 +28,7 @@ import { GlobSync } from 'glob';
*
* IMPORTANT
*
* All commands need to be stored under a folder in src/commands to maintain uniformity
* All commands need to be stored under a folder in lib/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.
@ -36,6 +36,9 @@ import { GlobSync } from 'glob';
* 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.
*
* Resources with plural names needs to have 2 sections if they have commands like:
* "fleet, fleets" or "device, devices" or "tag, tags"
*
*/
interface Category {
@ -52,15 +55,21 @@ interface Documentation {
// Mapping folders names to custom headings in the docs
const commandHeadings: { [key: string]: string } = {
'api-key': 'API Keys',
'api-keys': 'API Keys',
login: 'Authentication',
whoami: 'Authentication',
logout: 'Authentication',
env: 'Environment Variables',
envs: 'Environment Variables',
help: 'Help and Version',
'ssh-key': 'SSH Keys',
organization: 'Organizations',
key: 'SSH Keys',
keys: 'SSH Keys',
orgs: 'Organizations',
os: 'OS',
util: 'Utilities',
ssh: 'Network',
scan: 'Network',
tunnel: 'Network',
build: 'Deploy',
join: 'Platform',
leave: 'Platform',
@ -145,7 +154,7 @@ export async function getCapitanoDoc(): Promise<typeof capitanoDoc> {
throw new Error(`Error parsing section title`);
}
// match[1] has the title, match[2] has the rest
return match?.[2];
return match && match[2];
}),
mdParser.getSectionOfTitle('Installation'),
mdParser.getSectionOfTitle('Choosing a shell (command prompt/terminal)'),

View File

@ -26,7 +26,7 @@ export interface Document {
export interface Category {
title: string;
commands: Array<OclifCommand & { name: string }>;
commands: OclifCommand[];
}
export { OclifCommand };

View File

@ -18,6 +18,7 @@ import * as path from 'path';
import { getCapitanoDoc } from './capitanodoc';
import type { Category, Document, OclifCommand } from './doc-types';
import * as markdown from './markdown';
import { stripIndent } from '../../lib/utils/lazy';
/**
* 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) {
category.commands.push(await importOclifCommands(jsFilename));
category.commands.push(...importOclifCommands(jsFilename));
}
result.categories.push(category);
}
@ -46,23 +47,48 @@ export async function renderMarkdown(): Promise<string> {
return markdown.render(result);
}
async function importOclifCommands(jsFilename: string) {
const command = (await import(path.join(process.cwd(), jsFilename)))
.default as OclifCommand;
// Help is now managed via a plugin
// This fake command allows capitanodoc to include help in docs
class FakeHelpCommand {
description = stripIndent`
List balena commands, or get detailed help for a specific command.
return {
...command,
// build/commands/device/index.js -> device
// build/commands/device/list.js -> device list
name: jsFilename
.split('/')
.slice(2)
.join(' ')
.split('.')
.slice(0, 1)
.join(' ')
.split(' index')[0],
} as Category['commands'][0];
List balena commands, or get detailed help for a specific command.
`;
examples = [
'$ balena help',
'$ balena help login',
'$ balena help os download',
];
args = {
command: {
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];
}
/**

View File

@ -18,27 +18,12 @@ import { Parser } from '@oclif/core';
import * as ent from 'ent';
import * as _ from 'lodash';
import { capitanoizeOclifUsage } from '../../src/utils/oclif-utils';
import type { Category, Document } from './doc-types';
import { getManualSortCompareFunction } from '../../lib/utils/helpers';
import { capitanoizeOclifUsage } from '../../lib/utils/oclif-utils';
import type { Category, Document, OclifCommand } from './doc-types';
function renderOclifCommand(command: Category['commands'][0]): string[] {
const result = [`## ${ent.encode(command.name || '')}`];
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');
function renderOclifCommand(command: OclifCommand): string[] {
const result = [`## ${ent.encode(command.usage || '')}`];
const description = (command.description || '')
.split('\n')
.slice(1) // remove the first line, which oclif uses as help header
@ -95,7 +80,7 @@ function renderToc(categories: Category[]): string[] {
result.push(
category.commands
.map((command) => {
const signature = capitanoizeOclifUsage(command.name);
const signature = capitanoizeOclifUsage(command.usage);
return `\t- [${ent.encode(signature)}](${getAnchor(signature)})`;
})
.join('\n'),
@ -104,7 +89,33 @@ function renderToc(categories: Category[]): string[] {
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) {
sortCommands(doc);
const result = [
`# ${doc.title}`,
doc.introduction,

View File

@ -15,9 +15,41 @@
* limitations under the License.
*/
import type { OptionDefinition } from 'capitano';
import * as ent from 'ent';
import * as fs from 'fs';
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 {
constructor(public mdFilePath: string) {}

View File

@ -43,8 +43,8 @@ async function checkBuildTimestamps() {
...gitStatus.staged,
...gitStatus.renamed.map((o) => o.to),
])
// select only staged files that start with src/ or typings/
.filter((f) => f.match(/^(src|typings)[/\\]/))
// select only staged files that start with lib/ or typings/
.filter((f) => f.match(/^(lib|typings)[/\\]/))
.map((f) => path.join(ROOT, f));
const fStats = await Promise.all(stagedFiles.map((f) => fs.stat(f)));

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],
}),
)) as { html_url: any };
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 = 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

@ -19,11 +19,15 @@ import * as _ from 'lodash';
import {
buildOclifInstaller,
buildStandalone,
buildStandaloneZip,
catchUncommitted,
signFilesForNotarization,
testShrinkwrap,
} from './build-bin';
import {
release,
updateDescriptionOfReleasesAffectedByIssue1359,
} from './deploy-bin';
// DEBUG set to falsy for negative values else is truthy
process.env.DEBUG = ['0', 'no', 'false', '', undefined].includes(
@ -36,7 +40,8 @@ process.env.DEBUG = ['0', 'no', 'false', '', undefined].includes(
* Trivial command-line parser. Check whether the command-line argument is one
* of the following strings, then call the appropriate functions:
* 'build:installer' (to build a native oclif installer)
* 'build:standalone' (to build a standalone 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))
*/
@ -49,10 +54,12 @@ async function parse(args?: string[]) {
}
const commands: { [cmd: string]: () => void | Promise<void> } = {
'build:installer': buildOclifInstaller,
'build:standalone': buildStandalone,
'build:standalone': buildStandaloneZip,
'sign:binaries': signFilesForNotarization,
'catch-uncommitted': catchUncommitted,
'test-shrinkwrap': testShrinkwrap,
fix1359: updateDescriptionOfReleasesAffectedByIssue1359,
release,
};
for (const arg of args) {
if (!Object.hasOwn(commands, arg)) {

View File

@ -3,7 +3,7 @@ import * as semver from 'semver';
const changeTypes = ['major', 'minor', 'patch'] as const;
const validateChangeType = (maybeChangeType = 'minor') => {
const validateChangeType = (maybeChangeType: string = 'minor') => {
maybeChangeType = maybeChangeType.toLowerCase();
switch (maybeChangeType) {
case 'patch':
@ -136,4 +136,5 @@ async function main() {
}
}
void main();
// eslint-disable-next-line @typescript-eslint/no-floating-promises
main();

View File

@ -17,16 +17,74 @@
import { spawn } from 'child_process';
import * as path from 'path';
import * as fs from 'fs';
import * as whichMod from 'which';
export const ROOT = path.join(__dirname, '..');
export function loadPackageJson() {
const packageJsonPath = path.join(ROOT, 'package.json');
/** Tap and buffer this process' stdout and stderr */
export class StdOutTap {
public stdoutBuf: string[] = [];
public stderrBuf: string[] = [];
public allBuf: string[] = []; // both stdout and stderr
const packageJson = fs.readFileSync(packageJsonPath, 'utf8');
return JSON.parse(packageJson);
protected origStdoutWrite: typeof process.stdout.write;
protected origStderrWrite: typeof process.stdout.write;
constructor(protected printDots = false) {}
tap() {
this.origStdoutWrite = process.stdout.write;
this.origStderrWrite = process.stderr.write;
process.stdout.write = (chunk: string, ...args: any[]): boolean => {
this.stdoutBuf.push(chunk);
this.allBuf.push(chunk);
const str = this.printDots ? '.' : chunk;
return this.origStdoutWrite.call(process.stdout, str, ...args);
};
process.stderr.write = (chunk: string, ...args: any[]): boolean => {
this.stderrBuf.push(chunk);
this.allBuf.push(chunk);
const str = this.printDots ? '.' : chunk;
return this.origStderrWrite.call(process.stderr, str, ...args);
};
}
untap() {
process.stdout.write = this.origStdoutWrite;
process.stderr.write = this.origStderrWrite;
if (this.printDots) {
console.error('');
}
}
}
/**
* Diff strings by line, using the 'diff' npm package:
* https://www.npmjs.com/package/diff
*/
export function diffLines(str1: string, str2: string): string {
const { diffTrimmedLines } = require('diff');
const diffObjs = diffTrimmedLines(str1, str2);
const prefix = (chunk: string, char: string) =>
chunk
.split('\n')
.map((line: string) => `${char} ${line}`)
.join('\n');
const diffStr = diffObjs
.map((part: any) => {
return part.added
? prefix(part.value, '+')
: part.removed
? prefix(part.value, '-')
: prefix(part.value, ' ');
})
.join('\n');
return diffStr;
}
export function loadPackageJson() {
return require(path.join(ROOT, 'package.json'));
}
/**
@ -39,6 +97,7 @@ export function loadPackageJson() {
* @returns The program's full path, e.g. 'C:\WINDOWS\System32\OpenSSH\ssh.EXE'
*/
export async function which(program: string): Promise<string> {
const whichMod = await import('which');
let programPath: string;
try {
programPath = await whichMod(program);
@ -69,7 +128,7 @@ export async function whichSpawn(
.on('error', reject)
.on('close', resolve);
} catch (err) {
reject(err as Error);
reject(err);
}
});
} catch (err) {

View File

@ -29,7 +29,7 @@ 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/
// pointing oclif config options to lib/ instead of build/
modifyOclifPaths();
// Undo changes on exit
process.on('exit', function () {
@ -57,12 +57,9 @@ require('ts-node').register({
project: path.join(rootDir, 'tsconfig.json'),
transpileOnly: true,
});
void require('../src/app').run(undefined, {
dir: __dirname,
development: true,
});
require('../lib/app').run(undefined, { dir: __dirname, development: true });
// Modify package.json oclif paths from build/ -> src/, or vice versa
// 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');
@ -76,9 +73,9 @@ function modifyOclifPaths(revert) {
let oclifSectionText = JSON.stringify(packageObj.oclif);
if (!revert) {
oclifSectionText = oclifSectionText.replace(/\/build\//g, '/src/');
oclifSectionText = oclifSectionText.replace(/\/build\//g, '/lib/');
} else {
oclifSectionText = oclifSectionText.replace(/\/src\//g, '/build/');
oclifSectionText = oclifSectionText.replace(/\/lib\//g, '/build/');
}
packageObj.oclif = JSON.parse(oclifSectionText);

View File

@ -5,7 +5,7 @@
process.env.UV_THREADPOOL_SIZE = '64';
// Disable oclif registering ts-node
process.env.OCLIF_TS_NODE = '0';
process.env.OCLIF_TS_NODE = 0;
async function run() {
// Use fast-boot to cache require lookups, speeding up startup
@ -18,4 +18,4 @@ async function run() {
await require('../build/app').run(undefined, { dir: __dirname });
}
void run();
run();

View File

@ -8,28 +8,27 @@ _balena() {
local context state line curcontext="$curcontext"
# 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=( api-key api-keys app block build config deploy device device devices env envs fleet fleet fleets internal join key key keys leave local login logout logs notes orgs os preload push release release releases scan settings ssh support tag tags tunnel util version whoami )
# Sub-completions
api_key_cmds=( generate list revoke )
api_key_cmds=( generate revoke )
app_cmds=( create )
block_cmds=( create )
config_cmds=( generate inject read reconfigure write )
device_type_cmds=( list )
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 )
env_cmds=( list rename rm set )
fleet_cmds=( create list pin purge rename restart rm track-latest )
device_cmds=( deactivate identify init local-mode move os-update pin public-url purge reboot register rename restart rm shutdown start-service stop-service track-fleet )
devices_cmds=( supported )
env_cmds=( add rename rm )
fleet_cmds=( create pin purge rename restart rm track-latest )
internal_cmds=( osinit )
key_cmds=( add rm )
local_cmds=( configure flash )
organization_cmds=( list )
os_cmds=( build-config configure download initialize versions )
release_cmds=( finalize invalidate list validate )
ssh_key_cmds=( add list rm )
tag_cmds=( list rm set )
release_cmds=( finalize invalidate validate )
tag_cmds=( rm set )
_arguments -C \
'(- 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' \
'2:second command:_balena_sec_cmds' \
&& ret=0
@ -55,12 +54,12 @@ _balena_sec_cmds() {
"config")
_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")
_describe -t device_cmds 'device_cmd' device_cmds "$@" && ret=0
;;
"devices")
_describe -t devices_cmds 'devices_cmd' devices_cmds "$@" && ret=0
;;
"env")
_describe -t env_cmds 'env_cmd' env_cmds "$@" && ret=0
;;
@ -70,21 +69,18 @@ _balena_sec_cmds() {
"internal")
_describe -t internal_cmds 'internal_cmd' internal_cmds "$@" && ret=0
;;
"key")
_describe -t key_cmds 'key_cmd' key_cmds "$@" && ret=0
;;
"local")
_describe -t local_cmds 'local_cmd' local_cmds "$@" && ret=0
;;
"organization")
_describe -t organization_cmds 'organization_cmd' organization_cmds "$@" && ret=0
;;
"os")
_describe -t os_cmds 'os_cmd' os_cmds "$@" && ret=0
;;
"release")
_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")
_describe -t tag_cmds 'tag_cmd' tag_cmds "$@" && ret=0
;;

View File

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

View File

@ -33,7 +33,7 @@ const commandsJson = JSON.parse(fs.readFileSync(commandsFilePath, 'utf8'));
const mainCommands = [];
const additionalCommands = [];
for (const key of Object.keys(commandsJson.commands).sort()) {
for (const key of Object.keys(commandsJson.commands)) {
const cmd = key.split(':');
if (cmd.length > 1) {
additionalCommands.push(cmd);

View File

@ -14,7 +14,7 @@ $sub_cmds$
_arguments -C \
'(- 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' \
'2:second command:_balena_sec_cmds' \
&& 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

@ -34,14 +34,18 @@ export const setupSentry = onceAsync(async () => {
const config = await import('./config');
const Sentry = await import('@sentry/node');
Sentry.init({
autoSessionTracking: false,
dsn: config.sentryDsn,
release: packageJSON.version,
});
Sentry.getCurrentScope().setExtras({
is_pkg: !!(process as any).pkg,
node_version: process.version,
platform: process.platform,
Sentry.configureScope((scope) => {
scope.setExtras({
is_pkg: !!(process as any).pkg,
node_version: process.version,
platform: process.platform,
});
});
return Sentry.getCurrentHub();
});
async function checkNodeVersion() {
@ -97,9 +101,11 @@ async function init() {
/** Execute the oclif parser and the CLI command. */
async function oclifRun(command: string[], options: AppOptions) {
let deprecationPromise: Promise<void> | undefined;
let deprecationPromise: Promise<void>;
// 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 = new DeprecationChecker(packageJSON.version);
// warnAndAbortIfDeprecated uses previously cached data only
@ -131,8 +137,7 @@ async function oclifRun(command: string[], options: AppOptions) {
}
}
if (shouldFlush) {
const { flush } = await import('@oclif/core');
await flush();
await import('@oclif/core/flush');
}
// TODO: figure out why we need to call fast-boot stop() here, in
// addition to calling it in the main `run()` function in this file.
@ -147,7 +152,7 @@ async function oclifRun(command: string[], options: AppOptions) {
}
})(!options.noFlush);
const { trackPromise } = await import('./hooks/prerun');
const { trackPromise } = await import('./hooks/prerun/track');
await Promise.all([trackPromise, deprecationPromise, runPromise]);
}
@ -155,12 +160,18 @@ async function oclifRun(command: string[], options: AppOptions) {
/** CLI entrypoint. Called by the `bin/run.js` and `bin/dev.js` scripts. */
export async function run(cliArgs = process.argv, options: AppOptions) {
try {
const { setOfflineModeEnvVars, normalizeEnvVars } = await import(
const { setOfflineModeEnvVars, normalizeEnvVars, pkgExec } = await import(
'./utils/bootstrap'
);
setOfflineModeEnvVars();
normalizeEnvVars();
// The 'pkgExec' special/internal command provides a Node.js interpreter
// for use of the standalone zip package. See pkgExec function.
if (cliArgs.length > 3 && cliArgs[2] === 'pkgExec') {
return pkgExec(cliArgs[3], cliArgs.slice(4));
}
await init();
// Look for commands that have been removed and if so, exit with a notice

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/core';
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,76 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Args } from '@oclif/core';
import Command from '../../command';
import { ExpectedError } from '../../errors';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
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: Args.string({
description: 'the API key name',
required: true,
}),
};
public static usage = 'api-key generate <name>';
public static flags = {
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params } = await this.parse(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,7 +15,9 @@
* limitations under the License.
*/
import { Args, Command } from '@oclif/core';
import { Args } from '@oclif/core';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
export default class RevokeCmd extends Command {
@ -39,6 +41,12 @@ export default class RevokeCmd extends Command {
}),
};
public static usage = 'api-key revoke <ids>';
public static flags = {
help: cf.help,
};
public static authenticated = true;
public async run() {
@ -50,9 +58,9 @@ export default class RevokeCmd extends Command {
return;
}
await Promise.all(
apiKeyIds.map(async (id) => {
await getBalenaSdk().models.apiKey.revoke(Number(id));
}),
apiKeyIds.map(
async (id) => await getBalenaSdk().models.apiKey.revoke(Number(id)),
),
);
console.log('Successfully revoked the given API keys');
}

View File

@ -15,14 +15,12 @@
* limitations under the License.
*/
import { Flags, Command } from '@oclif/core';
import { Flags } from '@oclif/core';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
export default class APIKeyListCmd extends Command {
public static aliases = ['api-keys'];
public static deprecateAliases = true;
export default class ApiKeysCmd extends Command {
public static description = stripIndent`
Print a list of balenaCloud API keys.
@ -30,9 +28,12 @@ export default class APIKeyListCmd extends Command {
Print a list of balenaCloud API keys for the current user or for a specific fleet with the \`--fleet\` option.
`;
public static examples = ['$ balena api-key list'];
public static examples = ['$ balena api-keys'];
public static usage = 'api-keys';
public static flags = {
help: cf.help,
user: Flags.boolean({
char: 'u',
description: 'show API keys for your user',
@ -43,7 +44,7 @@ export default class APIKeyListCmd extends Command {
public static authenticated = true;
public async run() {
const { flags: options } = await this.parse(APIKeyListCmd);
const { flags: options } = await this.parse(ApiKeysCmd);
const { getApplication } = await import('../../utils/sdk');
const actorId = options.fleet
@ -51,7 +52,7 @@ export default class APIKeyListCmd extends Command {
await getApplication(getBalenaSdk(), options.fleet, {
$select: 'actor',
})
).actor.__id
).actor
: await getBalenaSdk().auth.getActorId();
const keys = await getBalenaSdk().pine.get({
resource: 'api_key',

View File

@ -15,7 +15,10 @@
* limitations under the License.
*/
import { Flags, Args, Command } from '@oclif/core';
import { Flags, Args } from '@oclif/core';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { stripIndent } from '../../utils/lazy';
export default class AppCreateCmd extends Command {
@ -27,10 +30,10 @@ export default class AppCreateCmd extends Command {
You can specify the organization the app should belong to using
the \`--organization\` option. The organization's handle, not its name,
should be provided. Organization handles can be listed with the
\`balena organization list\` command.
\`balena orgs\` command.
The app's default device type is specified with the \`--type\` option.
The \`balena device-type list\` command can be used to list the available
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
@ -53,6 +56,8 @@ export default class AppCreateCmd extends Command {
}),
};
public static usage = 'app create <name>';
public static flags = {
organization: Flags.string({
char: 'o',
@ -61,8 +66,9 @@ export default class AppCreateCmd extends Command {
type: Flags.string({
char: 't',
description:
'app device type (Check available types with `balena device-type list`)',
'app device type (Check available types with `balena devices supported`)',
}),
help: cf.help,
};
public static authenticated = true;

View File

@ -15,7 +15,10 @@
* limitations under the License.
*/
import { Flags, Args, Command } from '@oclif/core';
import { Flags, Args } from '@oclif/core';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { stripIndent } from '../../utils/lazy';
export default class BlockCreateCmd extends Command {
@ -27,10 +30,10 @@ export default class BlockCreateCmd extends Command {
You can specify the organization the block should belong to using
the \`--organization\` option. The organization's handle, not its name,
should be provided. Organization handles can be listed with the
\`balena organization list\` command.
\`balena orgs\` command.
The block's default device type is specified with the \`--type\` option.
The \`balena device-type list\` command can be used to list the available
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
@ -53,6 +56,8 @@ export default class BlockCreateCmd extends Command {
}),
};
public static usage = 'block create <name>';
public static flags = {
organization: Flags.string({
char: 'o',
@ -61,8 +66,9 @@ export default class BlockCreateCmd extends Command {
type: Flags.string({
char: 't',
description:
'block device type (Check available types with `balena device-type list`)',
'block device type (Check available types with `balena devices supported`)',
}),
help: cf.help,
};
public static authenticated = true;

View File

@ -15,7 +15,8 @@
* limitations under the License.
*/
import { Args, Flags, Command } from '@oclif/core';
import { Args, Flags } from '@oclif/core';
import Command from '../../command';
import { getBalenaSdk } from '../../utils/lazy';
import * as cf from '../../utils/common-flags';
import * as compose from '../../utils/compose';
@ -36,16 +37,15 @@ import { buildProject, composeCliFlags } from '../../utils/compose_ts';
import type { BuildOpts, DockerCliFlags } from '../../utils/docker';
import { dockerCliFlags } from '../../utils/docker';
type ComposeGenerateOptsParam = Parameters<typeof compose.generateOpts>[0];
interface PrepareBuildOpts
extends ComposeCliFlags,
DockerCliFlags,
ComposeGenerateOptsParam {
// TODO: For this special one we can't use Interfaces.InferredFlags/InferredArgs
// because of the 'registry-secrets' type which is defined in the actual code
// as a path (string | undefined) but then the cli turns it into an object
interface FlagsDef extends ComposeCliFlags, DockerCliFlags {
arch?: string;
deviceType?: string;
fleet?: string;
source?: string;
source?: string; // Not part of command profile - source param copied here.
help: void;
}
export default class BuildCmd extends Command {
@ -84,6 +84,8 @@ ${dockerignoreHelp}
source: Args.string({ description: 'path of project source directory' }),
};
public static usage = 'build [source]';
public static flags = {
arch: Flags.string({
description: 'the architecture to build for',
@ -96,6 +98,9 @@ ${dockerignoreHelp}
fleet: cf.fleet,
...composeCliFlags,
...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;
@ -103,43 +108,38 @@ ${dockerignoreHelp}
public async run() {
const { args: params, flags: options } = await this.parse(BuildCmd);
const Logger = await import('../../utils/logger');
const { checkLoggedInIf } = await import('../../utils/patterns');
await checkLoggedInIf(!!options.fleet);
await Command.checkLoggedInIf(!!options.fleet);
(await import('events')).defaultMaxListeners = 1000;
const sdk = getBalenaSdk();
const logger = Logger.getLogger();
const logger = await Command.getLogger();
logger.logDebug('Parsing input...');
const prepareBuildOpts = {
...options,
source: params.source,
};
// `build` accepts `source` as a parameter, but compose expects it as an option
options.source = params.source;
delete params.source;
await this.resolveArchFromDeviceType(sdk, prepareBuildOpts);
await this.resolveArchFromDeviceType(sdk, options);
await this.validateOptions(prepareBuildOpts, sdk);
await this.validateOptions(options, sdk);
// Build args are under consideration for removal - warn user
if (prepareBuildOpts.buildArg) {
if (options.buildArg) {
console.log(buildArgDeprecation);
}
const app = await this.getAppAndResolveArch(prepareBuildOpts);
const app = await this.getAppAndResolveArch(options);
const { docker, buildOpts, composeOpts } =
await this.prepareBuild(prepareBuildOpts);
const { docker, buildOpts, composeOpts } = await this.prepareBuild(options);
try {
await this.buildProject(docker, logger, composeOpts, {
appType: app?.application_type?.[0],
arch: prepareBuildOpts.arch!,
deviceType: prepareBuildOpts.deviceType!,
buildEmulated: prepareBuildOpts.emulated,
arch: options.arch!,
deviceType: options.deviceType!,
buildEmulated: options.emulated,
buildOpts,
});
} catch (err) {
@ -151,7 +151,7 @@ ${dockerignoreHelp}
logger.logSuccess('Build succeeded!');
}
protected async validateOptions(opts: PrepareBuildOpts, sdk: BalenaSDK) {
protected async validateOptions(opts: FlagsDef, sdk: BalenaSDK) {
// Validate option combinations
if (
(opts.fleet == null && (opts.arch == null || opts.deviceType == null)) ||
@ -179,10 +179,7 @@ ${dockerignoreHelp}
opts['registry-secrets'] = registrySecrets;
}
protected async resolveArchFromDeviceType(
sdk: BalenaSDK,
opts: PrepareBuildOpts,
) {
protected async resolveArchFromDeviceType(sdk: BalenaSDK, opts: FlagsDef) {
if (opts.deviceType != null && opts.arch == null) {
try {
const deviceTypeOpts = {
@ -215,7 +212,7 @@ ${dockerignoreHelp}
}
}
protected async getAppAndResolveArch(opts: PrepareBuildOpts) {
protected async getAppAndResolveArch(opts: FlagsDef) {
if (opts.fleet) {
const { getAppWithArch } = await import('../../utils/helpers');
const app = await getAppWithArch(opts.fleet);
@ -225,7 +222,7 @@ ${dockerignoreHelp}
}
}
protected async prepareBuild(options: PrepareBuildOpts) {
protected async prepareBuild(options: FlagsDef) {
const { getDocker, generateBuildOpts } = await import('../../utils/docker');
const [docker, buildOpts, composeOpts] = await Promise.all([
getDocker(options),

View File

@ -15,8 +15,9 @@
* limitations under the License.
*/
import { Flags, Command } from '@oclif/core';
import { Flags } from '@oclif/core';
import type { Interfaces } from '@oclif/core';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, getCliForm, stripIndent } from '../../utils/lazy';
import {
@ -59,6 +60,8 @@ export default class ConfigGenerateCmd extends Command {
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 15',
];
public static usage = 'config generate';
public static flags = {
version: Flags.string({
description: 'a balenaOS version',
@ -82,7 +85,7 @@ export default class ConfigGenerateCmd extends Command {
}),
deviceType: Flags.string({
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({
description: 'generate a fresh device key for the device',
@ -117,6 +120,7 @@ export default class ConfigGenerateCmd extends Command {
'expiry date assigned to generated provisioning api key (format: YYYY-MM-DD)',
exclusive: ['device'],
}),
help: cf.help,
};
public static authenticated = true;

View File

@ -15,7 +15,8 @@
* limitations under the License.
*/
import { Args, Command } from '@oclif/core';
import { Args } from '@oclif/core';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getVisuals, stripIndent } from '../../utils/lazy';
@ -42,8 +43,11 @@ export default class ConfigInjectCmd extends Command {
}),
};
public static usage = 'config inject <file>';
public static flags = {
drive: cf.driveOrImg,
help: cf.help,
};
public static root = true;
@ -64,12 +68,7 @@ export default class ConfigInjectCmd extends Command {
);
const config = await import('balena-config-json');
await config.write(
drive,
// Will be removed in the next major of balena-config-json
undefined,
configJSON,
);
await config.write(drive, '', configJSON);
console.info('Done');
}

View File

@ -15,7 +15,7 @@
* limitations under the License.
*/
import { Command } from '@oclif/core';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getVisuals, stripIndent } from '../../utils/lazy';
@ -36,8 +36,11 @@ export default class ConfigReadCmd extends Command {
'$ balena config read --drive balena.img',
];
public static usage = 'config read';
public static flags = {
drive: cf.driveOrImg,
help: cf.help,
json: cf.json,
};
@ -54,7 +57,7 @@ export default class ConfigReadCmd extends Command {
await safeUmount(drive);
const config = await import('balena-config-json');
const configJSON = await config.read(drive);
const configJSON = await config.read(drive, '');
if (options.json) {
console.log(JSON.stringify(configJSON, null, 4));

View File

@ -15,7 +15,8 @@
* limitations under the License.
*/
import { Flags, Command } from '@oclif/core';
import { Flags } from '@oclif/core';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getVisuals, stripIndent } from '../../utils/lazy';
@ -38,12 +39,15 @@ export default class ConfigReconfigureCmd extends Command {
'$ balena config reconfigure --drive balena.img --advanced',
];
public static usage = 'config reconfigure';
public static flags = {
drive: cf.driveOrImg,
advanced: Flags.boolean({
description: 'show advanced commands',
char: 'v',
}),
help: cf.help,
version: Flags.string({
description: 'balenaOS version, for example "2.32.0" or "2.44.0+rev1"',
}),
@ -62,7 +66,7 @@ export default class ConfigReconfigureCmd extends Command {
await safeUmount(drive);
const config = await import('balena-config-json');
const { uuid } = await config.read(drive);
const { uuid } = await config.read(drive, '');
await safeUmount(drive);
if (!uuid) {

View File

@ -15,7 +15,8 @@
* limitations under the License.
*/
import { Args, Command } from '@oclif/core';
import { Args } from '@oclif/core';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getVisuals, stripIndent } from '../../utils/lazy';
@ -47,8 +48,11 @@ export default class ConfigWriteCmd extends Command {
}),
};
public static usage = 'config write <key> <value>';
public static flags = {
drive: cf.driveOrImg,
help: cf.help,
};
public static root = true;
@ -64,19 +68,14 @@ export default class ConfigWriteCmd extends Command {
await safeUmount(drive);
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}`);
ConfigWriteCmd.updateConfigJson(configJSON, params.key, params.value);
await denyMount(drive, async () => {
await safeUmount(drive);
await config.write(
drive,
// Will be removed in the next major of balena-config-json
undefined,
configJSON,
);
await config.write(drive, '', configJSON);
});
console.info('Done');

View File

@ -15,8 +15,10 @@
* limitations under the License.
*/
import { Args, Flags, Command } from '@oclif/core';
import { Args, Flags } from '@oclif/core';
import type { ImageDescriptor } from '@balena/compose/dist/parse';
import Command from '../../command';
import { ExpectedError } from '../../errors';
import { getBalenaSdk, getChalk, stripIndent } from '../../utils/lazy';
import {
@ -60,6 +62,7 @@ interface FlagsDef extends ComposeCliFlags, DockerCliFlags {
'release-tag'?: string[];
draft: boolean;
note?: string;
help: void;
}
export default class DeployCmd extends Command {
@ -105,6 +108,8 @@ ${dockerignoreHelp}
image: Args.string({ description: 'the image to deploy' }),
};
public static usage = 'deploy <fleet> [image]';
public static flags = {
source: Flags.string({
description:
@ -138,6 +143,9 @@ ${dockerignoreHelp}
note: Flags.string({ description: 'The notes for this release' }),
...composeCliFlags,
...dockerCliFlags,
// NOTE: Not supporting -h for help, because of clash with -h in DockerCliFlags
// Revisit this in future release.
help: Flags.help({}),
};
public static authenticated = true;
@ -149,9 +157,7 @@ ${dockerignoreHelp}
(await import('events')).defaultMaxListeners = 1000;
const Logger = await import('../../utils/logger');
const logger = Logger.getLogger();
const logger = await Command.getLogger();
logger.logDebug('Parsing input...');
const { fleet, image } = params;
@ -358,17 +364,23 @@ ${dockerignoreHelp}
$select: ['commit'],
});
} else {
const [{ id: userId }, auth, apiEndpoint] = await Promise.all([
sdk.auth.getUserInfo(),
sdk.auth.getToken(),
sdk.settings.get('apiUrl'),
]);
release = await $deployProject(
docker,
sdk,
logger,
project.composition,
images,
opts.app.id,
userId,
`Bearer ${auth}`,
apiEndpoint,
!opts.shouldUploadLogs,
composeOpts.projectPath,
opts.createAsDraft,
project.descriptors,
);
}

View File

@ -15,7 +15,8 @@
* limitations under the License.
*/
import { Args, Command } from '@oclif/core';
import { Args } from '@oclif/core';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
@ -40,8 +41,11 @@ export default class DeviceDeactivateCmd extends Command {
}),
};
public static usage = 'device deactivate <uuid>';
public static flags = {
yes: cf.yes,
help: cf.help,
};
public static authenticated = true;

View File

@ -15,7 +15,9 @@
* limitations under the License.
*/
import { Args, Command } from '@oclif/core';
import { Args } from '@oclif/core';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { ExpectedError } from '../../errors';
@ -34,6 +36,12 @@ export default class DeviceIdentifyCmd extends Command {
}),
};
public static usage = 'device identify <uuid>';
public static flags = {
help: cf.help,
};
public static authenticated = true;
public async run() {

View File

@ -15,7 +15,8 @@
* limitations under the License.
*/
import { Flags, Args, Command } from '@oclif/core';
import { Flags, Args } from '@oclif/core';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { expandForAppName } from '../../utils/helpers';
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
@ -61,8 +62,11 @@ export default class DeviceCmd extends Command {
}),
};
public static usage = 'device <uuid>';
public static flags = {
json: cf.json,
help: cf.help,
view: Flags.boolean({
default: false,
description: 'open device dashboard page',
@ -77,59 +81,45 @@ export default class DeviceCmd extends Command {
const balena = getBalenaSdk();
let device: ExtendedDevice;
if (options.json) {
const [deviceBase, deviceComputed] = await Promise.all([
balena.models.device.get(params.uuid, {
$expand: {
device_tag: {
$select: ['tag_key', 'value'],
const device = (await balena.models.device.get(
params.uuid,
options.json
? {
$expand: {
device_tag: {
$select: ['tag_key', 'value'],
},
...expandForAppName.$expand,
},
...expandForAppName.$expand,
}
: {
$select: [
'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,
},
}),
balena.models.device.get(params.uuid, {
$select: [
'overall_status',
'overall_progress',
'should_be_running__release',
],
}),
]);
device = {
...deviceBase,
...deviceComputed,
} as ExtendedDevice;
} else {
device = (await balena.models.device.get(params.uuid, {
$select: [
'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;
}
)) as ExtendedDevice;
if (options.view) {
const open = await import('open');

View File

@ -15,7 +15,8 @@
* limitations under the License.
*/
import { Flags, Command } from '@oclif/core';
import { Flags } from '@oclif/core';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { applicationIdInfo } from '../../utils/messages';
@ -28,6 +29,7 @@ interface FlagsDef {
'os-version'?: string;
drive?: string;
config?: string;
help: void;
'provisioning-key-name'?: string;
'provisioning-key-expiry-date'?: string;
}
@ -41,17 +43,17 @@ export default class DeviceInitCmd extends Command {
This command effectively combines several other balena CLI commands in one,
namely:
'balena device register'
'balena os download'
'balena os build-config' or 'balena config generate'
'balena os configure'
'balena device register'
'balena os download'
'balena os build-config' or 'balena config generate'
'balena os configure'
'balena os local flash'
Possible arguments for the '--fleet', '--os-version' and '--drive' options can
be listed respectively with the commands:
'balena fleet list'
'balena os versions'
'balena fleets'
'balena os versions'
'balena util available-drives'
If the '--fleet' or '--drive' options are omitted, interactive menus will be
@ -72,6 +74,8 @@ 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',
];
public static usage = 'device init';
public static flags = {
fleet: cf.fleet,
yes: cf.yes,
@ -99,6 +103,7 @@ export default class DeviceInitCmd extends Command {
description:
'expiry date assigned to generated provisioning api key (format: YYYY-MM-DD)',
}),
help: cf.help,
};
public static authenticated = true;
@ -114,9 +119,8 @@ export default class DeviceInitCmd extends Command {
tmp.setGracefulCleanup();
const { downloadOSImage } = await import('../../utils/cloud');
const { getApplication } = await import('../../utils/sdk');
const Logger = await import('../../utils/logger');
const logger = Logger.getLogger();
const logger = await Command.getLogger();
const balena = getBalenaSdk();
// Get application and
@ -155,7 +159,7 @@ export default class DeviceInitCmd extends Command {
try {
logger.logDebug(`Process failed, removing device ${device.uuid}`);
await balena.models.device.remove(device.uuid);
} catch {
} catch (e) {
// Ignore removal failures, and throw original error
}
throw e;

View File

@ -15,7 +15,9 @@
* limitations under the License.
*/
import { Flags, Args, Command } from '@oclif/core';
import { Flags, Args } from '@oclif/core';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
export default class DeviceLocalModeCmd extends Command {
@ -40,6 +42,8 @@ export default class DeviceLocalModeCmd extends Command {
}),
};
public static usage = 'device local-mode <uuid>';
public static flags = {
enable: Flags.boolean({
description: 'enable local mode',
@ -53,6 +57,7 @@ export default class DeviceLocalModeCmd extends Command {
description: 'output boolean indicating local mode status',
exclusive: ['enable', 'disable'],
}),
help: cf.help,
};
public static authenticated = true;

View File

@ -15,13 +15,14 @@
* limitations under the License.
*/
import { Args, Command } from '@oclif/core';
import { Args } from '@oclif/core';
import type {
BalenaSDK,
Device,
PineOptions,
PineTypedResult,
} from 'balena-sdk';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { ExpectedError } from '../../errors';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
@ -53,8 +54,11 @@ export default class DeviceMoveCmd extends Command {
}),
};
public static usage = 'device move <uuid(s)>';
public static flags = {
fleet: cf.fleet,
help: cf.help,
};
public static authenticated = true;

View File

@ -15,12 +15,12 @@
* limitations under the License.
*/
import { Flags, Args, Command } from '@oclif/core';
import { Flags, Args } from '@oclif/core';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy';
import type { Device } from 'balena-sdk';
import { ExpectedError } from '../../errors';
import { getExpandedProp } from '../../utils/pine';
export default class DeviceOsUpdateCmd extends Command {
public static description = stripIndent`
@ -47,6 +47,8 @@ export default class DeviceOsUpdateCmd extends Command {
}),
};
public static usage = 'device os-update <uuid>';
public static flags = {
version: Flags.string({
description: 'a balenaOS version',
@ -58,6 +60,7 @@ export default class DeviceOsUpdateCmd extends Command {
exclusive: ['version'],
}),
yes: cf.yes,
help: cf.help,
};
public static authenticated = true;
@ -127,64 +130,27 @@ export default class DeviceOsUpdateCmd extends Command {
);
}
} 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({
message: 'Target OS version',
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');
// 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
await patterns.confirm(
options.yes || false,
'Host OS updates require a device restart when they complete. Are you sure you want to proceed?',
);
await sdk.models.device
.startOsUpdate(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);
});
await sdk.models.device.startOsUpdate(uuid, targetOsVersion);
await patterns.awaitDeviceOsUpdate(uuid, targetOsVersion);
}
}

View File

@ -15,7 +15,9 @@
* limitations under the License.
*/
import { Args, Command } from '@oclif/core';
import { Args } from '@oclif/core';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { getExpandedProp } from '../../utils/pine';
@ -42,6 +44,12 @@ export default class DevicePinCmd extends Command {
}),
};
public static usage = 'device pin <uuid> [releaseToPinTo]';
public static flags = {
help: cf.help,
};
public static authenticated = true;
public async run() {
@ -51,7 +59,7 @@ export default class DevicePinCmd extends Command {
const device = await balena.models.device.get(params.uuid, {
$expand: {
is_pinned_on__release: {
should_be_running__release: {
$select: 'commit',
},
belongs_to__application: {
@ -61,7 +69,7 @@ export default class DevicePinCmd extends Command {
});
const pinnedRelease = getExpandedProp(
device.is_pinned_on__release,
device.should_be_running__release,
'commit',
);
const appSlug = getExpandedProp(device.belongs_to__application, 'slug');
@ -74,7 +82,7 @@ export default class DevicePinCmd extends Command {
pinnedRelease
? `This device is currently pinned to ${pinnedRelease}.`
: 'This device is not currently pinned to any release.'
} \n\nTo see a list of all releases this device can be pinned to, run \`balena release list ${appSlug}\`.`,
} \n\nTo see a list of all releases this device can be pinned to, run \`balena releases ${appSlug}\`.`,
);
} else {
await balena.models.device.pinToRelease(params.uuid, releaseToPinTo);

View File

@ -15,8 +15,10 @@
* limitations under the License.
*/
import { Flags, Args, Command } from '@oclif/core';
import { Flags, Args } from '@oclif/core';
import Command from '../../command';
import { ExpectedError } from '../../errors';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
export default class DevicePublicUrlCmd extends Command {
@ -42,6 +44,8 @@ export default class DevicePublicUrlCmd extends Command {
}),
};
public static usage = 'device public-url <uuid>';
public static flags = {
enable: Flags.boolean({
description: 'enable the public URL',
@ -55,6 +59,7 @@ export default class DevicePublicUrlCmd extends Command {
description: 'determine if public URL is enabled',
exclusive: ['enable', 'disable'],
}),
help: cf.help,
};
public static authenticated = true;

View File

@ -15,7 +15,9 @@
* limitations under the License.
*/
import { Args, Command } from '@oclif/core';
import { Args } from '@oclif/core';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, getCliUx, stripIndent } from '../../utils/lazy';
export default class DevicePurgeCmd extends Command {
@ -33,6 +35,8 @@ export default class DevicePurgeCmd extends Command {
'$ balena device purge 55d43b3,23c73a1',
];
public static usage = 'device purge <uuid>';
public static args = {
uuid: Args.string({
description: 'comma-separated list (no blank spaces) of device UUIDs',
@ -40,6 +44,10 @@ export default class DevicePurgeCmd extends Command {
}),
};
public static flags = {
help: cf.help,
};
public static authenticated = true;
public async run() {

View File

@ -15,7 +15,8 @@
* limitations under the License.
*/
import { Args, Command } from '@oclif/core';
import { Args } from '@oclif/core';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
@ -34,8 +35,11 @@ export default class DeviceRebootCmd extends Command {
}),
};
public static usage = 'device reboot <uuid>';
public static flags = {
force: cf.force,
help: cf.help,
};
public static authenticated = true;

View File

@ -15,7 +15,9 @@
* limitations under the License.
*/
import { Flags, Command } from '@oclif/core';
import { Flags } from '@oclif/core';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import * as ca from '../../utils/common-args';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { applicationIdInfo } from '../../utils/messages';
@ -42,6 +44,8 @@ export default class DeviceRegisterCmd extends Command {
fleet: ca.fleetRequired,
};
public static usage = 'device register <fleet>';
public static flags = {
uuid: Flags.string({
description: 'custom uuid',
@ -49,8 +53,9 @@ export default class DeviceRegisterCmd extends Command {
}),
deviceType: Flags.string({
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;
@ -76,6 +81,6 @@ export default class DeviceRegisterCmd extends Command {
options.deviceType,
);
return result.uuid;
return result && result.uuid;
}
}

View File

@ -15,7 +15,9 @@
* limitations under the License.
*/
import { Args, Command } from '@oclif/core';
import { Args } from '@oclif/core';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy';
export default class DeviceRenameCmd extends Command {
@ -41,6 +43,12 @@ export default class DeviceRenameCmd extends Command {
}),
};
public static usage = 'device rename <uuid> [newName]';
public static flags = {
help: cf.help,
};
public static authenticated = true;
public async run() {

View File

@ -15,7 +15,9 @@
* limitations under the License.
*/
import { Flags, Args, Command } from '@oclif/core';
import { Flags, Args } from '@oclif/core';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, getCliUx, stripIndent } from '../../utils/lazy';
import type {
BalenaSDK,
@ -51,12 +53,15 @@ export default class DeviceRestartCmd extends Command {
}),
};
public static usage = 'device restart <uuid>';
public static flags = {
service: Flags.string({
description:
'comma-separated list (no blank spaces) of service names to restart',
char: 's',
}),
help: cf.help,
};
public static authenticated = true;
@ -154,7 +159,7 @@ export default class DeviceRestartCmd extends Command {
async restartAllServices(balena: BalenaSDK, deviceUuid: string) {
// Note: device.restartApplication throws `BalenaDeviceNotFound: Device not found` if device not online.
// Need to use device.get first to distinguish between non-existant and 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
const { instanceOf, ExpectedError } = await import('../../errors');
try {

View File

@ -15,7 +15,8 @@
* limitations under the License.
*/
import { Args, Command } from '@oclif/core';
import { Args } from '@oclif/core';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
@ -42,8 +43,11 @@ export default class DeviceRmCmd extends Command {
}),
};
public static usage = 'device rm <uuid(s)>';
public static flags = {
yes: cf.yes,
help: cf.help,
};
public static authenticated = true;

View File

@ -15,7 +15,8 @@
* limitations under the License.
*/
import { Args, Command } from '@oclif/core';
import { Args } from '@oclif/core';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { ExpectedError } from '../../errors';
@ -35,8 +36,11 @@ export default class DeviceShutdownCmd extends Command {
}),
};
public static usage = 'device shutdown <uuid>';
public static flags = {
force: cf.force,
help: cf.help,
};
public static authenticated = true;

View File

@ -15,7 +15,9 @@
* limitations under the License.
*/
import { Args, Command } from '@oclif/core';
import { Args } from '@oclif/core';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, getCliUx, stripIndent } from '../../utils/lazy';
import type { BalenaSDK } from 'balena-sdk';
@ -44,6 +46,12 @@ export default class DeviceStartServiceCmd extends Command {
}),
};
public static usage = 'device start-service <uuid>';
public static flags = {
help: cf.help,
};
public static authenticated = true;
public async run() {

View File

@ -15,7 +15,9 @@
* limitations under the License.
*/
import { Args, Command } from '@oclif/core';
import { Args } from '@oclif/core';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, getCliUx, stripIndent } from '../../utils/lazy';
import type { BalenaSDK } from 'balena-sdk';
@ -44,6 +46,12 @@ export default class DeviceStopServiceCmd extends Command {
}),
};
public static usage = 'device stop-service <uuid>';
public static flags = {
help: cf.help,
};
public static authenticated = true;
public async run() {

View File

@ -15,7 +15,9 @@
* limitations under the License.
*/
import { Args, Command } from '@oclif/core';
import { Args } from '@oclif/core';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
export default class DeviceTrackFleetCmd extends Command {
@ -33,6 +35,12 @@ export default class DeviceTrackFleetCmd extends Command {
}),
};
public static usage = 'device track-fleet <uuid>';
public static flags = {
help: cf.help,
};
public static authenticated = true;
public async run() {

View File

@ -15,7 +15,7 @@
* limitations under the License.
*/
import { Command } from '@oclif/core';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { expandForAppName } from '../../utils/helpers';
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
@ -35,10 +35,7 @@ const devicesSelectFields = {
],
} satisfies PineOptions<Device>;
export default class DeviceListCmd extends Command {
public static aliases = ['devices'];
public static deprecateAliases = true;
export default class DevicesCmd extends Command {
public static description = stripIndent`
List all devices.
@ -51,14 +48,17 @@ export default class DeviceListCmd extends Command {
${jsonInfo.split('\n').join('\n\t\t')}
`;
public static examples = [
'$ balena device list',
'$ balena device list --fleet MyFleet',
'$ balena device list -f myorg/myfleet',
'$ balena devices',
'$ balena devices --fleet MyFleet',
'$ balena devices -f myorg/myfleet',
];
public static usage = 'devices';
public static flags = {
fleet: cf.fleet,
json: cf.json,
help: cf.help,
};
public static primary = true;
@ -66,7 +66,7 @@ export default class DeviceListCmd extends Command {
public static authenticated = true;
public async run() {
const { flags: options } = await this.parse(DeviceListCmd);
const { flags: options } = await this.parse(DevicesCmd);
const balena = getBalenaSdk();
const devicesOptions = {

View File

@ -14,23 +14,20 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Flags, Command } from '@oclif/core';
import { Flags } from '@oclif/core';
import type * as BalenaSdk from 'balena-sdk';
import * as _ from 'lodash';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
import { CommandHelp } from '../../utils/oclif-utils';
export default class DeviceTypeListCmd extends Command {
public static aliases = ['devices supported'];
public static deprecateAliases = true;
export default class DevicesSupportedCmd extends Command {
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').
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.
List the supported device types (like 'raspberrypi3' or 'intel-nuc').
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
@ -39,24 +36,25 @@ export default class DeviceTypeListCmd extends Command {
(https://stedolan.github.io/jq/manual/).
`;
public static examples = [
'$ balena device-type list',
'$ balena device-type list --all',
'$ balena device-type list --json',
'$ balena devices supported',
'$ balena devices supported --json',
];
public static usage = (
'devices supported ' +
new CommandHelp({ args: DevicesSupportedCmd.args }).defaultUsage()
).trim();
public static flags = {
help: cf.help,
json: Flags.boolean({
char: 'j',
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() {
const { flags: options } = await this.parse(DeviceTypeListCmd);
const { flags: options } = await this.parse(DevicesSupportedCmd);
const pineOptions = {
$select: ['slug', 'name'],
$expand: {
@ -67,11 +65,9 @@ export default class DeviceTypeListCmd extends Command {
},
},
} satisfies BalenaSdk.PineOptions<BalenaSdk.DeviceType>;
const dts = (
options.all
? await getBalenaSdk().models.deviceType.getAll(pineOptions)
: await getBalenaSdk().models.deviceType.getAllSupported(pineOptions)
) as Array<
const dts = (await getBalenaSdk().models.deviceType.getAllSupported(
pineOptions,
)) as Array<
BalenaSdk.PineTypedResult<BalenaSdk.DeviceType, typeof pineOptions>
>;
interface DT {

View File

@ -15,8 +15,9 @@
* limitations under the License.
*/
import { Args, Command } from '@oclif/core';
import { Args } from '@oclif/core';
import type * as BalenaSdk from 'balena-sdk';
import Command from '../../command';
import { ExpectedError } from '../../errors';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
@ -25,6 +26,7 @@ import { applicationIdInfo } from '../../utils/messages';
interface FlagsDef {
fleet?: string;
device?: string; // device UUID
help: void;
quiet: boolean;
service?: string; // service name
}
@ -34,14 +36,11 @@ interface ArgsDef {
value?: string;
}
export default class EnvSetCmd extends Command {
public static aliases = ['env add'];
public static deprecateAliases = true;
export default class EnvAddCmd extends Command {
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
--fleet or the --device option must be provided, and either may be be
used alongside the --service option to define a service-specific variable.
@ -68,15 +67,15 @@ export default class EnvSetCmd extends Command {
`;
public static examples = [
'$ balena env set TERM --fleet MyFleet',
'$ balena env set EDITOR vim -f myorg/myfleet',
'$ balena env set EDITOR vim --fleet MyFleet,MyFleet2',
'$ balena env set EDITOR vim --fleet MyFleet --service MyService',
'$ balena env set EDITOR vim --fleet MyFleet,MyFleet2 --service MyService,MyService2',
'$ balena env set EDITOR vim --device 7cf02a6',
'$ balena env set EDITOR vim --device 7cf02a6,d6f1433',
'$ balena env set EDITOR vim --device 7cf02a6 --service MyService',
'$ balena env set EDITOR vim --device 7cf02a6,d6f1433 --service MyService,MyService2',
'$ balena env add TERM --fleet MyFleet',
'$ balena env add EDITOR vim -f myorg/myfleet',
'$ balena env add EDITOR vim --fleet MyFleet,MyFleet2',
'$ balena env add EDITOR vim --fleet MyFleet --service MyService',
'$ balena env add EDITOR vim --fleet MyFleet,MyFleet2 --service MyService,MyService2',
'$ balena env add EDITOR vim --device 7cf02a6',
'$ balena env add EDITOR vim --device 7cf02a6,d6f1433',
'$ balena env add EDITOR vim --device 7cf02a6 --service MyService',
'$ balena env add EDITOR vim --device 7cf02a6,d6f1433 --service MyService,MyService2',
];
public static args = {
@ -93,16 +92,18 @@ export default class EnvSetCmd extends Command {
// Required for supporting empty string ('') `value` args.
public static strict = false;
public static usage = 'env add <name> [value]';
public static flags = {
fleet: { ...cf.fleet, exclusive: ['device'] },
device: { ...cf.device, exclusive: ['fleet'] },
help: cf.help,
quiet: cf.quiet,
service: cf.service,
};
public async run() {
const { args: params, flags: options } = await this.parse(EnvSetCmd);
const { args: params, flags: options } = await this.parse(EnvAddCmd);
const cmd = this;
if (!options.fleet && !options.device) {
@ -111,9 +112,7 @@ export default class EnvSetCmd extends Command {
);
}
const { checkLoggedIn } = await import('../../utils/patterns');
await checkLoggedIn();
await Command.checkLoggedIn();
if (params.value == null) {
params.value = process.env[params.name];

View File

@ -14,7 +14,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Args, Command } from '@oclif/core';
import { Args } from '@oclif/core';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import * as ec from '../../utils/env-common';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { parseAsInteger } from '../../utils/validation';
@ -41,7 +44,7 @@ export default class EnvRenameCmd extends Command {
id: Args.integer({
required: true,
description: "variable's numeric database ID",
parse: (input) => parseAsInteger(input, 'id'),
parse: async (input) => parseAsInteger(input, 'id'),
}),
value: Args.string({
required: true,
@ -50,18 +53,19 @@ export default class EnvRenameCmd extends Command {
}),
};
public static usage = 'env rename <id> <value>';
public static flags = {
config: ec.booleanConfig,
device: ec.booleanDevice,
service: ec.booleanService,
help: cf.help,
};
public async run() {
const { args: params, flags: opt } = await this.parse(EnvRenameCmd);
const { checkLoggedIn } = await import('../../utils/patterns');
await checkLoggedIn();
await Command.checkLoggedIn();
await getBalenaSdk().pine.patch({
resource: ec.getVarResourceName(opt.config, opt.device, opt.service),

View File

@ -15,7 +15,9 @@
* limitations under the License.
*/
import { Flags, Args, Command } from '@oclif/core';
import { Flags, Args } from '@oclif/core';
import Command from '../../command';
import * as ec from '../../utils/env-common';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { parseAsInteger } from '../../utils/validation';
@ -46,10 +48,12 @@ export default class EnvRmCmd extends Command {
id: Args.integer({
required: true,
description: "variable's numeric database ID",
parse: (input) => parseAsInteger(input, 'id'),
parse: async (input) => parseAsInteger(input, 'id'),
}),
};
public static usage = 'env rm <id>';
public static flags = {
config: ec.booleanConfig,
device: ec.booleanDevice,
@ -65,9 +69,7 @@ export default class EnvRmCmd extends Command {
public async run() {
const { args: params, flags: opt } = await this.parse(EnvRmCmd);
const { checkLoggedIn } = await import('../../utils/patterns');
await checkLoggedIn();
await Command.checkLoggedIn();
const { confirm } = await import('../../utils/patterns');
await confirm(

View File

@ -14,16 +14,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Flags, Command } from '@oclif/core';
import { Flags } from '@oclif/core';
import type { Interfaces } from '@oclif/core';
import type * as SDK from 'balena-sdk';
import * as _ from 'lodash';
import Command from '../../command';
import { ExpectedError } from '../../errors';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
import { applicationIdInfo } from '../../utils/messages';
type FlagsDef = Interfaces.InferredFlags<typeof EnvListCmd.flags>;
type FlagsDef = Interfaces.InferredFlags<typeof EnvsCmd.flags>;
interface EnvironmentVariableInfo extends SDK.EnvironmentVariableBase {
fleet?: string | null; // fleet slug
@ -45,10 +46,7 @@ interface ServiceEnvironmentVariableInfo
serviceName?: string; // service name
}
export default class EnvListCmd extends Command {
public static aliases = ['envs'];
public static deprecateAliases = true;
export default class EnvsCmd extends Command {
public static description = stripIndent`
List the environment or config variables of a fleet, device or service.
@ -86,16 +84,18 @@ export default class EnvListCmd extends Command {
`;
public static examples = [
'$ balena env list --fleet myorg/myfleet',
'$ balena env list --fleet MyFleet --json',
'$ balena env list --fleet MyFleet --service MyService',
'$ balena env list --fleet MyFleet --config',
'$ balena env list --device 7cf02a6',
'$ balena env list --device 7cf02a6 --json',
'$ balena env list --device 7cf02a6 --config --json',
'$ balena env list --device 7cf02a6 --service MyService',
'$ balena envs --fleet myorg/myfleet',
'$ balena envs --fleet MyFleet --json',
'$ balena envs --fleet MyFleet --service MyService',
'$ balena envs --fleet MyFleet --config',
'$ balena envs --device 7cf02a6',
'$ balena envs --device 7cf02a6 --json',
'$ balena envs --device 7cf02a6 --config --json',
'$ balena envs --device 7cf02a6 --service MyService',
];
public static usage = 'envs';
public static flags = {
fleet: { ...cf.fleet, exclusive: ['device'] },
config: Flags.boolean({
@ -105,18 +105,17 @@ export default class EnvListCmd extends Command {
exclusive: ['service'],
}),
device: { ...cf.device, exclusive: ['fleet'] },
help: cf.help,
json: cf.json,
service: { ...cf.service, exclusive: ['config'] },
};
public async run() {
const { flags: options } = await this.parse(EnvListCmd);
const { flags: options } = await this.parse(EnvsCmd);
const variables: EnvironmentVariableInfo[] = [];
const { checkLoggedIn } = await import('../../utils/patterns');
await checkLoggedIn();
await Command.checkLoggedIn();
if (!options.fleet && !options.device) {
throw new ExpectedError('Missing --fleet or --device option');

View File

@ -15,7 +15,10 @@
* limitations under the License.
*/
import { Flags, Args, Command } from '@oclif/core';
import { Flags, Args } from '@oclif/core';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { stripIndent } from '../../utils/lazy';
export default class FleetCreateCmd extends Command {
@ -27,10 +30,10 @@ export default class FleetCreateCmd extends Command {
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 organization list\` command.
\`balena orgs\` command.
The fleet's default device type is specified with the \`--type\` option.
The \`balena device-type list\` command can be used to list the available
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
@ -53,6 +56,8 @@ export default class FleetCreateCmd extends Command {
}),
};
public static usage = 'fleet create <name>';
public static flags = {
organization: Flags.string({
char: 'o',
@ -61,8 +66,9 @@ export default class FleetCreateCmd extends Command {
type: Flags.string({
char: 't',
description:
'fleet device type (Check available types with `balena device-type list`)',
'fleet device type (Check available types with `balena devices supported`)',
}),
help: cf.help,
};
public static authenticated = true;

View File

@ -15,10 +15,12 @@
* limitations under the License.
*/
import { Flags, Command } from '@oclif/core';
import { Flags } from '@oclif/core';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
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';
export default class FleetCmd extends Command {
@ -39,12 +41,15 @@ export default class FleetCmd extends Command {
fleet: ca.fleetRequired,
};
public static usage = 'fleet <fleet>';
public static flags = {
help: cf.help,
view: Flags.boolean({
default: false,
description: 'open fleet dashboard page',
}),
json: cf.json,
...cf.dataOutputFlags,
};
public static authenticated = true;
@ -73,28 +78,16 @@ export default class FleetCmd extends Command {
return;
}
const applicationToDisplay = {
id: application.id,
app_name: application.app_name,
slug: application.slug,
const outputApplication = {
...application,
device_type: application.is_for__device_type[0].slug,
commit: application.should_be_running__release[0]?.commit,
};
if (options.json) {
console.log(JSON.stringify(applicationToDisplay, null, 4));
return;
}
// 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',
]),
await this.outputData(
outputApplication,
['app_name', 'id', 'device_type', 'slug', 'commit'],
options,
);
}
}

View File

@ -15,7 +15,9 @@
* limitations under the License.
*/
import { Args, Command } from '@oclif/core';
import { Args } from '@oclif/core';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { getExpandedProp } from '../../utils/pine';
@ -42,6 +44,12 @@ export default class FleetPinCmd extends Command {
}),
};
public static usage = 'fleet pin <slug> [releaseToPinTo]';
public static flags = {
help: cf.help,
};
public static authenticated = true;
public async run() {
@ -71,7 +79,7 @@ export default class FleetPinCmd extends Command {
pinnedRelease
? `This fleet is currently pinned to ${pinnedRelease}.`
: 'This fleet is not currently pinned to any release.'
} \n\nTo see a list of all releases this fleet can be pinned to, run \`balena release list ${slug}\`.`,
} \n\nTo see a list of all releases this fleet can be pinned to, run \`balena releases ${slug}\`.`,
);
} else {
await balena.models.application.pinToRelease(slug, releaseToPinTo);

View File

@ -15,7 +15,8 @@
* limitations under the License.
*/
import { Command } from '@oclif/core';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import * as ca from '../../utils/common-args';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { applicationIdInfo } from '../../utils/messages';
@ -39,6 +40,12 @@ export default class FleetPurgeCmd extends Command {
fleet: ca.fleetRequired,
};
public static usage = 'fleet purge <fleet>';
public static flags = {
help: cf.help,
};
public static authenticated = true;
public async run() {

View File

@ -15,7 +15,10 @@
* limitations under the License.
*/
import { Args, Command } from '@oclif/core';
import { Args } from '@oclif/core';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import * as ca from '../../utils/common-args';
import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy';
import { applicationIdInfo } from '../../utils/messages';
@ -45,6 +48,12 @@ export default class FleetRenameCmd extends Command {
}),
};
public static usage = 'fleet rename <fleet> [newName]';
public static flags = {
help: cf.help,
};
public static authenticated = true;
public async run() {

View File

@ -15,7 +15,8 @@
* limitations under the License.
*/
import { Command } from '@oclif/core';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import * as ca from '../../utils/common-args';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { applicationIdInfo } from '../../utils/messages';
@ -38,6 +39,12 @@ export default class FleetRestartCmd extends Command {
fleet: ca.fleetRequired,
};
public static usage = 'fleet restart <fleet>';
public static flags = {
help: cf.help,
};
public static authenticated = true;
public async run() {

View File

@ -15,11 +15,11 @@
* limitations under the License.
*/
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import * as ca from '../../utils/common-args';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { applicationIdInfo } from '../../utils/messages';
import { Command } from '@oclif/core';
export default class FleetRmCmd extends Command {
public static description = stripIndent`
@ -42,8 +42,11 @@ export default class FleetRmCmd extends Command {
fleet: ca.fleetRequired,
};
public static usage = 'fleet rm <fleet>';
public static flags = {
yes: cf.yes,
help: cf.help,
};
public static authenticated = true;

View File

@ -15,7 +15,9 @@
* limitations under the License.
*/
import { Args, Command } from '@oclif/core';
import { Args } from '@oclif/core';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
export default class FleetTrackLatestCmd extends Command {
@ -36,6 +38,12 @@ export default class FleetTrackLatestCmd extends Command {
}),
};
public static usage = 'fleet track-latest <slug>';
public static flags = {
help: cf.help,
};
public static authenticated = true;
public async run() {

View File

@ -16,9 +16,10 @@
*/
import type * as BalenaSdk from 'balena-sdk';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
import { Command } from '@oclif/core';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
interface ExtendedApplication extends ApplicationWithDeviceTypeSlug {
device_count: number;
@ -26,10 +27,7 @@ interface ExtendedApplication extends ApplicationWithDeviceTypeSlug {
device_type?: string;
}
export default class FleetListCmd extends Command {
public static aliases = ['fleets'];
public static deprecateAliases = true;
export default class FleetsCmd extends Command {
public static description = stripIndent`
List all fleets.
@ -39,17 +37,20 @@ export default class FleetListCmd extends Command {
\`balena fleet <fleet>\`
`;
public static examples = ['$ balena fleet list'];
public static examples = ['$ balena fleets'];
public static usage = 'fleets';
public static flags = {
json: cf.json,
...cf.dataSetOutputFlags,
help: cf.help,
};
public static authenticated = true;
public static primary = true;
public async run() {
const { flags: options } = await this.parse(FleetListCmd);
const { flags: options } = await this.parse(FleetsCmd);
const balena = getBalenaSdk();
@ -76,29 +77,17 @@ export default class FleetListCmd extends Command {
application.device_type = application.is_for__device_type[0].slug;
});
const applicationsToDisplay = applications.map((application) => ({
id: application.id,
app_name: application.app_name,
slug: application.slug,
device_type: application.device_type,
online_devices: application.online_devices,
device_count: application.device_count,
}));
if (options.json) {
console.log(JSON.stringify(applicationsToDisplay, null, 4));
return;
}
console.log(
getVisuals().table.horizontal(applicationsToDisplay, [
await this.outputData(
applications,
[
'id',
'app_name => NAME',
'app_name',
'slug',
'device_type',
'device_count',
'online_devices',
]),
],
options,
);
}
}

View File

@ -15,8 +15,10 @@
* limitations under the License.
*/
import { Args, Command } from '@oclif/core';
import { Args } from '@oclif/core';
import Command from '../../command';
import { stripIndent } from '../../utils/lazy';
import { CommandHelp } from '../../utils/oclif-utils';
// 'Internal' commands are called during the execution of other commands.
// `osinit` is called during `os initialize`
@ -46,6 +48,11 @@ export default class OsinitCmd extends Command {
}),
};
public static usage = (
'internal osinit ' +
new CommandHelp({ args: OsinitCmd.args }).defaultUsage()
).trim();
public static hidden = true;
public static root = true;
public static offlineCompatible = true;

View File

@ -15,7 +15,8 @@
* limitations under the License.
*/
import { Args, Flags, Command } from '@oclif/core';
import { Args, Flags } from '@oclif/core';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { applicationIdInfo } from '../../utils/messages';
@ -59,12 +60,16 @@ export default class JoinCmd extends Command {
}),
};
// Hardcoded to preserve camelcase
public static usage = 'join [deviceIpOrHostname]';
public static flags = {
fleet: cf.fleet,
pollInterval: Flags.integer({
description: 'the interval in minutes to check for updates',
char: 'i',
}),
help: cf.help,
};
public static authenticated = true;
@ -75,8 +80,7 @@ export default class JoinCmd extends Command {
const promote = await import('../../utils/promote');
const sdk = getBalenaSdk();
const Logger = await import('../../utils/logger');
const logger = Logger.getLogger();
const logger = await Command.getLogger();
return promote.join(
logger,
sdk,

View File

@ -15,13 +15,13 @@
* limitations under the License.
*/
import { Args, Command } from '@oclif/core';
import { Args } from '@oclif/core';
import Command from '../../command';
import { ExpectedError } from '../../errors';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
export default class SSHKeyAddCmd extends Command {
public static aliases = ['key add'];
public static deprecateAliases = true;
export default class KeyAddCmd extends Command {
public static description = stripIndent`
Add an SSH key to balenaCloud.
@ -29,7 +29,7 @@ export default class SSHKeyAddCmd extends Command {
If \`path\` is omitted, the command will attempt to read the SSH key from stdin.
About SSH keys
About SSH keys
An "SSH key" actually consists of a public/private key pair. A typical name
for the private key file is "id_rsa", and a typical name for the public key
file is "id_rsa.pub". Both key files are saved to your computer (with the
@ -37,7 +37,7 @@ export default class SSHKeyAddCmd extends Command {
saved to your balena account. This means that if you change computers or
otherwise lose the private key, you cannot recover the private key through
your balena account. You can however add new keys, and delete the old ones.
To generate a new SSH key pair, a nice guide can be found in GitHub's docs:
https://help.github.com/en/articles/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent
Skip the step about adding the key to a GitHub account, and instead add it to
@ -45,10 +45,10 @@ export default class SSHKeyAddCmd extends Command {
`;
public static examples = [
'$ balena ssh-key add Main ~/.ssh/id_rsa.pub',
'$ cat ~/.ssh/id_rsa.pub | balena ssh-key add Main',
'$ balena key add Main ~/.ssh/id_rsa.pub',
'$ cat ~/.ssh/id_rsa.pub | balena key add Main',
'# 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 = {
@ -58,21 +58,30 @@ export default class SSHKeyAddCmd extends Command {
}),
path: Args.string({
description: `the path to the public key file`,
required: true,
}),
};
public static usage = 'key add <name> [path]';
public static flags = {
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params } = await this.parse(SSHKeyAddCmd);
public static readStdin = true;
public async run() {
const { args: params } = await this.parse(KeyAddCmd);
const { readFile } = (await import('fs')).promises;
let key: string;
try {
if (params.path != null) {
const { readFile } = (await import('fs')).promises;
key = await readFile(params.path, 'utf8');
} catch {
key = params.path;
} else if (this.stdin.length > 0) {
key = this.stdin;
} else {
throw new ExpectedError('No public key file or path provided.');
}
await getBalenaSdk().models.key.create(params.name, key);

View File

@ -15,34 +15,39 @@
* limitations under the License.
*/
import { Args, Command } from '@oclif/core';
import { Args } from '@oclif/core';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
import { parseAsInteger } from '../../utils/validation';
export default class SSHKeyCmd extends Command {
public static aliases = ['key'];
public static deprecateAliases = true;
export default class KeyCmd extends Command {
public static description = stripIndent`
Display an SSH key.
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 = {
id: Args.integer({
description: 'balenaCloud ID for the SSH key',
parse: (x) => parseAsInteger(x, 'id'),
parse: async (x) => parseAsInteger(x, 'id'),
required: true,
}),
};
public static usage = 'key <id>';
public static flags = {
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params } = await this.parse(SSHKeyCmd);
const { args: params } = await this.parse(KeyCmd);
const key = await getBalenaSdk().models.key.get(params.id);

View File

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

View File

@ -15,24 +15,28 @@
* limitations under the License.
*/
import { Command } from '@oclif/core';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
export default class SSHKeyListCmd extends Command {
public static aliases = ['keys', 'key list'];
public static deprecateAliases = true;
export default class KeysCmd extends Command {
public static description = stripIndent`
List the SSH keys in balenaCloud.
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 = {
help: cf.help,
};
public static authenticated = true;
public async run() {
await this.parse(SSHKeyListCmd);
await this.parse(KeysCmd);
const keys = await getBalenaSdk().models.key.getAll();

View File

@ -15,7 +15,9 @@
* limitations under the License.
*/
import { Args, Command } from '@oclif/core';
import { Args } from '@oclif/core';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { stripIndent } from '../../utils/lazy';
import { parseAsLocalHostnameOrIp } from '../../utils/validation';
@ -48,6 +50,12 @@ export default class LeaveCmd extends Command {
}),
};
public static usage = 'leave [deviceIpOrHostname]';
public static flags = {
help: cf.help,
};
public static authenticated = true;
public static primary = true;
@ -55,8 +63,7 @@ export default class LeaveCmd extends Command {
const { args: params } = await this.parse(LeaveCmd);
const promote = await import('../../utils/promote');
const Logger = await import('../../utils/logger');
const logger = Logger.getLogger();
const logger = await Command.getLogger();
return promote.leave(logger, params.deviceIpOrHostname);
}
}

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