Compare commits

...

99 Commits

Author SHA1 Message Date
b602e9c294 Add balena update command for updating the CLI
Change-type: minor
2023-07-06 13:27:16 -04:00
f621daec82 Update oclif, improve help command
Change-type: minor
2023-07-06 13:26:29 -04:00
338477463a v16.6.3 2023-06-30 17:07:34 +00:00
d1275760fa Merge pull request #2641 from balena-io/drop-toolbelt
Pin dockerode and drop docker-toolbelt
2023-06-30 20:06:46 +03:00
0f4054fa4d Remove redundant dependency on docker-toolbelt
Change-type: patch
2023-06-30 19:22:07 +03:00
7545fc5d6e Pin dockerode to v3.3.3
v3.3.4 introduces a regression that is fixed by https://github.com/apocas/dockerode/pull/695 but Dockerode has not published a version that includes the fix yet. Pin the dependency to ensure we don’t ever update to a broken version.

Change-type: patch
2023-06-30 19:19:52 +03:00
a1f25809cb v16.6.2 2023-06-29 12:27:14 +00:00
e0a3c4bd95 Merge pull request #2640 from balena-io/make-apple-notarization-team-id-public
macos notarization: Expose team ID instead of keeping it in secrets
2023-06-29 12:26:27 +00:00
d843e75512 macos notarization: Expose team ID instead of keeping it in secrets
Change-type: patch
2023-06-29 07:41:07 -04:00
72c57608d5 v16.6.1 2023-06-28 12:47:56 +00:00
d9de7636db Merge pull request #2639 from balena-io/update-electron-notarize
Drop `electron-notarize` dependency in favor of `@electron/notarize`
2023-06-28 12:47:13 +00:00
10b5af6967 Drop electron-notarize dependency in favor of @electron/notarize
Change-type: patch
2023-06-28 08:01:39 -04:00
51c050c725 v16.6.0 2023-06-26 13:05:11 +00:00
eb52c47de5 Merge pull request #2631 from balena-io/add-api-key-revoke-command
api-key: Add `revoke` command which accepts a list of API key ids
2023-06-26 09:04:21 -04:00
4b1378dfbc api-key: Add revoke command which accepts a list of API key ids
Change-type: minor
2023-06-26 08:21:23 -04:00
1a77d86347 v16.5.2 2023-06-09 16:03:10 +00:00
bd5188f4b9 Merge pull request #2637 from balena-io/klutchell-patch-2
Re-enable automatic github final releases
2023-06-09 16:02:22 +00:00
034f459bfa Update npm-shrinkwrap
Signed-off-by: Kyle Harding <kyle@balena.io>
2023-06-09 11:23:49 -04:00
bc405d997e Re-enable automatic github final releases
Change-type: patch
2023-06-09 11:05:01 -04:00
af27cf2cbe v16.5.1 2023-06-02 06:30:00 +00:00
83b9bf67c2 Merge pull request #2636 from balena-io/bump-ts
Update TypeScript to 5.1.3
2023-06-02 06:29:11 +00:00
abd73b805b Update TypeScript to 5.1.3
Change-type: patch
2023-06-01 21:58:34 +03:00
37bfd4db98 v16.5.0 2023-05-25 16:45:02 +00:00
be74143d5f Merge pull request #2633 from balena-io/add-block-create-command
Add `balena block create` command for creating Blocks
2023-05-25 16:44:19 +00:00
9975e5d9ac Add balena block create command for creating Blocks
Change-type: minor
2023-05-25 12:00:39 -04:00
1341413966 v16.4.0 2023-05-25 15:53:23 +00:00
1a5b914a6f Merge pull request #2632 from balena-io/add-app-create-command
Add `balena app create` command for creatings Apps
2023-05-25 15:52:37 +00:00
c5e8f0d6ea Add balena app create command for creatings Apps
Change-type: minor
2023-05-25 11:09:02 -04:00
3a143fe413 v16.3.0 2023-05-25 15:06:39 +00:00
3445e4a08e Merge pull request #2630 from balena-io/add-command-to-list-user-api-keys
Add `balena api-keys` command for listing user API keys
2023-05-25 15:05:38 +00:00
166130c3df Add balena api-keys command for listing user/fleet API keys
Change-type: minor
2023-05-25 09:11:36 -04:00
c3a8a905f7 v16.2.7 2023-05-24 12:04:59 +00:00
2b878e87d8 Merge pull request #2629 from balena-io/less-requests-2
Fetch the application and its devices in one request in balena devices, device init, ssh, tunnel
2023-05-24 12:04:05 +00:00
063e9d40f0 device init: Avoid extra request when not providing the --fleet option
Change-type: patch
2023-05-24 14:22:04 +03:00
2b58143164 devices: Use a single request when providing the --fleet parameter
Reduces the response time when using
--fleet from 1.5s to 1s.

Change-type: patch
2023-05-24 14:22:04 +03:00
861d4f33b7 ssh,tunnel: Fetch the fleet & devices in one request
Change-type: patch
2023-05-24 14:22:04 +03:00
81f4aae7d2 v16.2.6 2023-05-24 11:17:30 +00:00
46ab335407 Merge pull request #2626 from balena-io/less-requests
Stop fetching unnecessary fields
2023-05-24 14:16:42 +03:00
07cb0cbfcd device init: Stop fetching unnecessary app fields
Change-type: patch
2023-05-24 13:37:19 +03:00
a2392dc580 device move: Stop fetching unnecessary device & app fields
Reduces the amount of device data retrieved
by 66%.

Change-type: patch
2023-05-24 13:37:19 +03:00
8b3235ab2b device register: Stop fetching unnecessary app fields
Change-type: patch
2023-05-24 13:37:19 +03:00
15dac6f194 config generate: Stop fetching unnecessary app fields
Change-type: patch
2023-05-24 13:37:19 +03:00
3c93db8449 devices: Stop fetching unnecessary device fields
Change-type: patch
2023-05-24 13:37:19 +03:00
9d8df0b781 fleet rm,restart,rename,purge: Stop fetching unnecessary app fields
Halves the amout of application data retrieved.

Change-type: patch
2023-05-24 13:37:19 +03:00
bcadbdbed8 push: Stop unnecessarily fetching the application name
Change-type: patch
2023-05-24 13:37:19 +03:00
05a96fa60e ssh,tunnel: Reduce the amount of application fields fetched
Halves the amout of application data retrieved.

Change-type: patch
2023-05-24 13:37:19 +03:00
2e37536e7a orgs: Stop fetching unnecessary org fields
Halves the amount of org data retrieved to
show the list of orgs to select from.

Change-type: patch
2023-05-24 13:37:19 +03:00
025c4ef7f2 fleet create: Reduce the amount of org fields fetched
Halves the amount of org data retrieved to
show the list of orgs to select from.

Change-type: patch
2023-05-24 13:37:19 +03:00
ecbc660bf5 Fetch only the app slug when resolving an app by name
Affects many env & tags commands, as well
as support, releases & preload.

Change-type: patch
2023-05-24 13:37:19 +03:00
ba1f17d537 v16.2.5 2023-05-24 10:30:21 +00:00
3ab8f7500e Merge pull request #2628 from balena-io/73-fix-device-init-os-initialize
Fix device int & os initialize failing to initialize the drive list
2023-05-24 13:29:36 +03:00
0a25bec010 Fix device int & os initialize failing to initialize the drive list
Resolves: #2627
Change-type: patch
2023-05-24 12:47:06 +03:00
01e765e670 v16.2.4 2023-05-23 20:31:27 +00:00
61844f2386 Merge pull request #2625 from balena-io/app-select-reduce-requests
Remove extra request when filling the application selection list
2023-05-23 20:30:32 +00:00
46aa08c953 Remove extra request when filling the application selection list
Saves one request of about 450ms on the
init and move commands.

Change-type: patch
2023-05-23 22:47:10 +03:00
b6c7fb82c3 v16.2.3 2023-05-23 19:46:47 +00:00
fcda09009a Merge pull request #2624 from balena-io/improve-typings
Use stricter typings
2023-05-23 19:46:02 +00:00
1a6fe1f3de Use stricter typings
Change-type: patch
2023-05-23 18:57:54 +03:00
98e91c0607 v16.2.2 2023-05-23 14:02:36 +00:00
bed2387d83 Merge pull request #2623 from balena-io/fix-setting-service-env-vars-by-app-name
env add: Fix accepting fleet names when setting service vars
2023-05-23 14:01:21 +00:00
50e852acee env add: Fix accepting fleet names when setting service vars
Change-type: patch
2023-05-23 15:59:41 +03:00
da30623e4e v16.2.1 2023-05-23 12:28:25 +00:00
7a46b367a7 Merge pull request #2621 from balena-io/sdk-v17
Update balena-sdk to 17.0.0
2023-05-23 12:27:29 +00:00
d9651c7393 Update balena-settings-client to 5.0.2
Change-type: patch
2023-05-23 13:22:38 +03:00
e371b1e759 Update balena-preload & balena-image-manager
Update balena-image-manager from 8.0.1 to 9.0.0
Update balena-preload from 13.0.0 to 14.0.0

Change-type: patch
2023-05-23 13:22:38 +03:00
77cf4af166 Update balena-sdk to 17.0.0
Update balena-sdk from 16.45.1 to 17.0.0

Change-type: patch
2023-05-23 13:22:38 +03:00
9d197317ca v16.2.0 2023-05-19 18:11:57 +00:00
9a8b0b4a0d Merge pull request #2619 from balena-io/alexgg/sb
os configure, config generate: Add '--secureBoot' option to opt-in secure boot
2023-05-19 18:11:02 +00:00
0c62b9ef08 Deduplicate npm-shrinkwra.json 2023-05-19 20:28:12 +03:00
83a5e7392a secureboot: Retrieve the OS release & contract in one request
Change-type: patch
2023-05-19 19:22:23 +03:00
f0c8c37022 os configure, config generate: Add '--secureBoot' option to opt-in secure boot
Allow to generate a config file with `installer.secureboot` set so that
a secure boot and disk encrypted system can be installed.

Change-type: minor
Signed-off-by: Alex Gonzalez <alexg@balena.io>
2023-05-19 18:10:00 +02:00
ba26d3204d package.json: Update balena-sdk to 16.44.2
Update balena-sdk from 16.40.0 to 16.44.2

Change-type: patch
Signed-off-by: Alex Gonzalez <alexg@balena.io>
2023-05-19 18:10:00 +02:00
d53542975e flowzone: update custom runs to use macos-12
After the flowzone update to use zstd as compression algorithm for sources
there is an error on macos-11 as tar does not support it.

Change-type: patch
Signed-off-by: Alex Gonzalez <alexg@balena.io>
2023-05-19 11:33:31 +02:00
632296a271 v16.1.0 2023-05-16 18:26:10 +00:00
3e089fcdb2 Merge pull request #2616 from balena-io/ab77/operational
build linux/arm packages
2023-05-16 18:25:27 +00:00
d61c300750 build linux/arm packages
change-type: minor
2023-05-16 10:39:00 -07:00
a0a97c5f40 v16.0.0 2023-05-16 00:02:37 +00:00
165f3b83ca Merge pull request #2618 from balena-io/node-16
Update to Node 16
2023-05-16 00:01:48 +00:00
5bf95300ee support: Change the printed support expiry date in ISO 8601 UTC format
Change-type: major
2023-05-12 19:00:10 +03:00
adb460b270 logs: Change the timestamp format to ISO 8601 UTC
Resolves: #2608
Change-type: major
2023-05-12 19:00:10 +03:00
ca80bd52fe Pin flowzone to v4.7.1
The macos-11 runners apparently do not support zst compression format as
added in flowzone 4.7.2. While support is rolled out, we can keep
the flowzone branch to the previous working version.

Change-type: patch
2023-05-11 19:10:10 -04:00
281f8abb9a Update etcher-sdk to v8.5.3
This removes the dependency on our custom fork of [node-usb](https://github.com/balena-io-modules/node-usb)
and uses the maintained building method of the official node-usb repo

Change-type: patch
2023-05-11 18:10:52 -04:00
2cf2918d73 Update vercel/pkg to v5.8.1
This seems to be needed to build the binaries for node v16 since earlier
versions failed with

```
Error: Could not detect abi for version 16.13.0 and runtime node.  Updating "node-abi" might help solve this issue if it is a new release of node
```

Change-type: patch
2023-05-11 17:49:49 -04:00
7dfb7474f5 Update to Node 16
This also drops support for Node 14

Change-type: major
2023-05-11 17:49:37 -04:00
6ee0b48c9a v15.2.3 2023-05-03 20:03:49 +00:00
bd01fbf90c Merge pull request #2614 from balena-io/local-release-uuid
Use valid release uuid for local releases
2023-05-03 20:02:59 +00:00
cd19845b6b Use valid release uuid for local releases
On local push, the CLI uses `localrelease` as the `commit` property for
the development application. This is not a valid uuid and will not be
read properly by the supervisor, as seen in

https://github.com/balena-os/balena-supervisor/blob/master/src/compose/service.ts#L652

While this is not a problem right now, the commit is becoming the main
way to identify a service release (replacing `releaseId` and `imageId`),
and the invalid release uuid could cause update issues when pushing a
local release on when using some API endpoints.

Change-type: patch
Relates-to: balena-os/balena-supervisor#2136
2023-05-03 15:08:19 -04:00
5545883c3f v15.2.2 2023-04-28 16:16:44 +00:00
75a380b0ba Merge pull request #2615 from balena-io/remove-nvmrc
Remove nvmrc
2023-04-28 16:15:57 +00:00
35fe7c6a58 Remove nvmrc
There is not a lot of benefit to using `.nvmrc` as it still requires
`nvm use`, and not everybody uses `nvm`. The call to `npm install` will
already warn about using the wrong version.

Change-type: patch
2023-04-28 10:27:15 -04:00
69249b3139 v15.2.1 2023-04-28 09:25:50 +00:00
bf897fd56d Merge pull request #2612 from balena-io/sync-tslib
Fix tslib going out of sync causing HUP to fail
2023-04-28 09:25:01 +00:00
150c6e75f5 Fix tslib going out of sync causing HUP to fail
Change-type: patch
2023-04-27 14:07:57 +03:00
e8bc43dc64 v15.2.0 2023-04-05 13:09:24 +00:00
1213689de2 Merge pull request #2606 from balena-io/update-balena-sdk-16.40.0
Add support for device restarts in open-balena
2023-04-05 13:08:14 +00:00
c1017e8e27 Add support for device restarts in open-balena
Update balena-sdk from 16.28.2 to 16.40.0

Change-type: minor
2023-04-05 12:57:33 +03:00
7ad9e685f6 v15.1.3 2023-04-05 08:06:56 +00:00
c778aaffaf Merge pull request #2607 from balena-io/update-balena-sdk-16.28.2
devices supported: Fix showing types without a valid & finalized release
2023-04-05 08:06:05 +00:00
b98047cacf devices supported: Fix showing types without a valid & finalized release
Update balena-sdk from 16.28.0 to 16.28.2

Resolves: #2524
Change-type: patch
2023-04-05 10:19:39 +03:00
71 changed files with 31986 additions and 7661 deletions

View File

@ -3,31 +3,34 @@ name: package and draft GitHub release
# https://github.com/product-os/flowzone/tree/master/.github/actions
inputs:
json:
description: "JSON stringified object containing all the inputs from the calling workflow"
description: 'JSON stringified object containing all the inputs from the calling workflow'
required: true
secrets:
description: "JSON stringified object containing all the secrets from the calling workflow"
description: 'JSON stringified object containing all the secrets from the calling workflow'
required: true
variables:
description: 'JSON stringified object containing all the variables from the calling workflow'
required: true
# --- custom environment
XCODE_APP_LOADER_EMAIL:
type: string
default: "accounts+apple@balena.io"
default: 'accounts+apple@balena.io'
NODE_VERSION:
type: string
default: "14.x"
default: '16.x'
VERBOSE:
type: string
default: "true"
default: 'true'
runs:
# https://docs.github.com/en/actions/creating-actions/creating-a-composite-action
using: "composite"
using: 'composite'
steps:
- name: Download custom source artifact
uses: actions/download-artifact@v3
with:
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}-${{ runner.arch }}
path: ${{ runner.temp }}
- name: Extract custom source artifact
@ -121,6 +124,7 @@ runs:
# Apple notarization (automation/build-bin.ts)
XCODE_APP_LOADER_EMAIL: ${{ inputs.XCODE_APP_LOADER_EMAIL }}
XCODE_APP_LOADER_PASSWORD: ${{ fromJSON(inputs.secrets).XCODE_APP_LOADER_PASSWORD }}
XCODE_APP_LOADER_TEAM_ID: ${{ inputs.XCODE_APP_LOADER_TEAM_ID }}
- name: Upload artifacts
uses: actions/upload-artifact@v3

View File

@ -8,11 +8,14 @@ inputs:
secrets:
description: "JSON stringified object containing all the secrets from the calling workflow"
required: true
variables:
description: "JSON stringified object containing all the variables from the calling workflow"
required: true
# --- custom environment
NODE_VERSION:
type: string
default: "14.x"
default: '16.x'
VERBOSE:
type: string
default: "true"
@ -51,6 +54,6 @@ runs:
- name: Upload custom artifact
uses: actions/upload-artifact@v3
with:
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}-${{ runner.arch }}
path: ${{ runner.temp }}/custom.tgz
retention-days: 1

View File

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

1
.gitignore vendored
View File

@ -10,6 +10,7 @@
*.seed
/.idea/
/.lock-wscript
/.nvmrc
/.nyc_output/
/.vscode/
/coverage/

1
.nvmrc
View File

@ -1 +0,0 @@
12

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,476 @@ 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/).
## 16.6.3 - 2023-06-30
* Remove redundant dependency on docker-toolbelt [Akis Kesoglou]
* Pin dockerode to v3.3.3 [Akis Kesoglou]
## 16.6.2 - 2023-06-29
* macos notarization: Expose team ID instead of keeping it in secrets [myarmolinsky]
## 16.6.1 - 2023-06-28
* Drop `electron-notarize` dependency in favor of `@electron/notarize` [myarmolinsky]
## 16.6.0 - 2023-06-26
* api-key: Add `revoke` command which accepts a list of API key ids [myarmolinsky]
## 16.5.2 - 2023-06-09
* Re-enable automatic github final releases [Kyle Harding]
## 16.5.1 - 2023-06-02
* Update TypeScript to 5.1.3 [Thodoris Greasidis]
## 16.5.0 - 2023-05-25
* Add `balena block create` command for creating Blocks [myarmolinsky]
## 16.4.0 - 2023-05-25
* Add `balena app create` command for creatings Apps [myarmolinsky]
## 16.3.0 - 2023-05-25
* Add `balena api-keys` command for listing user/fleet API keys [myarmolinsky]
## 16.2.7 - 2023-05-24
* device init: Avoid extra request when not providing the --fleet option [Thodoris Greasidis]
* devices: Use a single request when providing the --fleet parameter [Thodoris Greasidis]
* ssh,tunnel: Fetch the fleet & devices in one request [Thodoris Greasidis]
## 16.2.6 - 2023-05-24
* device init: Stop fetching unnecessary app fields [Thodoris Greasidis]
* device move: Stop fetching unnecessary device & app fields [Thodoris Greasidis]
* device register: Stop fetching unnecessary app fields [Thodoris Greasidis]
* config generate: Stop fetching unnecessary app fields [Thodoris Greasidis]
* devices: Stop fetching unnecessary device fields [Thodoris Greasidis]
* fleet rm,restart,rename,purge: Stop fetching unnecessary app fields [Thodoris Greasidis]
* push: Stop unnecessarily fetching the application name [Thodoris Greasidis]
* ssh,tunnel: Reduce the amount of application fields fetched [Thodoris Greasidis]
* orgs: Stop fetching unnecessary org fields [Thodoris Greasidis]
* fleet create: Reduce the amount of org fields fetched [Thodoris Greasidis]
* Fetch only the app slug when resolving an app by name [Thodoris Greasidis]
## 16.2.5 - 2023-05-24
* Fix device int & os initialize failing to initialize the drive list [Thodoris Greasidis]
## 16.2.4 - 2023-05-23
* Remove extra request when filling the application selection list [Thodoris Greasidis]
## 16.2.3 - 2023-05-23
* Use stricter typings [Thodoris Greasidis]
## 16.2.2 - 2023-05-23
* env add: Fix accepting fleet names when setting service vars [Thodoris Greasidis]
## 16.2.1 - 2023-05-23
* Update balena-settings-client to 5.0.2 [Thodoris Greasidis]
<details>
<summary> Update balena-preload & balena-image-manager [Thodoris Greasidis] </summary>
> ### balena-image-manager-9.0.0 - 2023-05-23
>
> * Drop .hound.yml [Thodoris Greasidis]
> * Update dependencies [Thodoris Greasidis]
> * Update README.md [Thodoris Greasidis]
> * Update to balena-sdk 17.0.0 [Thodoris Greasidis]
> * Convert to typescript an emit type declarations [Thodoris Greasidis]
> * Require es2019 capable runtime [Thodoris Greasidis]
> * Drop support for Node.js v12 & v14 [Thodoris Greasidis]
>
> ### balena-image-manager-8.0.3 - 2022-09-26
>
> * Delete redundant .resinci.yml [Thodoris Greasidis]
>
> ### balena-image-manager-8.0.2 - 2022-09-22
>
> * Drop the .travis.yml [Thodoris Greasidis]
> * Replace balenaCI with flowzone [Thodoris Greasidis]
>
> ### balena-preload-14.0.0 - 2023-05-23
>
> * Improve typings & source type safety [Thodoris Greasidis]
> * Update dev dependencies [Thodoris Greasidis]
> * Update to balena-sdk 17.0.0 [Thodoris Greasidis]
> * Emit type declaration files [Thodoris Greasidis]
> * Switch to es6 module exports [Thodoris Greasidis]
> * Require es2019 capable runtime [Thodoris Greasidis]
> * Drop support for Node.js v12 & v14 [Thodoris Greasidis]
>
</details>
<details>
<summary> Update balena-sdk to 17.0.0 [Thodoris Greasidis] </summary>
> ### balena-sdk-17.0.0 - 2023-05-22
>
> * Fully type the result of os.getAllOsVersions when providing pine options [Thodoris Greasidis]
> * Change all .create() methods to return fully typed results [Thodoris Greasidis]
> * Change the pine.post() method to return fully typed results [Thodoris Greasidis]
> * Add the imageType property to the os.download method [Thodoris Greasidis]
> * Add OS download custom properties [Otávio Jacobi]
> * **BREAKING**: Drop the tags.getAll() method from all models [Thodoris Greasidis]
> * **BREAKING**: Drop organization.membership.getAll() [Thodoris Greasidis]
> * **BREAKING**: Drop application.membership.getAll() [Thodoris Greasidis]
> * **BREAKING**: Drop device.getAll() in favor of getAllByOrganization() [Thodoris Greasidis]
> * Replace lodash flatten with native flatMap [Thodoris Greasidis]
> * Move the sources under the /src folder [Thodoris Greasidis]
> * types: Add type checks for the direction of the string $orderby variant [Thodoris Greasidis]
> * types: Require a generic param in pine's associated resource helpers [Thodoris Greasidis]
> * tests: Convert some async expectations to asyn-await [Thodoris Greasidis]
> * types: Rename ODataOptionsWithSelect to ODataOptionsStrict [Thodoris Greasidis]
> * types: Rename PineOptionsWithSelect to PineOptionsStrict [Thodoris Greasidis]
> * Types: Rename ParamsObjWithSelect to ParamsObjStrict [Thodoris Greasidis]
> * release: Rename the `.note()` method to `.setNote()` [Thodoris Greasidis]
> * device: Rename the `.note()` method to `.setNote()` [Thodoris Greasidis]
> * os.download: Change to accept a single object parameter [Thodoris Greasidis]
> * OsVersion: Make `variant` non-nullable to match the Release field [Thodoris Greasidis]
> * OsVersion: Drop the formattedVersion property [Thodoris Greasidis]
> * OsVersion: Drop the rawVersion property in favor of raw_version [Thodoris Greasidis]
> * Drop device.getManifestByApplication() [Thodoris Greasidis]
> * Drop device.getManifestBySlug in favor of config.getDeviceTypeManifestBySlug [Thodoris Greasidis]
> * Drop support for callbacks [Thodoris Greasidis]
> * getWithServiceDetails: Drop the current_gateway_downloads property [Thodoris Greasidis]
> * types: Drop the deprecated public_key property from the JWTUser [Thodoris Greasidis]
> * Drop the deprecated needsPasswordReset property from the JWTUser [Thodoris Greasidis]
> * Bump mockttp to v3.0.0 [Thodoris Greasidis]
>
> <details>
> <summary> Update balena-hup-action-utils to 5.0.0 [Thodoris Greasidis] </summary>
>
>> #### balena-hup-action-utils-5.0.0 - 2023-04-28
>>
>> * Re-enable TS-compatibility checks [Thodoris Greasidis]
>> * Update Typescript to 5.0.4 [Thodoris Greasidis]
>> * Throw a typed HUPActionError for expected errors [Thodoris Greasidis]
>> * Change the build target to es6 [Thodoris Greasidis]
>> * Drop support for node < v12 [Thodoris Greasidis]
>>
>> #### balena-hup-action-utils-4.1.3 - 2022-09-26
>>
>> * Delete redundant .resinci.yml [Thodoris Greasidis]
>>
>> #### balena-hup-action-utils-4.1.2 - 2022-09-22
>>
>> * Specify node 10 as the minimum supported node engine in the package.json [Thodoris Greasidis]
>> * Replace balenaCI with flowzone [Thodoris Greasidis]
>>
>> #### balena-hup-action-utils-4.1.1 - 2022-04-09
>>
>> * Bump karma to v6 [Thodoris Greasidis]
>>
>
> </details>
>
> * Update balena-settings-client to v5.0.0 [Thodoris Greasidis]
> * Bump minimum supported Typescript version to v5.0.2 [Thodoris Greasidis]
> * **BREAKING**: Drop device.getAllByParentDevice [Thodoris Greasidis]
> * **BREAKING**: Drop support for node 12, require es2019 builtin APIs [Thodoris Greasidis]
> * **BREAKING**: Stop publishing an unminified browser bundle [Thodoris Greasidis]
>
</details>
## 16.2.0 - 2023-05-19
* secureboot: Retrieve the OS release & contract in one request [Thodoris Greasidis]
* os configure, config generate: Add '--secureBoot' option to opt-in secure boot [Alex Gonzalez]
<details>
<summary> package.json: Update balena-sdk to 16.44.2 [Alex Gonzalez] </summary>
> ### balena-sdk-16.44.2 - 2023-05-16
>
> * Update flowzone's macos runner to v12 [Thodoris Greasidis]
> * Add device type yocto properties to typings [Otávio Jacobi]
> * Optimize getDeviceUrl request in one query [Otávio Jacobi]
>
> ### balena-sdk-16.44.1 - 2023-05-09
>
> * Fix device.getAllByOrganization parameter docs [Otávio Jacobi]
>
> ### balena-sdk-16.44.0 - 2023-04-27
>
> * Add device.getAllByOrganization() [Thodoris Greasidis]
> * Deprecate Device's is_managed_by__device & manages__device properties [Thodoris Greasidis]
>
> ### balena-sdk-16.43.0 - 2023-04-19
>
> * Add test case DeviceHistory expandable resources [fisehara]
> * Make DeviceHistory referenced resources expandable [fisehara]
>
> ### balena-sdk-16.42.0 - 2023-04-18
>
> * Add support for pine queries on Concept Type properties [Thodoris Greasidis]
> * Properly type Actor properties on resources [fisehara]
>
> ### balena-sdk-16.41.0 - 2023-04-07
>
> * Release model: Add support for getting/patching releases by `application` & `rawVersion` pairs [myarmolinsky]
>
</details>
* flowzone: update custom runs to use macos-12 [Alex Gonzalez]
## 16.1.0 - 2023-05-16
* build linux/arm packages [balenaCI]
## 16.0.0 - 2023-05-16
* support: Change the printed support expiry date in ISO 8601 UTC format [Thodoris Greasidis]
* logs: Change the timestamp format to ISO 8601 UTC [Thodoris Greasidis]
* Pin flowzone to v4.7.1 [Felipe Lalanne]
* Update etcher-sdk to v8.5.3 [Felipe Lalanne]
* Update vercel/pkg to v5.8.1 [Felipe Lalanne]
* Update to Node 16 [Felipe Lalanne]
## 15.2.3 - 2023-05-03
* Use valid release uuid for local releases [Felipe Lalanne]
## 15.2.2 - 2023-04-28
* Remove nvmrc [Felipe Lalanne]
## 15.2.1 - 2023-04-28
* Fix tslib going out of sync causing HUP to fail [Thodoris Greasidis]
## 15.2.0 - 2023-04-05
<details>
<summary> Add support for device restarts in open-balena [Thodoris Greasidis] </summary>
> ### balena-sdk-16.40.0 - 2023-04-05
>
> * device.reboot: Fix the typings requiring a second argument [Thodoris Greasidis]
> * device.restartApplication: Use the supervisor endpoint to issue restarts [Thodoris Greasidis]
>
> ### balena-sdk-16.39.1 - 2023-04-04
>
> * patch: Split instruction strings on linebreak [Vipul Gupta (@vipulgupta2048)]
>
> ### balena-sdk-16.39.0 - Invalid date
>
> * Add `device history` model [fisehara]
>
> ### balena-sdk-16.38.2 - 2023-03-28
>
> * Fix credit-bundle jsdocs [Josh Bowling]
>
> ### balena-sdk-16.38.1 - 2023-03-27
>
> * Deprecate the device-type.json's instructions field [Thodoris Greasidis]
>
> ### balena-sdk-16.38.0 - 2023-03-21
>
> * Add aliases for the DT contrast slugs used in getInstructions [Thodoris Greasidis]
>
> ### balena-sdk-16.37.0 - 2023-03-21
>
> * device-type/getInstructions: Overload to accept the device type contract [Thodoris Greasidis]
>
> ### balena-sdk-16.36.6 - 2023-03-20
>
> * Update TypeScript to 5.0.2 [Thodoris Greasidis]
>
> ### balena-sdk-16.36.5 - 2023-03-16
>
> * patch: Improve jetsonFlash provisioning partial [Vipul Gupta (@vipulgupta2048)]
>
> ### balena-sdk-16.36.4 - 2023-03-15
>
> * Avoid running write operation tests in parallel to support retries [Thodoris Greasidis]
> * Retry failing tests twice [Thodoris Greasidis]
> * Fix tests per removal of `microservices-starter` application type [myarmolinsky]
>
> ### balena-sdk-16.36.3 - 2023-02-28
>
> * models/device-type: Add test for Radxa Zero instructions [Alexandru Costache]
> * lib/models: Add radxaFlash protocol for Radxa boards [Alexandru Costache]
>
> ### balena-sdk-16.36.2 - 2023-02-24
>
> * tests: Stop using flowzone internal env vars to for skipping npm test [Thodoris Greasidis]
>
> ### balena-sdk-16.36.1 - 2023-02-20
>
> * Add plan validity date fields [Josh Bowling]
>
> ### balena-sdk-16.36.0 - 2023-02-16
>
> * Add contract partial based instruction generation [Micah Halter]
>
> ### balena-sdk-16.35.0 - 2023-02-10
>
> * Add `CreditBundle` model [myarmolinsky]
>
> ### balena-sdk-16.34.0 - 2023-02-09
>
> * Add configVarInvalidRegex to Config Var typing [Felipe Lalanne]
>
> ### balena-sdk-16.33.0 - 2023-02-09
>
> * CurrentServiceWithCommit: Add release `raw_version` to type [myarmolinsky]
>
> ### balena-sdk-16.32.3 - 2023-02-07
>
> * Optimize the device.get method [Thodoris Greasidis]
>
> ### balena-sdk-16.32.2 - 2023-02-02
>
> * Improve pine typings for public resources without id fields [Thodoris Greasidis]
>
> ### balena-sdk-16.32.1 - 2023-01-16
>
> * Drop no longer used .travis.yml & .hound.yml [Thodoris Greasidis]
> * Rerun prettier [Thodoris Greasidis]
>
> ### balena-sdk-16.32.0 - 2023-01-05
>
> * typings: Add the device.is_frozen field [Thodoris Greasidis]
>
> ### balena-sdk-16.31.2 - 2022-12-20
>
> * application.create: Deprecate the `parent` option [Thodoris Greasidis]
> * Deprecate the device.getAllByParentDevice() method [Thodoris Greasidis]
> * Simplify the device.move() checks [Thodoris Greasidis]
>
> ### balena-sdk-16.31.1 - 2022-12-17
>
> * Replace appveyor with flowzone [Thodoris Greasidis]
>
> ### balena-sdk-16.31.0 - 2022-12-16
>
> * Add `updateAccountInfo` method to billing model for updating billing account info [myarmolinsky]
>
> ### balena-sdk-16.30.2 - 2022-12-13
>
> * Flowzone: Allow external contributions [Thodoris Greasidis]
>
> ### balena-sdk-16.30.1 - 2022-12-07
>
> * patch: bump catch-uncommitted from 1.6.2 to 2.0.0 [dependabot[bot]]
>
> ### balena-sdk-16.30.0 - 2022-11-24
>
> * Add utils and export mergePineOptions `balena.utils.mergePineOptions()` [JSReds]
>
> ### balena-sdk-16.29.3 - 2022-11-24
>
> * device.getWithServiceDetails: Stop auto-expanding the gateway_downloads [Thodoris Greasidis]
>
> ### balena-sdk-16.29.2 - 2022-11-16
>
> * Update TypeScript to 4.9.3 [Thodoris Greasidis]
>
> ### balena-sdk-16.29.1 - 2022-11-12
>
> * Fix release end_timestamp type [Thodoris Greasidis]
>
> ### balena-sdk-16.29.0 - 2022-11-12
>
>
> <details>
> <summary> Support filtered $count operations inside $filter & $orderby [Thodoris Greasidis] </summary>
>
>> #### pinejs-client-js-6.12.0 - 2022-11-10
>>
>> * Deprecate the 'a/count' notation in $orderby [Thodoris Greasidis]
>> * Deprecate the $count: { $op: number } notation [Thodoris Greasidis]
>> * Add support for `$filter: { $op: [{ $count: {} }, number] }` notation [Thodoris Greasidis]
>>
>> #### pinejs-client-js-6.11.0 - 2022-11-09
>>
>> * Deprecate non-$filter props inside `$expand: { a: { $count: {...}}}` [Thodoris Greasidis]
>> * Add support for `$orderby: { a: { $count: ... }, $dir: 'asc' }` notation [Thodoris Greasidis]
>>
>> #### pinejs-client-js-6.10.7 - 2022-11-07
>>
>> * Refactor the deprecation message definitions [Thodoris Greasidis]
>>
>> #### pinejs-client-js-6.10.6 - 2022-11-01
>>
>> * tests: Support `.only` & `.skip` in the higher level test functions [Thodoris Greasidis]
>>
>> #### pinejs-client-js-6.10.5 - 2022-10-14
>>
>> * Flowzone: Use inherited secrets [Pagan Gazzard]
>>
>> #### pinejs-client-js-6.10.4 - 2022-09-26
>>
>> * Specify node 10 as the minimum supported node engine in the package.json [Thodoris Greasidis]
>> * Replace balenaCI with flowzone [Thodoris Greasidis]
>>
>> #### pinejs-client-js-6.10.3 - 2022-09-15
>>
>> * Fix $count typings to only allow $filter under it [Thodoris Greasidis]
>>
>> #### pinejs-client-js-6.10.2 - 2022-04-08
>>
>> * Update dependencies [Pagan Gazzard]
>> * Remove circleci [Pagan Gazzard]
>>
>> #### pinejs-client-js-6.10.1 - 2022-02-08
>>
>> * Do not await the _request() result to allow enhanced promises downstream [Thodoris Greasidis]
>>
>> #### pinejs-client-js-6.10.0 - 2022-01-24
>>
>> * Add optional retry logic to client [Paul Jonathan Zoulin]
>>
>
> </details>
>
>
> ### balena-sdk-16.28.4 - 2022-11-04
>
> * Use deep imports for date-fns to improve tree-shaking [Thodoris Greasidis]
> * Enable esModuleInterop build option [Thodoris Greasidis]
>
> ### balena-sdk-16.28.3 - 2022-11-03
>
> * Update balena-errors to v4.7.3 [JSReds]
>
</details>
## 15.1.3 - 2023-04-05
<details>
<summary> devices supported: Fix showing types without a valid & finalized release [Thodoris Greasidis] </summary>
> ### balena-sdk-16.28.2 - 2022-10-27
>
> * Update tests to run on node 18 [Thodoris Greasidis]
> * deviceType.getAllSupported: Require a valid & final release to exist [Thodoris Greasidis]
>
> ### balena-sdk-16.28.1 - 2022-10-14
>
> * flowzone: Run the node tests using the latest LTS version [Thodoris Greasidis]
>
</details>
## 15.1.2 - 2023-03-27
* Improve type checking by using the satisfies operator [Thodoris Greasidis]

View File

@ -78,8 +78,8 @@ If you are a Node.js developer, you may wish to install the balena CLI via [npm]
The npm installation involves building native (platform-specific) binary modules, which require
some development tools to be installed first, as follows.
> **The balena CLI currently requires Node.js version 14.**
> **Versions 15 and later are not yet fully supported.**
> **The balena CLI currently requires Node.js version 16.**
> **Versions 17 and later are not yet fully supported.**
### Install development tools
@ -89,7 +89,7 @@ some development tools to be installed first, as follows.
$ sudo apt-get update && sudo apt-get -y install curl python3 git make g++
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
$ . ~/.bashrc
$ nvm install 14
$ nvm install 16
```
The `curl` command line above uses
@ -106,15 +106,15 @@ recommended.
```sh
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
$ . ~/.bashrc
$ nvm install 14
$ nvm install 16
```
#### **Windows** (not WSL)
Install:
* Node.js v14 from the [Nodejs.org releases page](https://nodejs.org/en/download/releases/).
* If you'd like the ability to switch between Node.js versions, install
- Node.js v16 from the [Nodejs.org releases page](https://nodejs.org/en/download/releases/).
[nvm-windows](https://github.com/coreybutler/nvm-windows#node-version-manager-nvm-for-windows)
instead.
* The [MSYS2 shell](https://www.msys2.org/), which provides `git`, `make`, `g++` and more:

View File

@ -87,7 +87,7 @@ async function diffPkgOutput(pkgOut: string) {
'tests',
'test-data',
'pkg',
`expected-warnings-${process.platform}.txt`,
`expected-warnings-${process.platform}-${arch}.txt`,
);
const absSavedPath = path.join(ROOT, relSavedPath);
const ignoreStartsWith = [
@ -180,9 +180,18 @@ async function execPkg(...args: any[]) {
* to be directly executed from inside another binary executable.)
*/
async function buildPkg() {
// https://github.com/vercel/pkg#targets
let targets = `linux-${arch}`;
// TBC: not possible to build for macOS or Windows arm64 on x64 nodes
if (process.platform === 'darwin') {
targets = `macos-x64`;
}
if (process.platform === 'win32') {
targets = `win-x64`;
}
const args = [
'--target',
'host',
'--targets',
targets,
'--output',
'build-bin/balena',
'package.json',
@ -456,16 +465,17 @@ async function signWindowsInstaller() {
* Wait for Apple Installer Notarization to continue
*/
async function notarizeMacInstaller(): Promise<void> {
const teamId = process.env.XCODE_APP_LOADER_TEAM_ID || '66H43P8FRG';
const appleId =
process.env.XCODE_APP_LOADER_EMAIL || 'accounts+apple@balena.io';
const appBundleId = packageJSON.oclif.macos.identifier || 'io.balena.cli';
const appleIdPassword = process.env.XCODE_APP_LOADER_PASSWORD;
if (appleIdPassword) {
const { notarize } = await import('electron-notarize');
// https://github.com/electron/notarize/blob/main/README.md
if (appleIdPassword && teamId) {
const { notarize } = await import('@electron/notarize');
// https://github.com/electron/notarize#readme
await notarize({
appBundleId,
tool: 'notarytool',
teamId,
appPath: renamedOclifInstallers.darwin,
appleId,
appleIdPassword,

View File

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

View File

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

View File

@ -2160,6 +2160,9 @@ confuse the balenaOS "development mode" with a device's "local mode", the latter
being a supervisor feature that allows the "balena push" command to push a user's
application directly to a device in the local network.
The '--secureBoot' option is used to configure a balenaOS installer image to opt-in
secure boot and disk encryption.
The --system-connection (-c) option is used to inject NetworkManager connection
profiles for additional network interfaces, such as cellular/GSM or additional
WiFi or ethernet connections. This option may be passed multiple times in case there
@ -2230,6 +2233,10 @@ WiFi SSID (network name) (non-interactive configuration)
Configure balenaOS to operate in development mode
#### --secureBoot
Configure balenaOS installer to opt-in secure boot and disk encryption
#### -d, --device DEVICE
device UUID
@ -2314,6 +2321,9 @@ confuse the balenaOS "development mode" with a device's "local mode", the latter
being a supervisor feature that allows the "balena push" command to push a user's
application directly to a device in the local network.
The '--secureBoot' option is used to configure a balenaOS installer image to opt-in
secure boot and disk encryption.
To configure an image for a fleet of mixed device types, use the --fleet option
alongside the --deviceType option to specify the target device type.
@ -2337,6 +2347,7 @@ Examples:
$ balena config generate --device 7cf02a6 --version 2.12.7 --deviceApiKey <existingDeviceKey>
$ balena config generate --device 7cf02a6 --version 2.12.7 --output config.json
$ balena config generate --fleet myorg/fleet --version 2.12.7 --dev
$ balena config generate --fleet myorg/fleet --version 2.12.7 --secureBoot
$ balena config generate --fleet myorg/fleet --version 2.12.7 --deviceType fincm3
$ balena config generate --fleet myorg/fleet --version 2.12.7 --output config.json
$ balena config generate --fleet myorg/fleet --version 2.12.7 --network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 15
@ -2355,6 +2366,10 @@ fleet name or slug (preferred)
Configure balenaOS to operate in development mode
#### --secureBoot
Configure balenaOS installer to opt-in secure boot and disk encryption
#### -d, --device DEVICE
device UUID

View File

@ -0,0 +1,80 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
interface FlagsDef {
help: void;
}
interface ArgsDef {
ids: string;
}
export default class RevokeCmd extends Command {
public static description = stripIndent`
Revoke balenaCloud API keys.
Revoke balenaCloud API keys with the given
comma-separated list of ids.
The given balenaCloud API keys will no longer be usable.
`;
public static examples = [
'$ balena api-key revoke 123',
'$ balena api-key revoke 123,124,456',
];
public static args = [
{
name: 'ids',
description: 'the API key ids',
required: true,
},
];
public static usage = 'api-key revoke <ids>';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params } = this.parse<FlagsDef, ArgsDef>(RevokeCmd);
try {
const apiKeyIds = params.ids.split(',');
if (apiKeyIds.filter((apiKeyId) => !apiKeyId.match(/^\d+$/)).length > 0) {
console.log('API key ids must be positive integers');
return;
}
await Promise.all(
apiKeyIds.map(
async (id) => await getBalenaSdk().models.apiKey.revoke(Number(id)),
),
);
console.log('Successfully revoked the given API keys');
} catch (e) {
throw e;
}
}
}

View File

@ -0,0 +1,95 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
interface FlagsDef {
help: void;
user?: void;
fleet?: string;
}
export default class ApiKeysCmd extends Command {
public static description = stripIndent`
Print a list of balenaCloud API keys.
Print a list of balenaCloud API keys.
Print a list of balenaCloud API keys for the current user or for a specific fleet with the \`--fleet\` option.
`;
public static examples = ['$ balena api-keys'];
public static args = [];
public static usage = 'api-keys';
public static flags: flags.Input<FlagsDef> = {
help: cf.help,
user: flags.boolean({
char: 'u',
description: 'show API keys for your user',
}),
fleet: cf.fleet,
};
public static authenticated = true;
public async run() {
const { flags: options } = this.parse<FlagsDef, {}>(ApiKeysCmd);
try {
const { getApplication } = await import('../../utils/sdk');
const actorId = options.fleet
? (
await getApplication(getBalenaSdk(), options.fleet, {
$select: 'actor',
})
).actor
: await getBalenaSdk().auth.getUserActorId();
const keys = await getBalenaSdk().pine.get({
resource: 'api_key',
options: {
$select: ['id', 'created_at', 'name', 'description', 'expiry_date'],
$filter: {
is_of__actor: actorId,
...(options.user
? {
name: {
$ne: null,
},
}
: {}),
},
$orderby: 'name asc',
},
});
const fields = ['id', 'name', 'created_at', 'description', 'expiry_date'];
const _ = await import('lodash');
console.log(
getVisuals().table.horizontal(
keys.map((key) => _.mapValues(key, (val) => val ?? 'N/a')),
fields,
),
);
} catch (e) {
throw e;
}
}
}

150
lib/commands/app/create.ts Normal file
View File

@ -0,0 +1,150 @@
/**
* @license
* Copyright 2016-2021 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { flags } from '@oclif/command';
import Command from '../../command';
import { ExpectedError } from '../../errors';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
interface FlagsDef {
organization?: string;
type?: string; // application device type
help: void;
}
interface ArgsDef {
name: string;
}
export default class AppCreateCmd extends Command {
public static description = stripIndent`
Create an app.
Create a new balena app.
You can specify the organization the app should belong to using
the \`--organization\` option. The organization's handle, not its name,
should be provided. Organization handles can be listed with the
\`balena orgs\` command.
The app's default device type is specified with the \`--type\` option.
The \`balena devices supported\` command can be used to list the available
device types.
Interactive dropdowns will be shown for selection if no device type or
organization is specified and there are multiple options to choose from.
If there is a single option to choose from, it will be chosen automatically.
This interactive behavior can be disabled by explicitly specifying a device
type and organization.
`;
public static examples = [
'$ balena app create MyApp',
'$ balena app create MyApp --organization mmyorg',
'$ balena app create MyApp -o myorg --type raspberry-pi',
];
public static args = [
{
name: 'name',
description: 'app name',
required: true,
},
];
public static usage = 'app create <name>';
public static flags: flags.Input<FlagsDef> = {
organization: flags.string({
char: 'o',
description: 'handle of the organization the app should belong to',
}),
type: flags.string({
char: 't',
description:
'app device type (Check available types with `balena devices supported`)',
}),
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
AppCreateCmd,
);
// Ascertain device type
const deviceType =
options.type ||
(await (await import('../../utils/patterns')).selectDeviceType());
// Ascertain organization
const organization =
options.organization?.toLowerCase() || (await this.getOrganization());
// Create application
try {
const application = await getBalenaSdk().models.application.create({
name: params.name,
deviceType,
organization,
applicationClass: 'app',
});
// Output
console.log(
`App created: slug "${application.slug}", device type "${deviceType}"`,
);
} catch (err) {
if ((err.message || '').toLowerCase().includes('unique')) {
// BalenaRequestError: Request error: "organization" and "app_name" must be unique.
throw new ExpectedError(
`Error: An app or block or fleet with the name "${params.name}" already exists in organization "${organization}".`,
);
} else if ((err.message || '').toLowerCase().includes('unauthorized')) {
// BalenaRequestError: Request error: Unauthorized
throw new ExpectedError(
`Error: You are not authorized to create apps in organization "${organization}".`,
);
}
throw err;
}
}
async getOrganization() {
const { getOwnOrganizations } = await import('../../utils/sdk');
const organizations = await getOwnOrganizations(getBalenaSdk(), {
$select: ['name', 'handle'],
});
if (organizations.length === 0) {
// User is not a member of any organizations (should not happen).
throw new Error('This account is not a member of any organizations');
} else if (organizations.length === 1) {
// User is a member of only one organization - use this.
return organizations[0].handle;
} else {
// User is a member of multiple organizations -
const { selectOrganization } = await import('../../utils/patterns');
return selectOrganization(organizations);
}
}
}

View File

@ -0,0 +1,150 @@
/**
* @license
* Copyright 2016-2021 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { flags } from '@oclif/command';
import Command from '../../command';
import { ExpectedError } from '../../errors';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
interface FlagsDef {
organization?: string;
type?: string; // application device type
help: void;
}
interface ArgsDef {
name: string;
}
export default class BlockCreateCmd extends Command {
public static description = stripIndent`
Create an block.
Create a new balena block.
You can specify the organization the block should belong to using
the \`--organization\` option. The organization's handle, not its name,
should be provided. Organization handles can be listed with the
\`balena orgs\` command.
The block's default device type is specified with the \`--type\` option.
The \`balena devices supported\` command can be used to list the available
device types.
Interactive dropdowns will be shown for selection if no device type or
organization is specified and there are multiple options to choose from.
If there is a single option to choose from, it will be chosen automatically.
This interactive behavior can be disabled by explicitly specifying a device
type and organization.
`;
public static examples = [
'$ balena block create MyBlock',
'$ balena block create MyBlock --organization mmyorg',
'$ balena block create MyBlock -o myorg --type raspberry-pi',
];
public static args = [
{
name: 'name',
description: 'block name',
required: true,
},
];
public static usage = 'block create <name>';
public static flags: flags.Input<FlagsDef> = {
organization: flags.string({
char: 'o',
description: 'handle of the organization the block should belong to',
}),
type: flags.string({
char: 't',
description:
'block device type (Check available types with `balena devices supported`)',
}),
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
BlockCreateCmd,
);
// Ascertain device type
const deviceType =
options.type ||
(await (await import('../../utils/patterns')).selectDeviceType());
// Ascertain organization
const organization =
options.organization?.toLowerCase() || (await this.getOrganization());
// Create application
try {
const application = await getBalenaSdk().models.application.create({
name: params.name,
deviceType,
organization,
applicationClass: 'block',
});
// Output
console.log(
`Block created: slug "${application.slug}", device type "${deviceType}"`,
);
} catch (err) {
if ((err.message || '').toLowerCase().includes('unique')) {
// BalenaRequestError: Request error: "organization" and "app_name" must be unique.
throw new ExpectedError(
`Error: An app or block or fleet with the name "${params.name}" already exists in organization "${organization}".`,
);
} else if ((err.message || '').toLowerCase().includes('unauthorized')) {
// BalenaRequestError: Request error: Unauthorized
throw new ExpectedError(
`Error: You are not authorized to create blocks in organization "${organization}".`,
);
}
throw err;
}
}
async getOrganization() {
const { getOwnOrganizations } = await import('../../utils/sdk');
const organizations = await getOwnOrganizations(getBalenaSdk(), {
$select: ['name', 'handle'],
});
if (organizations.length === 0) {
// User is not a member of any organizations (should not happen).
throw new Error('This account is not a member of any organizations');
} else if (organizations.length === 1) {
// User is a member of only one organization - use this.
return organizations[0].handle;
} else {
// User is a member of multiple organizations -
const { selectOrganization } = await import('../../utils/patterns');
return selectOrganization(organizations);
}
}
}

View File

@ -20,7 +20,7 @@ import Command from '../command';
import { getBalenaSdk } from '../utils/lazy';
import * as cf from '../utils/common-flags';
import * as compose from '../utils/compose';
import type { Application, ApplicationType, BalenaSDK } from 'balena-sdk';
import type { ApplicationType, BalenaSDK } from 'balena-sdk';
import {
buildArgDeprecation,
dockerignoreHelp,
@ -208,7 +208,7 @@ ${dockerignoreHelp}
* buildEmulated
* buildOpts: arguments to forward to docker build command
*
* @param {DockerToolbelt} docker
* @param {Dockerode} docker
* @param {Logger} logger
* @param {ComposeOpts} composeOpts
* @param opts
@ -218,7 +218,9 @@ ${dockerignoreHelp}
logger: import('../utils/logger'),
composeOpts: ComposeOpts,
opts: {
app?: Application;
app?: {
application_type: [Pick<ApplicationType, 'supports_multicontainer'>];
};
arch: string;
deviceType: string;
buildEmulated: boolean;
@ -234,7 +236,7 @@ ${dockerignoreHelp}
opts.buildOpts.t,
);
const appType = (opts.app?.application_type as ApplicationType[])?.[0];
const appType = opts.app?.application_type?.[0];
if (
appType != null &&
project.descriptors.length > 1 &&

View File

@ -19,13 +19,18 @@ import { flags } from '@oclif/command';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, getCliForm, stripIndent } from '../../utils/lazy';
import { applicationIdInfo, devModeInfo } from '../../utils/messages';
import type { PineDeferred } from 'balena-sdk';
import {
applicationIdInfo,
devModeInfo,
secureBootInfo,
} from '../../utils/messages';
import type { BalenaSDK, PineDeferred } from 'balena-sdk';
interface FlagsDef {
version: string; // OS version
fleet?: string;
dev?: boolean; // balenaOS development variant
secureBoot?: boolean;
device?: string;
deviceApiKey?: string;
deviceType?: string;
@ -51,6 +56,8 @@ export default class ConfigGenerateCmd extends Command {
${devModeInfo.split('\n').join('\n\t\t')}
${secureBootInfo.split('\n').join('\n\t\t')}
To configure an image for a fleet of mixed device types, use the --fleet option
alongside the --deviceType option to specify the target device type.
@ -66,6 +73,7 @@ export default class ConfigGenerateCmd extends Command {
'$ balena config generate --device 7cf02a6 --version 2.12.7 --deviceApiKey <existingDeviceKey>',
'$ balena config generate --device 7cf02a6 --version 2.12.7 --output config.json',
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --dev',
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --secureBoot',
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --deviceType fincm3',
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --output config.json',
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 15',
@ -80,6 +88,7 @@ export default class ConfigGenerateCmd extends Command {
}),
fleet: { ...cf.fleet, exclusive: ['device'] },
dev: cf.dev,
secureBoot: cf.secureBoot,
device: {
...cf.device,
exclusive: [
@ -135,17 +144,25 @@ export default class ConfigGenerateCmd extends Command {
public static authenticated = true;
public async getApplication(balena: BalenaSDK, fleet: string) {
const { getApplication } = await import('../../utils/sdk');
return await getApplication(balena, fleet, {
$select: 'slug',
$expand: {
is_for__device_type: { $select: 'slug' },
},
});
}
public async run() {
const { flags: options } = this.parse<FlagsDef, {}>(ConfigGenerateCmd);
const { getApplication } = await import('../../utils/sdk');
const balena = getBalenaSdk();
await this.validateOptions(options);
let resourceDeviceType: string;
let application: ApplicationWithDeviceType | null = null;
let application: Awaited<ReturnType<typeof this.getApplication>> | null =
null;
let device:
| (DeviceWithDeviceType & { belongs_to__application: PineDeferred })
| null = null;
@ -165,11 +182,7 @@ export default class ConfigGenerateCmd extends Command {
resourceDeviceType = device.is_of__device_type[0].slug;
} else {
// Disambiguate application (if is a number, it could either be an ID or a numerical name)
application = (await getApplication(balena, options.fleet!, {
$expand: {
is_for__device_type: { $select: 'slug' },
},
})) as ApplicationWithDeviceType;
application = await this.getApplication(balena, options.fleet!);
resourceDeviceType = application.is_for__device_type[0].slug;
}
@ -191,8 +204,16 @@ export default class ConfigGenerateCmd extends Command {
}
}
const deviceManifest = await balena.models.device.getManifestBySlug(
const deviceManifest =
await balena.models.config.getDeviceTypeManifestBySlug(deviceType);
const { validateSecureBootOptionAndWarn } = await import(
'../../utils/config'
);
await validateSecureBootOptionAndWarn(
options.secureBoot,
deviceType,
options.version,
);
// Prompt for values
@ -203,6 +224,7 @@ export default class ConfigGenerateCmd extends Command {
});
answers.version = options.version;
answers.developmentMode = options.dev;
answers.secureBoot = options.secureBoot;
answers.provisioningKeyName = options['provisioning-key-name'];
answers.provisioningKeyExpiryDate = options['provisioning-key-expiry-date'];

View File

@ -43,15 +43,13 @@ import {
parseReleaseTagKeysAndValues,
} from '../utils/compose_ts';
import { dockerCliFlags } from '../utils/docker';
import type {
Application,
ApplicationType,
DeviceType,
Release,
} from 'balena-sdk';
import type { ApplicationType, DeviceType, Release } from 'balena-sdk';
interface ApplicationWithArch extends Application {
interface ApplicationWithArch {
id: number;
arch: string;
is_for__device_type: [Pick<DeviceType, 'slug'>];
application_type: [Pick<ApplicationType, 'slug' | 'supports_multicontainer'>];
}
interface FlagsDef extends ComposeCliFlags, DockerCliFlags {
@ -235,7 +233,7 @@ ${dockerignoreHelp}
releaseTagValues,
);
if (options.note) {
await sdk.models.release.note(release.id, options.note);
await sdk.models.release.setNote(release.id, options.note);
}
}
@ -262,7 +260,7 @@ ${dockerignoreHelp}
'../utils/compose_ts'
);
const appType = (opts.app?.application_type as ApplicationType[])?.[0];
const appType = opts.app.application_type[0];
try {
const project = await loadProject(
@ -319,7 +317,7 @@ ${dockerignoreHelp}
projectName: project.name,
composition: compositionToBuild,
arch: opts.app.arch,
deviceType: (opts.app?.is_for__device_type as DeviceType[])?.[0].slug,
deviceType: opts.app.is_for__device_type[0].slug,
emulated: opts.buildEmulated,
buildOpts: opts.buildOpts,
inlineLogs: composeOpts.inlineLogs,

View File

@ -124,20 +124,16 @@ export default class DeviceInitCmd extends Command {
const balena = getBalenaSdk();
// Get application and
const application = (await getApplication(
balena,
options.fleet ||
(
await (await import('../../utils/patterns')).selectApplication()
).slug,
{
$expand: {
is_for__device_type: {
$select: 'slug',
const application = options.fleet
? await getApplication(balena, options.fleet, {
$select: ['id', 'slug'],
$expand: {
is_for__device_type: {
$select: 'slug',
},
},
},
},
)) as ApplicationWithDeviceType;
})
: await (await import('../../utils/patterns')).selectApplication();
// Register new device
const deviceUuid = balena.models.device.generateUniqueKey();

View File

@ -30,13 +30,6 @@ import { ExpectedError } from '../../errors';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { applicationIdInfo } from '../../utils/messages';
type ExtendedDevice = PineTypedResult<
Device,
typeof import('../../utils/helpers').expandForAppNameAndCpuArch
> & {
application_name?: string;
};
interface FlagsDef {
fleet?: string;
help: void;
@ -82,6 +75,33 @@ export default class DeviceMoveCmd extends Command {
public static authenticated = true;
private async getDevices(balena: BalenaSDK, deviceUuids: string[]) {
const deviceOptions = {
$select: 'belongs_to__application',
$expand: {
is_of__device_type: {
$select: 'is_of__cpu_architecture',
$expand: {
is_of__cpu_architecture: {
$select: 'slug',
},
},
},
},
} satisfies PineOptions<Device>;
// TODO: Refacor once `device.get()` accepts an array of uuids`
const devices = await Promise.all(
deviceUuids.map(
(uuid) =>
balena.models.device.get(uuid, deviceOptions) as Promise<
PineTypedResult<Device, typeof deviceOptions>
>,
),
);
return devices;
}
public async run() {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
DeviceMoveCmd,
@ -89,36 +109,17 @@ export default class DeviceMoveCmd extends Command {
const balena = getBalenaSdk();
const { expandForAppNameAndCpuArch } = await import('../../utils/helpers');
// Split uuids string into array of uuids
const deviceUuids = params.uuid.split(',');
// Get devices
const devices = await Promise.all(
deviceUuids.map(
(uuid) =>
balena.models.device.get(
uuid,
expandForAppNameAndCpuArch,
) as Promise<ExtendedDevice>,
),
);
// Map application name for each device
for (const device of devices) {
const belongsToApplication = device.belongs_to__application;
device.application_name = belongsToApplication?.[0]
? belongsToApplication[0].app_name
: 'N/a';
}
const devices = await this.getDevices(balena, deviceUuids);
// Disambiguate application
const { getApplication } = await import('../../utils/sdk');
// Get destination application
const application = options.fleet
? await getApplication(balena, options.fleet)
? await getApplication(balena, options.fleet, { $select: ['id', 'slug'] })
: await this.interactivelySelectApplication(balena, devices);
// Move each device
@ -135,7 +136,7 @@ export default class DeviceMoveCmd extends Command {
async interactivelySelectApplication(
balena: BalenaSDK,
devices: ExtendedDevice[],
devices: Awaited<ReturnType<typeof this.getDevices>>,
) {
const { getExpandedProp } = await import('../../utils/pine');
// deduplicate the slugs
@ -181,7 +182,9 @@ export default class DeviceMoveCmd extends Command {
const application = await patterns.selectApplication(
(app) =>
compatibleDeviceTypeSlugs.has(app.is_for__device_type[0].slug) &&
devices.some((device) => device.application_name !== app.app_name),
devices.some(
(device) => device.belongs_to__application.__id !== app.id,
),
true,
);
return application;

View File

@ -78,7 +78,9 @@ export default class DeviceRegisterCmd extends Command {
const balena = getBalenaSdk();
const application = await getApplication(balena, params.fleet);
const application = await getApplication(balena, params.fleet, {
$select: ['id', 'slug'],
});
const uuid = options.uuid ?? balena.models.device.generateUniqueKey();
console.info(`Registering to ${application.slug}: ${uuid}`);

View File

@ -22,13 +22,7 @@ import { expandForAppName } from '../../utils/helpers';
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
import { applicationIdInfo, jsonInfo } from '../../utils/messages';
import type { Application, Device, PineOptions } from 'balena-sdk';
interface ExtendedDevice extends DeviceWithDeviceType {
dashboard_url?: string;
fleet?: string | null; // 'org/name' slug
device_type?: string | null;
}
import type { Device, PineOptions } from 'balena-sdk';
interface FlagsDef {
fleet?: string;
@ -82,37 +76,39 @@ export default class DevicesCmd extends Command {
const { flags: options } = this.parse<FlagsDef, {}>(DevicesCmd);
const balena = getBalenaSdk();
const devicesOptions = { ...devicesSelectFields, ...expandForAppName };
const devicesOptions = {
...devicesSelectFields,
...expandForAppName,
$orderby: { device_name: 'asc' },
} satisfies PineOptions<Device>;
let devices;
const devices = (
await (async () => {
if (options.fleet != null) {
const { getApplication } = await import('../../utils/sdk');
const application = await getApplication(balena, options.fleet, {
$select: 'slug',
$expand: {
owns__device: devicesOptions,
},
});
return application.owns__device;
}
if (options.fleet != null) {
const { getApplication } = await import('../../utils/sdk');
const application = await getApplication(balena, options.fleet);
devices = (await balena.models.device.getAllByApplication(
application.id,
devicesOptions,
)) as ExtendedDevice[];
} else {
devices = (await balena.models.device.getAll(
devicesOptions,
)) as ExtendedDevice[];
}
return await balena.pine.get({
resource: 'device',
options: devicesOptions,
});
})()
).map((device) => ({
...device,
dashboard_url: balena.models.device.getDashboardUrl(device.uuid),
fleet: device.belongs_to__application?.[0]?.slug || null,
uuid: options.json ? device.uuid : device.uuid.slice(0, 7),
device_type: device.is_of__device_type?.[0]?.slug || null,
}));
devices = devices.map(function (device) {
device.dashboard_url = balena.models.device.getDashboardUrl(device.uuid);
const belongsToApplication =
device.belongs_to__application as Application[];
device.fleet = belongsToApplication?.[0]?.slug || null;
device.uuid = options.json ? device.uuid : device.uuid.slice(0, 7);
device.device_type = device.is_of__device_type?.[0]?.slug || null;
return device;
});
const fields = [
const fields: Array<keyof (typeof devices)[number]> = [
'id',
'uuid',
'device_name',

View File

@ -151,16 +151,15 @@ export default class EnvAddCmd extends Command {
const varType = isConfigVar ? 'configVar' : 'envVar';
if (options.fleet) {
const { getFleetSlug } = await import('../../utils/sdk');
for (const app of options.fleet.split(',')) {
for (const appSlug of await resolveFleetSlugs(balena, options.fleet)) {
try {
await balena.models.application[varType].set(
await getFleetSlug(balena, app),
appSlug,
params.name,
params.value,
);
} catch (err) {
console.error(`${err.message}, fleet: ${app}`);
console.error(`${err.message}, fleet: ${appSlug}`);
process.exitCode = 1;
}
}
@ -181,6 +180,25 @@ export default class EnvAddCmd extends Command {
}
}
// TODO: Stop accepting application names in the next major
// and just drop this in favor of doing the .split(',') directly.
async function resolveFleetSlugs(
balena: BalenaSdk.BalenaSDK,
fleetOption: string,
) {
const fleetSlugs: string[] = [];
const { getFleetSlug } = await import('../../utils/sdk');
for (const appNameOrSlug of fleetOption.split(',')) {
try {
fleetSlugs.push(await getFleetSlug(balena, appNameOrSlug));
} catch (err) {
console.error(`${err.message}, fleet: ${appNameOrSlug}`);
process.exitCode = 1;
}
}
return fleetSlugs;
}
/**
* Add service variables for a device or fleet.
*/
@ -190,17 +208,17 @@ async function setServiceVars(
options: FlagsDef,
) {
if (options.fleet) {
for (const app of options.fleet.split(',')) {
for (const appSlug of await resolveFleetSlugs(sdk, options.fleet)) {
for (const service of options.service!.split(',')) {
try {
const serviceId = await getServiceIdForApp(sdk, app, service);
const serviceId = await getServiceIdForApp(sdk, appSlug, service);
await sdk.models.service.var.set(
serviceId,
params.name,
params.value!,
);
} catch (err) {
console.error(`${err.message}, fleet: ${app}`);
console.error(`${err.message}, fleet: ${appSlug}`);
process.exitCode = 1;
}
}
@ -245,11 +263,11 @@ async function setServiceVars(
*/
async function getServiceIdForApp(
sdk: BalenaSdk.BalenaSDK,
appName: string,
appSlug: string,
serviceName: string,
): Promise<number> {
let serviceId: number | undefined;
const services = await sdk.models.service.getAllByApplication(appName, {
const services = await sdk.models.service.getAllByApplication(appSlug, {
$filter: { service_name: serviceName },
});
if (services.length > 0) {
@ -257,7 +275,7 @@ async function getServiceIdForApp(
}
if (serviceId === undefined) {
throw new ExpectedError(
`Cannot find service ${serviceName} for fleet ${appName}`,
`Cannot find service ${serviceName} for fleet ${appSlug}`,
);
}
return serviceId;

View File

@ -16,7 +16,6 @@
*/
import { flags } from '@oclif/command';
import type { Application } from 'balena-sdk';
import Command from '../../command';
import { ExpectedError } from '../../errors';
@ -101,18 +100,22 @@ export default class FleetCreateCmd extends Command {
options.organization?.toLowerCase() || (await this.getOrganization());
// Create application
let application: Application;
try {
application = await getBalenaSdk().models.application.create({
const application = await getBalenaSdk().models.application.create({
name: params.name,
deviceType,
organization,
});
// Output
console.log(
`Fleet created: slug "${application.slug}", device type "${deviceType}"`,
);
} catch (err) {
if ((err.message || '').toLowerCase().includes('unique')) {
// BalenaRequestError: Request error: "organization" and "app_name" must be unique.
throw new ExpectedError(
`Error: fleet "${params.name}" already exists in organization "${organization}".`,
`Error: An app or block or fleet with the name "${params.name}" already exists in organization "${organization}".`,
);
} else if ((err.message || '').toLowerCase().includes('unauthorized')) {
// BalenaRequestError: Request error: Unauthorized
@ -123,16 +126,13 @@ export default class FleetCreateCmd extends Command {
throw err;
}
// Output
console.log(
`Fleet created: slug "${application.slug}", device type "${deviceType}"`,
);
}
async getOrganization() {
const { getOwnOrganizations } = await import('../../utils/sdk');
const organizations = await getOwnOrganizations(getBalenaSdk());
const organizations = await getOwnOrganizations(getBalenaSdk(), {
$select: ['name', 'handle'],
});
if (organizations.length === 0) {
// User is not a member of any organizations (should not happen).

View File

@ -17,7 +17,6 @@
import type { flags as flagsType } from '@oclif/command';
import { flags } from '@oclif/command';
import type { Release } from 'balena-sdk';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
@ -74,17 +73,12 @@ export default class FleetCmd extends Command {
const balena = getBalenaSdk();
const application = (await getApplication(balena, params.fleet, {
const application = await getApplication(balena, params.fleet, {
$expand: {
is_for__device_type: { $select: 'slug' },
should_be_running__release: { $select: 'commit' },
},
})) as ApplicationWithDeviceType & {
should_be_running__release: [Release?];
// For display purposes:
device_type: string;
commit?: string;
};
});
if (options.view) {
const open = await import('open');
@ -95,11 +89,14 @@ export default class FleetCmd extends Command {
return;
}
application.device_type = application.is_for__device_type[0].slug;
application.commit = application.should_be_running__release[0]?.commit;
const outputApplication = {
...application,
device_type: application.is_for__device_type[0].slug,
commit: application.should_be_running__release[0]?.commit,
};
await this.outputData(
application,
outputApplication,
['app_name', 'id', 'device_type', 'slug', 'commit'],
options,
);

View File

@ -65,7 +65,9 @@ export default class FleetPurgeCmd extends Command {
// balena.models.application.purge only accepts a numeric id
// so we must first fetch the app to get it's id,
const application = await getApplication(balena, params.fleet);
const application = await getApplication(balena, params.fleet, {
$select: 'id',
});
try {
await balena.models.application.purge(application.id);

View File

@ -16,7 +16,6 @@
*/
import type { flags } from '@oclif/command';
import type { ApplicationType } from 'balena-sdk';
import Command from '../../command';
import * as cf from '../../utils/common-flags';
@ -78,9 +77,10 @@ export default class FleetRenameCmd extends Command {
// Disambiguate target application (if params.params is a number, it could either be an ID or a numerical name)
const { getApplication } = await import('../../utils/sdk');
const application = await getApplication(balena, params.fleet, {
$select: ['id', 'app_name', 'slug'],
$expand: {
application_type: {
$select: ['slug'],
$select: 'slug',
},
},
});
@ -91,7 +91,7 @@ export default class FleetRenameCmd extends Command {
}
// Check app supports renaming
const appType = (application.application_type as ApplicationType[])?.[0];
const appType = application.application_type[0];
if (appType.slug === 'legacy-v1' || appType.slug === 'legacy-v2') {
throw new ExpectedError(
`Fleet ${params.fleet} is of 'legacy' type, and cannot be renamed.`,
@ -133,9 +133,9 @@ export default class FleetRenameCmd extends Command {
}
// Get application again, to be sure of results
const renamedApplication = await balena.models.application.get(
application.id,
);
const renamedApplication = await getApplication(balena, application.id, {
$select: ['app_name', 'slug'],
});
// Output result
console.log(`Fleet renamed`);

View File

@ -63,7 +63,9 @@ export default class FleetRestartCmd extends Command {
const balena = getBalenaSdk();
// Disambiguate application
const application = await getApplication(balena, params.fleet);
const application = await getApplication(balena, params.fleet, {
$select: 'slug',
});
await balena.models.application.restart(application.slug);
}

View File

@ -76,7 +76,9 @@ export default class FleetRmCmd extends Command {
);
// Disambiguate application (if is a number, it could either be an ID or a numerical name)
const application = await getApplication(balena, params.fleet);
const application = await getApplication(balena, params.fleet, {
$select: 'slug',
});
// Remove
await balena.models.application.remove(application.slug);

View File

@ -88,6 +88,6 @@ export default class NoteCmd extends Command {
const balena = getBalenaSdk();
return balena.models.device.note(options.device!, params.note);
return balena.models.device.setNote(options.device, params.note);
}
}

View File

@ -46,7 +46,9 @@ export default class OrgsCmd extends Command {
const { getOwnOrganizations } = await import('../utils/sdk');
// Get organizations
const organizations = await getOwnOrganizations(getBalenaSdk());
const organizations = await getOwnOrganizations(getBalenaSdk(), {
$select: ['name', 'handle'],
});
// Display
console.log(

View File

@ -23,7 +23,11 @@ import Command from '../../command';
import { ExpectedError } from '../../errors';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy';
import { applicationIdInfo, devModeInfo } from '../../utils/messages';
import {
applicationIdInfo,
devModeInfo,
secureBootInfo,
} from '../../utils/messages';
const CONNECTIONS_FOLDER = '/system-connections';
@ -36,6 +40,7 @@ interface FlagsDef {
'config-wifi-key'?: string;
'config-wifi-ssid'?: string;
dev?: boolean; // balenaOS development variant
secureBoot?: boolean;
device?: string; // device UUID
'device-type'?: string;
help?: void;
@ -53,6 +58,7 @@ interface ArgsDef {
interface Answers {
appUpdatePollInterval: number; // in minutes
developmentMode?: boolean; // balenaOS development variant
secureBoot?: boolean;
deviceType: string; // e.g. "raspberrypi3"
network: 'ethernet' | 'wifi';
version: string; // e.g. "2.32.0+rev1"
@ -80,6 +86,8 @@ export default class OsConfigureCmd extends Command {
${devModeInfo.split('\n').join('\n\t\t')}
${secureBootInfo.split('\n').join('\n\t\t')}
The --system-connection (-c) option is used to inject NetworkManager connection
profiles for additional network interfaces, such as cellular/GSM or additional
WiFi or ethernet connections. This option may be passed multiple times in case there
@ -140,6 +148,7 @@ export default class OsConfigureCmd extends Command {
description: 'WiFi SSID (network name) (non-interactive configuration)',
}),
dev: cf.dev,
secureBoot: cf.secureBoot,
device: {
...cf.device,
exclusive: [
@ -238,6 +247,15 @@ export default class OsConfigureCmd extends Command {
const { validateDevOptionAndWarn } = await import('../../utils/config');
await validateDevOptionAndWarn(options.dev, osVersion);
const { validateSecureBootOptionAndWarn } = await import(
'../../utils/config'
);
await validateSecureBootOptionAndWarn(
options.secureBoot,
deviceTypeSlug,
osVersion,
);
const answers: Answers = await askQuestionsForDeviceType(
deviceTypeManifest,
options,
@ -248,6 +266,7 @@ export default class OsConfigureCmd extends Command {
}
answers.version = osVersion;
answers.developmentMode = options.dev;
answers.secureBoot = options.secureBoot;
answers.provisioningKeyName = options['provisioning-key-name'];
answers.provisioningKeyExpiryDate = options['provisioning-key-expiry-date'];
@ -362,7 +381,9 @@ async function getOsVersionFromImage(
*/
async function checkDeviceTypeCompatibility(
options: FlagsDef,
app: ApplicationWithDeviceType,
app: {
is_for__device_type: [Pick<BalenaSdk.DeviceType, 'slug'>];
},
) {
if (options['device-type']) {
const helpers = await import('../../utils/helpers');

View File

@ -31,7 +31,14 @@ import { parseAsInteger } from '../utils/validation';
import { flags } from '@oclif/command';
import * as _ from 'lodash';
import type { Application, BalenaSDK, PineExpand, Release } from 'balena-sdk';
import type {
Application,
BalenaSDK,
PineExpand,
PineOptions,
PineTypedResult,
Release,
} from 'balena-sdk';
import type { Preloader } from 'balena-preload';
interface FlagsDef extends DockerConnectionCliFlags {
@ -242,7 +249,7 @@ Can be repeated to add multiple certificates.\
const dockerUtils = await import('../utils/docker');
const docker = await dockerUtils.getDocker(options);
const preloader = new balenaPreload.Preloader(
null,
undefined,
docker,
fleetSlug,
commit,
@ -308,7 +315,7 @@ Can be repeated to add multiple certificates.\
}
}
readonly applicationExpandOptions: PineExpand<Application> = {
readonly applicationExpandOptions = {
owns__release: {
$select: ['id', 'commit', 'end_timestamp', 'composition'],
$expand: {
@ -329,7 +336,7 @@ Can be repeated to add multiple certificates.\
should_be_running__release: {
$select: 'commit',
},
};
} satisfies PineExpand<Application>;
isCurrentCommit(commit: string) {
return commit === 'latest' || commit === 'current';
@ -343,7 +350,7 @@ Can be repeated to add multiple certificates.\
} catch {
throw new Error(`Device type "${deviceTypeSlug}" not found in API query`);
}
return (await balena.models.application.getAllDirectlyAccessible({
const options = {
$select: ['id', 'slug', 'should_track_latest_release'],
$expand: this.applicationExpandOptions,
$filter: {
@ -388,11 +395,10 @@ Can be repeated to add multiple certificates.\
},
},
$orderby: 'slug asc',
})) as Array<
ApplicationWithDeviceType & {
should_be_running__release: [Release?];
}
>;
} satisfies PineOptions<Application>;
return (await balena.models.application.getAllDirectlyAccessible(
options,
)) as Array<PineTypedResult<Application, typeof options>>;
}
async selectApplication(deviceTypeSlug: string) {
@ -442,7 +448,7 @@ Can be repeated to add multiple certificates.\
}
async offerToDisableAutomaticUpdates(
application: Application,
application: Pick<Application, 'id' | 'should_track_latest_release'>,
commit: string,
pinDevice: boolean,
) {
@ -494,9 +500,9 @@ Would you like to disable automatic updates for this fleet now?\
async getAppWithReleases(balenaSdk: BalenaSDK, slug: string) {
const { getApplication } = await import('../utils/sdk');
return (await getApplication(balenaSdk, slug, {
return await getApplication(balenaSdk, slug, {
$expand: this.applicationExpandOptions,
})) as Application & { should_be_running__release: [Release?] };
});
}
async prepareAndPreload(
@ -512,7 +518,7 @@ Would you like to disable automatic updates for this fleet now?\
const application = options.slug
? await this.getAppWithReleases(balenaSdk, options.slug)
: await this.selectApplication(preloader.config.deviceType);
: await this.selectApplication(preloader.config!.deviceType);
let commit: string; // commit hash or the strings 'latest' or 'current'

View File

@ -328,7 +328,7 @@ export default class PushCmd extends Command {
]);
const application = await getApplication(sdk, appNameOrSlug, {
$select: ['app_name', 'slug'],
$select: 'slug',
});
const opts = {
@ -358,7 +358,7 @@ export default class PushCmd extends Command {
releaseTagValues,
);
if (options.note) {
await sdk.models.release.note(releaseId, options.note);
await sdk.models.release.setNote(releaseId, options.note);
}
} else if (releaseTagKeys.length > 0) {
throw new Error(stripIndent`

View File

@ -149,7 +149,7 @@ export default class SupportCmd extends Command {
console.log(
`Access has been granted for ${duration}, expiring ${new Date(
expiryTs,
).toLocaleString()}`,
).toISOString()}`,
);
}
}

View File

@ -153,19 +153,19 @@ export default class TunnelCmd extends Command {
try {
await handler(client);
logConnection(
client.remoteAddress || '',
client.remotePort || 0,
client.localAddress,
client.localPort,
client.remoteAddress ?? '',
client.remotePort ?? 0,
client.localAddress ?? '',
client.localPort ?? 0,
uuid,
remotePort,
);
} catch (err) {
logConnection(
client.remoteAddress || '',
client.remotePort || 0,
client.localAddress,
client.localPort,
client.remoteAddress ?? '',
client.remotePort ?? 0,
client.localAddress ?? '',
client.localPort ?? 0,
uuid,
remotePort,
err,

View File

@ -14,10 +14,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Help from '@oclif/plugin-help';
import { Help } from '@oclif/core';
import { HelpFormatter } from '@oclif/core/lib/help/formatter';
import * as indent from 'indent-string';
import { getChalk } from './utils/lazy';
import { renderList } from '@oclif/plugin-help/lib/list';
import { ExpectedError } from './errors';
// Partially overrides standard implementation of help plugin
@ -39,9 +39,11 @@ function getHelpSubject(args: string[]): string | undefined {
}
export default class BalenaHelp extends Help {
public helpFormatter = new HelpFormatter(this.config);
public static usage: 'help [command]';
public showHelp(argv: string[]) {
public async showHelp(argv: string[]) {
const chalk = getChalk();
const subject = getHelpSubject(argv);
if (!subject) {
@ -52,7 +54,7 @@ export default class BalenaHelp extends Help {
const command = this.config.findCommand(subject);
if (command) {
this.showCommandHelp(command);
await this.showCommandHelp(command);
return;
}
@ -187,14 +189,15 @@ See: https://git.io/JRHUW#deprecation-policy`,
return '';
}
const body = renderList(
const body = this.helpFormatter.renderList(
commands
.filter((c) => c.usage != null && c.usage !== '')
.map((c) => [c.usage, this.formatDescription(c.description)]),
{
spacer: '\n',
stripAnsi: this.opts.stripAnsi,
maxWidth: this.opts.maxWidth - 2,
indentation: 2,
multiline: true,
},
);

View File

@ -74,6 +74,12 @@ export const dev: IBooleanFlag<boolean> = flags.boolean({
default: false,
});
export const secureBoot: IBooleanFlag<boolean> = flags.boolean({
description:
'Configure balenaOS installer to opt-in secure boot and disk encryption',
default: false,
});
export const drive = flags.string({
char: 'd',
description: stripIndent`

View File

@ -52,10 +52,14 @@ export interface ImgConfig {
os?: {
sshKeys?: string[];
};
installer?: {
secureboot?: boolean;
};
}
export async function generateApplicationConfig(
application: BalenaSdk.Application,
application: Pick<BalenaSdk.Application, 'slug'>,
options: {
version: string;
appUpdatePollInterval?: number;
@ -63,6 +67,7 @@ export async function generateApplicationConfig(
os?: {
sshKeys?: string[];
};
secureBoot?: boolean;
},
): Promise<ImgConfig> {
options = {
@ -84,6 +89,12 @@ export async function generateApplicationConfig(
: options.os.sshKeys;
}
// configure installer secure boot opt-in if specified
if (options.secureBoot) {
config.installer ??= {};
config.installer.secureboot = options.secureBoot;
}
return config;
}
@ -165,3 +176,54 @@ export async function validateDevOptionAndWarn(
and exposes network ports such as 2375 that allows unencrypted access to balenaEngine.
Therefore, development mode should only be used in private, trusted local networks.`);
}
/**
* Chech whether the `--secureBoot` option of commands related to OS configuration
* such as `os configure` and `config generate` is compatible with a given
* OS release, and print a warning regarding the consequences of using that
* option.
*/
export async function validateSecureBootOptionAndWarn(
secureBoot?: boolean,
slug?: string,
version?: string,
logger?: import('./logger'),
) {
if (!secureBoot) {
return;
}
const { ExpectedError } = await import('../errors');
if (!version) {
throw new ExpectedError(`Error: No version provided`);
}
if (!slug) {
throw new ExpectedError(`Error: No device type provided`);
}
const sdk = getBalenaSdk();
const [osRelease] = await sdk.models.os.getAllOsVersions(slug, {
$select: 'contract',
$filter: { raw_version: `${version.replace(/^v/, '')}` },
});
if (!osRelease) {
throw new ExpectedError(`Error: No ${version} release for ${slug}`);
}
const contract = osRelease.contract ? JSON.parse(osRelease.contract) : null;
if (
contract?.provides.some((entry: Dictionary<string>) => {
return entry.type === 'sw.feature' && entry.slug === 'secureboot';
})
) {
if (!logger) {
const Logger = await import('./logger');
logger = Logger.getLogger();
}
logger.logInfo(stripIndent`
The '--secureBoot' option is being used to configure a balenaOS installer image
into secure boot and full disk encryption.`);
} else {
throw new ExpectedError(
`Error: The '--secureBoot' option is not supported for ${slug} in ${version}`,
);
}
}

View File

@ -44,7 +44,7 @@ import { displayBuildLog } from './logs';
import { stripIndent } from '../lazy';
const LOCAL_APPNAME = 'localapp';
const LOCAL_RELEASEHASH = 'localrelease';
const LOCAL_RELEASEHASH = '10ca12e1ea5e';
const LOCAL_PROJECT_NAME = 'local_image';
// Define the logger here so the debug output

View File

@ -249,7 +249,7 @@ export class LivepushManager {
cwd: serviceContext,
followSymlinks: true,
ignoreInitial: true,
ignored: (filePath: string, stats: fs.Stats | undefined) => {
ignored: (filePath: string, stats?: fs.Stats) => {
if (!stats) {
try {
// sync because chokidar defines a sync interface

View File

@ -155,12 +155,8 @@ export function displayLogObject<T extends Log>(
system: boolean,
filterServices?: string[],
): void {
let toPrint: string;
if (obj.timestamp != null) {
toPrint = `[${new Date(obj.timestamp).toLocaleString()}]`;
} else {
toPrint = `[${new Date().toLocaleString()}]`;
}
const d = obj.timestamp != null ? new Date(obj.timestamp) : new Date();
let toPrint = `[${d.toISOString()}]`;
if (obj.serviceName != null) {
if (filterServices) {

View File

@ -119,7 +119,10 @@ export async function getManifest(
`The device type of the provided OS image ${manifest.slug}, does not match the expected device type ${deviceType}`,
);
}
return manifest ?? (await sdk.models.device.getManifestBySlug(deviceType));
return (
manifest ??
(await sdk.models.config.getDeviceTypeManifestBySlug(deviceType))
);
}
export const areDeviceTypesCompatible = async (
@ -177,11 +180,10 @@ export async function osProgressHandler(step: InitializeEmitter) {
});
}
export async function getAppWithArch(
applicationName: string,
): Promise<ApplicationWithDeviceType & { arch: string }> {
export async function getAppWithArch(applicationName: string) {
const { getApplication } = await import('./sdk');
const options: BalenaSdk.PineOptions<BalenaSdk.Application> = {
const balena = getBalenaSdk();
const app = await getApplication(balena, applicationName, {
$expand: {
application_type: {
$select: ['name', 'slug', 'supports_multicontainer'],
@ -195,20 +197,10 @@ export async function getAppWithArch(
},
},
},
};
const balena = getBalenaSdk();
const app = (await getApplication(
balena,
applicationName,
options,
)) as ApplicationWithDeviceType;
const { getExpanded } = await import('./pine');
});
return {
...app,
arch: getExpanded(
getExpanded(app.is_for__device_type)!.is_of__cpu_architecture,
)!.slug,
arch: app.is_for__device_type[0].is_of__cpu_architecture[0].slug,
};
}
@ -445,20 +437,6 @@ export const expandForAppName = {
},
} satisfies BalenaSdk.PineOptions<BalenaSdk.Device>;
export const expandForAppNameAndCpuArch = {
$expand: {
...expandForAppName.$expand,
is_of__device_type: {
$select: 'slug',
$expand: {
is_of__cpu_architecture: {
$select: 'slug',
},
},
},
},
} satisfies BalenaSdk.PineOptions<BalenaSdk.Device>;
/**
* Use the `readline` library on Windows to install SIGINT handlers.
* This appears to be necessary on MSYS / Git for Windows, and also useful

View File

@ -20,7 +20,7 @@ import type * as BalenaSdk from 'balena-sdk';
import type { Chalk } from 'chalk';
import type * as visuals from 'resin-cli-visuals';
import type * as CliForm from 'resin-cli-form';
import type { ux } from 'cli-ux';
import type { ux } from '@oclif/core';
// Equivalent of _.once but avoiding the need to import lodash for lazy deps
const once = <T>(fn: () => T) => {
@ -57,7 +57,9 @@ export const getCliForm = once(
() => require('resin-cli-form') as typeof CliForm,
);
export const getCliUx = once(() => require('cli-ux').ux as typeof ux);
export const getCliUx = once(
() => require('@oclif/core/lib/cli-ux') as typeof ux,
);
// Directly export stripIndent as we always use it immediately, but importing just `stripIndent` reduces startup time
export const stripIndent =

View File

@ -169,6 +169,10 @@ confuse the balenaOS "development mode" with a device's "local mode", the latter
being a supervisor feature that allows the "balena push" command to push a user's
application directly to a device in the local network.`;
export const secureBootInfo = `\
The '--secureBoot' option is used to configure a balenaOS installer image to opt-in
secure boot and disk encryption.`;
export const jsonInfo = `\
The --json option is recommended when scripting the output of this command,
because field names are less likely to change in JSON format and because it

View File

@ -14,7 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import type { Application, BalenaSDK, Device, Organization } from 'balena-sdk';
import type {
Application,
BalenaSDK,
Device,
Organization,
PineOptions,
PineTypedResult,
} from 'balena-sdk';
import _ = require('lodash');
import { instanceOf, NotLoggedInError, ExpectedError } from '../errors';
@ -157,46 +164,55 @@ export async function confirm(
}
}
export function selectApplication(
filter?: (app: ApplicationWithDeviceType) => boolean,
const selectApplicationPineOptions = {
$select: ['id', 'slug', 'app_name'],
$expand: {
is_for__device_type: {
$select: 'slug',
},
},
} satisfies PineOptions<Application>;
type SelectApplicationResult = PineTypedResult<
Application,
typeof selectApplicationPineOptions
>;
export async function selectApplication(
filter?: (app: SelectApplicationResult) => boolean,
errorOnEmptySelection = false,
) {
const balena = getBalenaSdk();
return balena.models.application
.hasAny()
.then(async (hasAnyApplications) => {
if (!hasAnyApplications) {
throw new ExpectedError('No fleets found');
}
const apps = (await balena.models.application.getAllDirectlyAccessible(
selectApplicationPineOptions,
)) as SelectApplicationResult[];
const apps = (await balena.models.application.getAllDirectlyAccessible({
$expand: {
is_for__device_type: {
$select: 'slug',
},
},
})) as ApplicationWithDeviceType[];
return apps.filter(filter || _.constant(true));
})
.then((applications) => {
if (errorOnEmptySelection && applications.length === 0) {
throw new ExpectedError('No suitable fleets found for selection');
}
return getCliForm().ask({
message: 'Select an application',
type: 'list',
choices: _.map(applications, (application) => ({
name: `${application.app_name} (${application.slug}) [${application.is_for__device_type[0].slug}]`,
value: application,
})),
});
});
if (!apps.length) {
throw new ExpectedError('No fleets found');
}
const applications = filter ? apps.filter(filter) : apps;
if (errorOnEmptySelection && applications.length === 0) {
throw new ExpectedError('No suitable fleets found for selection');
}
return getCliForm().ask({
message: 'Select an application',
type: 'list',
choices: _.map(applications, (application) => ({
name: `${application.app_name} (${application.slug}) [${application.is_for__device_type[0].slug}]`,
value: application,
})),
});
}
export async function selectOrganization(organizations?: Organization[]) {
export async function selectOrganization(
organizations?: Array<Pick<Organization, 'handle' | 'name'>>,
) {
// Use either provided orgs (if e.g. already loaded) or load from cloud
organizations =
organizations || (await getBalenaSdk().models.organization.getAll());
organizations ??= await getBalenaSdk().models.organization.getAll({
$select: ['name', 'handle'],
});
return getCliForm().ask({
message: 'Select an organization',
type: 'list',
@ -252,33 +268,6 @@ export async function awaitDeviceOsUpdate(
return uuid;
}
export function inferOrSelectDevice(preferredUuid: string) {
const balena = getBalenaSdk();
return balena.models.device.getAll().then((devices) => {
const onlineDevices = devices.filter((device) => device.is_online);
if (_.isEmpty(onlineDevices)) {
throw new ExpectedError("You don't have any devices online");
}
const defaultUuid = _(onlineDevices).map('uuid').includes(preferredUuid)
? preferredUuid
: onlineDevices[0].uuid;
return getCliForm().ask({
message: 'Select a device',
type: 'list',
default: defaultUuid,
choices: _.map(onlineDevices, (device) => ({
name: `${device.device_name || 'Untitled'} (${device.uuid.slice(
0,
7,
)})`,
value: device.uuid,
})),
});
});
}
/*
* Given fleetOrDevice, which may be
* - a fleet name
@ -322,25 +311,31 @@ export async function getOnlineTargetDeviceUuid(
}
// Not a device UUID, try application
let application: Application;
try {
logger.logDebug(`Fetching fleet ${fleetOrDevice}`);
const { getApplication } = await import('./sdk');
application = await getApplication(sdk, fleetOrDevice);
} catch (err) {
const { BalenaApplicationNotFound } = await import('balena-errors');
if (instanceOf(err, BalenaApplicationNotFound)) {
throw new ExpectedError(`Fleet or Device not found: ${fleetOrDevice}`);
} else {
throw err;
const application = await (async () => {
try {
logger.logDebug(`Fetching fleet ${fleetOrDevice}`);
const { getApplication } = await import('./sdk');
return await getApplication(sdk, fleetOrDevice, {
$select: ['id', 'slug'],
$expand: {
owns__device: {
$select: ['device_name', 'uuid'],
$filter: { is_online: true },
},
},
});
} catch (err) {
const { BalenaApplicationNotFound } = await import('balena-errors');
if (instanceOf(err, BalenaApplicationNotFound)) {
throw new ExpectedError(`Fleet or Device not found: ${fleetOrDevice}`);
} else {
throw err;
}
}
}
})();
// App found, load its devices
const devices = await sdk.models.device.getAllByApplication(application.id, {
$select: ['device_name', 'uuid'],
$filter: { is_online: true },
});
const devices = application.owns__device;
// Throw if no devices online
if (_.isEmpty(devices)) {

View File

@ -16,10 +16,10 @@ limitations under the License.
import type { OptionalNavigationResource } from 'balena-sdk';
export const getExpanded = <T>(obj: OptionalNavigationResource<T>) =>
export const getExpanded = <T extends {}>(obj: OptionalNavigationResource<T>) =>
(Array.isArray(obj) && obj[0]) || undefined;
export const getExpandedProp = <T, K extends keyof T>(
export const getExpandedProp = <T extends {}, K extends keyof T>(
obj: OptionalNavigationResource<T>,
key: K,
) => (Array.isArray(obj) && obj[0] && obj[0][key]) || undefined;

View File

@ -176,7 +176,7 @@ async function selectLocalBalenaOsDevice(timeout = 4000): Promise<string> {
});
const responsiveDevices: typeof devices = [];
const Docker = await import('docker-toolbelt');
const Docker = await import('dockerode');
await Promise.all(
devices.map(async function (device) {
const address = device?.address;
@ -427,7 +427,7 @@ async function generateApplicationConfig(
) {
const { generateApplicationConfig: configGen } = await import('./config');
const manifest = await sdk.models.device.getManifestBySlug(
const manifest = await sdk.models.config.getDeviceTypeManifestBySlug(
app.is_for__device_type[0].slug,
);
const opts =

View File

@ -20,8 +20,18 @@ import type {
BalenaSDK,
Organization,
PineOptions,
PineTypedResult,
} from 'balena-sdk';
export async function getApplication(
sdk: BalenaSDK,
nameOrSlugOrId: string | number,
): Promise<Application>;
export async function getApplication<TP extends PineOptions<Application>>(
sdk: BalenaSDK,
nameOrSlugOrId: string | number,
options?: TP,
): Promise<PineTypedResult<Application, TP>>;
/**
* Get a fleet object, disambiguating the fleet identifier which may be a
* a fleet slug or name.
@ -29,21 +39,24 @@ import type {
*/
export async function getApplication(
sdk: BalenaSDK,
nameOrSlug: string,
nameOrSlugOrId: string | number,
options?: PineOptions<Application>,
): Promise<Application> {
const { looksLikeFleetSlug } = await import('./validation');
if (!looksLikeFleetSlug(nameOrSlug)) {
if (
typeof nameOrSlugOrId === 'string' &&
!looksLikeFleetSlug(nameOrSlugOrId)
) {
// Not a slug: must be an app name.
// TODO: revisit this logic when we add support for fleet UUIDs.
return await sdk.models.application.getAppByName(
nameOrSlug,
nameOrSlugOrId,
options,
'directly_accessible',
);
}
return await sdk.models.application.getDirectlyAccessible(
nameOrSlug,
nameOrSlugOrId,
options,
);
}
@ -63,31 +76,44 @@ export async function getFleetSlug(
if (!looksLikeFleetSlug(nameOrSlug)) {
// Not a slug: must be an app name.
// TODO: revisit this logic when we add support for fleet UUIDs.
return (await getApplication(sdk, nameOrSlug)).slug;
return (await getApplication(sdk, nameOrSlug, { $select: 'slug' })).slug;
}
return nameOrSlug.toLowerCase();
}
export async function getOwnOrganizations(
sdk: BalenaSDK,
): Promise<Organization[]>;
export async function getOwnOrganizations<TP extends PineOptions<Organization>>(
sdk: BalenaSDK,
options: TP,
): Promise<Array<PineTypedResult<Organization, TP>>>;
/**
* Wraps the sdk organization.getAll method,
* restricting to those orgs user is a member of
*/
export async function getOwnOrganizations(
sdk: BalenaSDK,
options?: PineOptions<Organization>,
): Promise<Organization[]> {
return await sdk.models.organization.getAll({
$filter: {
organization_membership: {
$any: {
$alias: 'orm',
$expr: {
orm: {
user: await sdk.auth.getUserId(),
return await sdk.models.organization.getAll(
sdk.utils.mergePineOptions(
{
$filter: {
organization_membership: {
$any: {
$alias: 'orm',
$expr: {
orm: {
user: await sdk.auth.getUserId(),
},
},
},
},
},
$orderby: 'name asc',
},
},
$orderby: 'name asc',
});
options,
),
);
}

34699
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "balena-cli",
"version": "15.1.2",
"version": "16.6.3",
"description": "The official balena Command Line Interface",
"main": "./build/app.js",
"homepage": "https://github.com/balena-io/balena-cli",
@ -89,7 +89,7 @@
"author": "Balena Inc. (https://balena.io/)",
"license": "Apache-2.0",
"engines": {
"node": ">=14 <16"
"node": ">=16 <18"
},
"husky": {
"hooks": {
@ -109,11 +109,13 @@
"sign": "Developer ID Installer: Balena Ltd (66H43P8FRG)"
},
"plugins": [
"@oclif/plugin-help"
"@oclif/plugin-help",
"@oclif/plugin-update"
]
},
"devDependencies": {
"@balena/lint": "^6.2.2",
"@electron/notarize": "^2.0.0",
"@oclif/config": "^1.18.2",
"@oclif/parser": "^3.8.6",
"@octokit/plugin-throttling": "^3.5.1",
@ -132,6 +134,7 @@
"@types/global-agent": "^2.1.1",
"@types/global-tunnel-ng": "^2.1.1",
"@types/http-proxy": "^1.17.8",
"@types/inquirer": "^7.3.3",
"@types/intercept-stdout": "^0.1.0",
"@types/is-root": "^2.1.2",
"@types/js-yaml": "^4.0.5",
@ -145,7 +148,7 @@
"@types/ndjson": "^2.0.1",
"@types/net-keepalive": "^0.4.1",
"@types/nock": "^11.1.0",
"@types/node": "^14.18.36",
"@types/node": "^16.18.25",
"@types/node-cleanup": "^2.1.2",
"@types/parse-link-header": "^1.0.1",
"@types/prettyjson": "^0.0.30",
@ -169,7 +172,6 @@
"cross-env": "^7.0.3",
"deep-object-diff": "^1.1.0",
"diff": "^5.0.0",
"electron-notarize": "^1.0.0",
"ent": "^2.2.0",
"filehound": "^1.17.5",
"fs-extra": "^9.1.0",
@ -183,50 +185,48 @@
"mock-require": "^3.0.3",
"nock": "^13.2.1",
"parse-link-header": "^2.0.0",
"pkg": "^5.5.1",
"pkg": "^5.8.1",
"publish-release": "^1.6.1",
"rewire": "^5.0.0",
"simple-git": "^3.14.1",
"sinon": "^11.1.2",
"ts-node": "^10.4.0",
"typescript": "^5.0.2"
"typescript": "^5.1.3"
},
"dependencies": {
"@balena/compose": "^2.2.1",
"@balena/dockerignore": "^1.0.2",
"@balena/es-version": "^1.0.1",
"@oclif/command": "^1.8.16",
"@oclif/plugin-update": "^3.1.15",
"@resin.io/valid-email": "^0.1.0",
"@sentry/node": "^6.16.1",
"@types/fast-levenshtein": "0.0.1",
"@types/update-notifier": "^4.1.1",
"JSONStream": "^1.0.3",
"balena-config-json": "^4.2.0",
"balena-device-init": "^6.0.0",
"balena-errors": "^4.7.1",
"balena-errors": "^4.7.3",
"balena-image-fs": "^7.0.6",
"balena-image-manager": "^8.0.0",
"balena-preload": "^13.0.0",
"balena-sdk": "^16.28.0",
"balena-image-manager": "^9.0.0",
"balena-preload": "^14.0.0",
"balena-sdk": "^17.0.0",
"balena-semver": "^2.3.0",
"balena-settings-client": "^4.0.7",
"balena-settings-client": "^5.0.2",
"balena-settings-storage": "^7.0.0",
"bluebird": "^3.7.2",
"body-parser": "^1.19.1",
"chalk": "^3.0.0",
"chokidar": "^3.5.2",
"cli-truncate": "^2.1.0",
"cli-ux": "^5.5.1",
"color-hash": "^1.1.1",
"columnify": "^1.5.2",
"common-tags": "^1.7.2",
"denymount": "^2.3.0",
"docker-modem": "3.0.0",
"docker-progress": "^5.1.3",
"docker-toolbelt": "^3.3.10",
"dockerode": "^3.3.1",
"dockerode": "3.3.3",
"ejs": "^3.1.6",
"etcher-sdk": "^6.2.1",
"etcher-sdk": "^8.5.3",
"event-stream": "3.3.4",
"express": "^4.17.2",
"fast-boot2": "^1.1.0",
@ -242,6 +242,7 @@
"is-elevated": "^3.0.0",
"is-root": "^2.1.0",
"js-yaml": "^4.1.0",
"JSONStream": "^1.0.3",
"klaw": "^3.0.0",
"livepush": "^3.5.1",
"lodash": "^4.17.21",
@ -252,7 +253,7 @@
"net-keepalive": "^3.0.0",
"node-cleanup": "^2.1.2",
"node-unzip-2": "^0.2.8",
"oclif": "^1.18.4",
"oclif": "^3.9.1",
"open": "^7.1.0",
"patch-package": "^6.4.7",
"prettyjson": "^1.2.5",
@ -260,8 +261,8 @@
"reconfix": "^1.0.0-v0-1-0-fork-46760acff4d165f5238bfac5e464256ef1944476",
"request": "^2.88.2",
"resin-cli-form": "^2.0.2",
"resin-cli-visuals": "^1.8.0",
"resin-discoverable-services": "^2.0.3",
"resin-cli-visuals": "^1.8.3",
"resin-discoverable-services": "^2.0.4",
"resin-doodles": "^0.2.0",
"resin-stream-logger": "^0.1.2",
"rimraf": "^3.0.2",
@ -284,6 +285,6 @@
"windosu": "^0.3.0"
},
"versionist": {
"publishedAt": "2023-03-27T15:14:46.144Z"
"publishedAt": "2023-06-30T17:07:33.152Z"
}
}

View File

@ -0,0 +1,43 @@
diff --git a/node_modules/@oclif/core/lib/help/command.js b/node_modules/@oclif/core/lib/help/command.js
index 6de139b..b625b46 100644
--- a/node_modules/@oclif/core/lib/help/command.js
+++ b/node_modules/@oclif/core/lib/help/command.js
@@ -206,7 +206,7 @@ class CommandHelp extends formatter_1.HelpFormatter {
if (args.filter(a => a.description).length === 0)
return;
return args.map(a => {
- const name = a.name.toUpperCase();
+ const name = a.required ? `<${a.name}>` : `[${a.name}]`;
let description = a.description || '';
if (a.default)
description = `[default: ${a.default}] ${description}`;
@@ -244,9 +244,7 @@ class CommandHelp extends formatter_1.HelpFormatter {
}
if (flag.multiple)
value += '...';
- if (!value.includes('|'))
- value = underline(value);
- label += `=${value}`;
+ label += ` <${value}>`
}
return label;
}
diff --git a/node_modules/@oclif/core/lib/help/index.js b/node_modules/@oclif/core/lib/help/index.js
index f584e65..394ec36 100644
--- a/node_modules/@oclif/core/lib/help/index.js
+++ b/node_modules/@oclif/core/lib/help/index.js
@@ -132,11 +132,12 @@ class Help extends HelpBase {
}
this.log(this.formatCommand(command));
this.log('');
- if (subTopics.length > 0) {
+ const SUPPRESS_SUBTOPICS = true;
+ if (subTopics.length > 0 && !SUPPRESS_SUBTOPICS) {
this.log(this.formatTopics(subTopics));
this.log('');
}
- if (subCommands.length > 0) {
+ if (subCommands.length > 0 && !SUPPRESS_SUBTOPICS) {
const aliases = [];
const uniqueSubCommands = subCommands.filter(p => {
aliases.push(...p.aliases);

View File

@ -1,24 +1,24 @@
diff --git a/node_modules/@oclif/parser/lib/errors.js b/node_modules/@oclif/parser/lib/errors.js
index 0c93a81..95d06be 100644
index 39936e3..23e3925 100644
--- a/node_modules/@oclif/parser/lib/errors.js
+++ b/node_modules/@oclif/parser/lib/errors.js
@@ -13,7 +13,8 @@ const m = deps_1.default()
@@ -14,7 +14,8 @@ const m = deps_1.default()
.add('list', () => require('./list'));
class CLIParseError extends errors_1.CLIError {
constructor(options) {
- options.message += '\nSee more help with --help';
+ const help = options.command ? `\`${options.command} --help\`` : '--help';
+ options.message += `\nSee more help with ${help}`;
+ const help = options.command ? `\`${options.command} --help\`` : '--help';
+ options.message += `\nSee more help with ${help}`;
super(options.message);
this.parse = options.parse;
}
@@ -34,22 +35,24 @@ class InvalidArgsSpecError extends CLIParseError {
@@ -35,22 +36,24 @@ class InvalidArgsSpecError extends CLIParseError {
exports.InvalidArgsSpecError = InvalidArgsSpecError;
class RequiredArgsError extends CLIParseError {
constructor({ args, parse }) {
- let message = `Missing ${args.length} required arg${args.length === 1 ? '' : 's'}`;
+ const command = 'balena ' + parse.input.context.id.replace(/:/g, ' ');
+ let message = `Missing ${args.length} required argument${args.length === 1 ? '' : 's'}`;
+ const command = 'balena ' + parse.input.context.id.replace(/:/g, ' ');
+ let message = `Missing ${args.length} required argument${args.length === 1 ? '' : 's'}`;
const namedArgs = args.filter(a => a.name);
if (namedArgs.length > 0) {
const list = m.list.renderList(namedArgs.map(a => [a.name, a.description]));
@ -32,7 +32,7 @@ index 0c93a81..95d06be 100644
exports.RequiredArgsError = RequiredArgsError;
class RequiredFlagError extends CLIParseError {
constructor({ flag, parse }) {
+ const command = 'balena ' + parse.input.context.id.replace(/:/g, ' ');
+ const command = 'balena ' + parse.input.context.id.replace(/:/g, ' ');
const usage = m.list.renderList(m.help.flagUsages([flag], { displayRequired: false }));
const message = `Missing required flag:\n${usage}`;
- super({ parse, message });
@ -41,15 +41,15 @@ index 0c93a81..95d06be 100644
}
}
diff --git a/node_modules/@oclif/parser/lib/list.js b/node_modules/@oclif/parser/lib/list.js
index 3907cc0..b689ca1 100644
index 9d020b7..6ea9eb9 100644
--- a/node_modules/@oclif/parser/lib/list.js
+++ b/node_modules/@oclif/parser/lib/list.js
@@ -21,7 +21,7 @@ function renderList(items) {
@@ -22,7 +22,7 @@ function renderList(items) {
}
left = left.padEnd(maxLength);
right = linewrap(maxLength + 2, right);
- return `${left} ${right}`;
+ return `${left} : ${right}`;
+ return `${left} : ${right}`;
});
return lines.join('\n');
}

View File

@ -1,43 +0,0 @@
diff --git a/node_modules/@oclif/plugin-help/lib/command.js b/node_modules/@oclif/plugin-help/lib/command.js
index b3b9010..788e5c6 100644
--- a/node_modules/@oclif/plugin-help/lib/command.js
+++ b/node_modules/@oclif/plugin-help/lib/command.js
@@ -88,7 +88,7 @@ class CommandHelp {
return;
const body = list_1.renderList(args.map(a => {
var _a;
- const name = a.name.toUpperCase();
+ const name = a.required ? `<${a.name}>` : `[${a.name}]`;
let description = a.description || '';
// `a.default` is actually not always a string (typing bug), hence `toString()`
if (a.default || ((_a = a.default) === null || _a === void 0 ? void 0 : _a.toString()) === '0')
@@ -133,9 +133,7 @@ class CommandHelp {
if (!flag.helpValue && flag.options) {
value = flag.options.join('|');
}
- if (!value.includes('|'))
- value = underline(value);
- left += `=${value}`;
+ left += ` <${value}>`;
}
let right = flag.description || '';
// `flag.default` is not always a string (typing bug), hence `toString()`
diff --git a/node_modules/@oclif/plugin-help/lib/index.js b/node_modules/@oclif/plugin-help/lib/index.js
index 04d7861..c2fb591 100644
--- a/node_modules/@oclif/plugin-help/lib/index.js
+++ b/node_modules/@oclif/plugin-help/lib/index.js
@@ -98,11 +98,12 @@ class Help extends HelpBase {
console.log(title + '\n');
console.log(this.formatCommand(command));
console.log('');
- if (subTopics.length > 0) {
+ const SUPPRESS_SUBTOPICS = true;
+ if (subTopics.length > 0 && !SUPPRESS_SUBTOPICS) {
console.log(this.formatTopics(subTopics));
console.log('');
}
- if (subCommands.length > 0) {
+ if (subCommands.length > 0 && !SUPPRESS_SUBTOPICS) {
console.log(this.formatCommands(subCommands));
console.log('');
}

View File

@ -1,278 +0,0 @@
diff --git a/node_modules/oclif/lib/commands/pack/macos.js b/node_modules/oclif/lib/commands/pack/macos.js
index 924f092..a69e60b 100644
--- a/node_modules/oclif/lib/commands/pack/macos.js
+++ b/node_modules/oclif/lib/commands/pack/macos.js
@@ -133,6 +133,7 @@ class PackMacos extends command_1.Command {
if (process.env.OSX_KEYCHAIN)
args.push('--keychain', process.env.OSX_KEYCHAIN);
args.push(dist);
+ console.error(`[debug] oclif pkgbuild "${args.join('" "')}"`);
await qq.x('pkgbuild', args);
}
}
diff --git a/node_modules/oclif/lib/commands/pack/win.js b/node_modules/oclif/lib/commands/pack/win.js
index bf4657e..fd58c7d 100644
--- a/node_modules/oclif/lib/commands/pack/win.js
+++ b/node_modules/oclif/lib/commands/pack/win.js
@@ -52,6 +52,13 @@ VIAddVersionKey /LANG=\${LANG_ENGLISH} "ProductVersion" "\${VERSION}.0"
InstallDir "\$PROGRAMFILES${arch === 'x64' ? '64' : ''}\\${config.dirname}"
Section "${config.name} CLI \${VERSION}"
+ ; First remove any old client files.
+ ; (Remnants of old versions were causing CLI errors)
+ ; Initially tried running the Uninstall.exe, but was
+ ; unable to make script wait for completion (despite using _?)
+ DetailPrint "Removing files from previous version."
+ RMDir /r "$INSTDIR\\client"
+
SetOutPath $INSTDIR
File /r bin
File /r client
@@ -61,6 +68,8 @@ Section "${config.name} CLI \${VERSION}"
WriteUninstaller "$INSTDIR\\Uninstall.exe"
WriteRegStr HKLM "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${config.dirname}" \\
"DisplayName" "${config.name}"
+ WriteRegStr HKLM "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${config.dirname}" \\
+ "DisplayVersion" "\${VERSION}"
WriteRegStr HKLM "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${config.dirname}" \\
"UninstallString" "$\\"$INSTDIR\\uninstall.exe$\\""
WriteRegStr HKLM "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${config.dirname}" \\
@@ -193,7 +202,8 @@ class PackWin extends command_1.Command {
async run() {
await this.checkForNSIS();
const { flags } = this.parse(PackWin);
- const buildConfig = await Tarballs.buildConfig(flags.root);
+ const $targets = flags.targets ? flags.targets.split(',') : undefined;
+ const buildConfig = await Tarballs.buildConfig(flags.root, { targets: $targets });
const { config, version, gitSha, targets, tmp } = buildConfig;
await Tarballs.build(buildConfig, { platform: 'win32', pack: false });
const arches = targets.filter(t => t.platform === 'win32').map(t => t.arch);
@@ -208,7 +218,8 @@ class PackWin extends command_1.Command {
// eslint-disable-next-line no-await-in-loop
await qq.mv(buildConfig.workspace({ platform: 'win32', arch }), [installerBase, 'client']);
// eslint-disable-next-line no-await-in-loop
- await qq.x(`makensis ${installerBase}/${config.bin}.nsi | grep -v "\\[compress\\]" | grep -v "^File: Descending to"`);
+ const { msysExec, toMsysPath } = require("../../util");
+ await msysExec(`makensis ${toMsysPath(installerBase)}/${config.bin}.nsi | grep -v "\\[compress\\]" | grep -v "^File: Descending to"`);
const templateKey = upload_util_1.templateShortKey('win32', { bin: config.bin, version: version, sha: gitSha, arch });
const o = buildConfig.dist(`win32/${templateKey}`);
// eslint-disable-next-line no-await-in-loop
@@ -255,4 +266,5 @@ PackWin.hidden = true;
PackWin.description = 'create windows installer from oclif CLI';
PackWin.flags = {
root: command_1.flags.string({ char: 'r', description: 'path to oclif CLI root', default: '.', required: true }),
+ targets: command_1.flags.string({char: 't', description: 'comma-separated targets to pack (e.g.: win32-x86,win32-x64)'}),
};
diff --git a/node_modules/oclif/lib/tarballs/build.js b/node_modules/oclif/lib/tarballs/build.js
index d3e8e89..a5d29e2 100644
--- a/node_modules/oclif/lib/tarballs/build.js
+++ b/node_modules/oclif/lib/tarballs/build.js
@@ -18,8 +18,9 @@ const pack = async (from, to) => {
qq.cd(prevCwd);
};
async function build(c, options = {}) {
- const { xz, config, version, s3Config, gitSha, nodeVersion, targets, updateConfig } = c;
+ const { xz, config, version, s3Config, gitSha, nodeVersion, targets, updateConfig, tmp } = c;
const prevCwd = qq.cwd();
+ console.error(`[debug] oclif cwd="${prevCwd}"\n c.root="${c.root}" c.workspace()="${c.workspace()}"`);
const packCLI = async () => {
const stdout = await qq.x.stdout('npm', ['pack', '--unsafe-perm'], { cwd: c.root });
return path.join(c.root, stdout.split('\n').pop());
@@ -30,11 +31,19 @@ async function build(c, options = {}) {
tarball = path.basename(tarball);
tarball = qq.join([c.workspace(), tarball]);
qq.cd(c.workspace());
- await qq.x(`tar -xzf ${tarball}`);
+ const { msysExec, toMsysPath } = require("../util");
+ await msysExec(`tar -xzf ${toMsysPath(tarball)}`);
// eslint-disable-next-line no-await-in-loop
for (const f of await qq.ls('package', { fullpath: true }))
await qq.mv(f, '.');
await qq.rm('package', tarball, 'bin/run.cmd');
+ // rename the original balena-cli ./bin/balena entry point for oclif compatibility
+ await qq.mv('bin/balena', 'bin/run');
+ // The oclif installers are a production installation, while the source
+ // `bin` folder may contain a `.fast-boot.json` file of a dev installation.
+ // This has previously led to issues preventing the CLI from starting, so
+ // delete `.fast-boot.json` (if any) from the destination folder.
+ await qq.rm('bin/.fast-boot.json');
};
const updatePJSON = async () => {
qq.cd(c.workspace());
@@ -46,21 +55,21 @@ async function build(c, options = {}) {
await qq.writeJSON('package.json', pjson);
};
const addDependencies = async () => {
- qq.cd(c.workspace());
- const yarnRoot = findYarnWorkspaceRoot(c.root) || c.root;
- const yarn = await qq.exists([yarnRoot, 'yarn.lock']);
- if (yarn) {
- await qq.cp([yarnRoot, 'yarn.lock'], '.');
- await qq.x('yarn --no-progress --production --non-interactive');
- }
- else {
- let lockpath = qq.join(c.root, 'package-lock.json');
- if (!await qq.exists(lockpath)) {
- lockpath = qq.join(c.root, 'npm-shrinkwrap.json');
- }
- await qq.cp(lockpath, '.');
- await qq.x('npm install --production');
+ const ws = c.workspace();
+ qq.cd(ws);
+ console.error(`[debug] oclif copying node_modules to "${ws}"`)
+ const source = path.join(c.root, 'node_modules');
+ if (process.platform === 'win32') {
+ // xcopy is much faster than `qq.cp(source, ws)`
+ await qq.x(`xcopy "${source}" "${ws}\\node_modules" /S /E /B /I /K /Q /Y`);
+ } else {
+ // use the shell's `cp` on macOS in order to preserve extended
+ // file attributes containing `codesign` digital signatures
+ await qq.x(`cp -pR "${source}" "${ws}"`);
}
+ console.error(`[debug] oclif running "npm prune --production" in "${ws}"`);
+ await qq.x('npm prune --production');
+ console.error(`[debug] oclif done`);
};
const pretarball = async () => {
qq.cd(c.workspace());
@@ -99,7 +108,8 @@ async function build(c, options = {}) {
output: path.join(workspace, 'bin', 'node'),
platform: target.platform,
arch: target.arch,
- tmp: qq.join(config.root, 'tmp'),
+ tmp,
+ projectRootPath: c.root,
});
if (options.pack === false)
return;
diff --git a/node_modules/oclif/lib/tarballs/config.js b/node_modules/oclif/lib/tarballs/config.js
index 0dc3cd7..1336219 100644
--- a/node_modules/oclif/lib/tarballs/config.js
+++ b/node_modules/oclif/lib/tarballs/config.js
@@ -18,7 +18,10 @@ function gitSha(cwd, options = {}) {
}
exports.gitSha = gitSha;
async function Tmp(config) {
- const tmp = path.join(config.root, 'tmp');
+ const tmp = process.env.BUILD_TMP
+ ? path.join(process.env.BUILD_TMP, 'oclif')
+ : path.join(config.root, 'tmp');
+ console.error(`[debug] oclif tmp="${tmp}"`);
await qq.mkdirp(tmp);
return tmp;
}
@@ -43,7 +46,7 @@ async function buildConfig(root, options = {}) {
s3Config: updateConfig.s3,
nodeVersion: updateConfig.node.version || process.versions.node,
workspace(target) {
- const base = qq.join(config.root, 'tmp');
+ const base = tmp;
if (target && target.platform)
return qq.join(base, [target.platform, target.arch].join('-'), upload_util_1.templateShortKey('baseDir', { bin: config.bin }));
return qq.join(base, upload_util_1.templateShortKey('baseDir', { bin: config.bin }));
diff --git a/node_modules/oclif/lib/tarballs/node.js b/node_modules/oclif/lib/tarballs/node.js
index fabe5c4..e32dd76 100644
--- a/node_modules/oclif/lib/tarballs/node.js
+++ b/node_modules/oclif/lib/tarballs/node.js
@@ -4,9 +4,10 @@ const errors_1 = require("@oclif/errors");
const path = require("path");
const qq = require("qqjs");
const log_1 = require("../log");
+const { isMSYS2, msysExec, toMsysPath } = require("../util");
async function checkFor7Zip() {
try {
- await qq.x('7z', { stdio: [0, null, 2] });
+ await msysExec('7z', { stdio: [0, null, 2] });
}
catch (error) {
if (error.code === 127)
@@ -41,7 +42,8 @@ async function fetchNodeBinary({ nodeVersion, output, platform, arch, tmp }) {
const basedir = path.dirname(tarball);
await qq.mkdirp(basedir);
await qq.download(url, tarball);
- await qq.x(`grep ${path.basename(tarball)} ${shasums} | shasum -a 256 -c -`, { cwd: basedir });
+ const shaCmd = isMSYS2 ? 'sha256sum -c -' : 'shasum -a 256 -c -';
+ await msysExec(`grep ${path.basename(tarball)} ${toMsysPath(shasums)} | ${shaCmd}`, { cwd: basedir });
};
const extract = async () => {
log_1.log(`extracting ${nodeBase}`);
@@ -51,7 +53,7 @@ async function fetchNodeBinary({ nodeVersion, output, platform, arch, tmp }) {
await qq.mkdirp(path.dirname(cache));
if (platform === 'win32') {
qq.pushd(nodeTmp);
- await qq.x(`7z x -bd -y ${tarball} > /dev/null`);
+ await msysExec(`7z x -bd -y ${toMsysPath(tarball)} > /dev/null`);
await qq.mv([nodeBase, 'node.exe'], cache);
qq.popd();
}
diff --git a/node_modules/oclif/lib/upload-util.js b/node_modules/oclif/lib/upload-util.js
index 45392cb..3c806c7 100644
--- a/node_modules/oclif/lib/upload-util.js
+++ b/node_modules/oclif/lib/upload-util.js
@@ -28,10 +28,10 @@ function templateShortKey(type, ext, options = { root: '.' }) {
const templates = {
baseDir: '<%- bin %>',
unversioned: '<%- bin %>-<%- platform %>-<%- arch %><%- ext %>',
- versioned: '<%- bin %>-v<%- version %>-<%- sha %>-<%- platform %>-<%- arch %><%- ext %>',
- manifest: '<%- bin %>-v<%- version %>-<%- sha %>-<%- platform %>-<%- arch %>-buildmanifest',
- macos: '<%- bin %>-v<%- version %>-<%- sha %>.pkg',
- win32: '<%- bin %>-v<%- version %>-<%- sha %>-<%- arch %>.exe',
+ versioned: '<%- bin %>-v<%- version %>-<%- platform %>-<%- arch %><%- ext %>',
+ manifest: '<%- bin %>-v<%- version %>-<%- platform %>-<%- arch %>-buildmanifest',
+ macos: '<%- bin %>-v<%- version %>.pkg',
+ win32: '<%- bin %>-v<%- version %>-<%- arch %>.exe',
deb: '<%- bin %>_<%- versionShaRevision %>_<%- arch %>.deb',
};
return _.template(templates[type])(Object.assign({}, options));
diff --git a/node_modules/oclif/lib/util.js b/node_modules/oclif/lib/util.js
index 17748ad..4928fc9 100644
--- a/node_modules/oclif/lib/util.js
+++ b/node_modules/oclif/lib/util.js
@@ -67,3 +67,47 @@ exports.sortVersionsObjectByKeysDesc = (input) => {
}
return result;
};
+
+// OSTYPE is 'msys' for MSYS 1.0 and for MSYS2, or 'cygwin' for Cygwin
+// but note that OSTYPE is not "exported" by default, so run: export OSTYPE=$OSTYPE
+// MSYSTEM is 'MINGW32' for MSYS 1.0, 'MSYS' for MSYS2, and undefined for Cygwin
+const isCygwin = process.env.OSTYPE === 'cygwin';
+const isMinGW = process.env.MSYSTEM && process.env.MSYSTEM.startsWith('MINGW');
+const isMSYS2 = process.env.MSYSTEM && process.env.MSYSTEM.startsWith('MSYS');
+const MSYSSHELLPATH = process.env.MSYSSHELLPATH ||
+ (isMSYS2 ? 'C:\\msys64\\usr\\bin\\bash.exe' :
+ (isMinGW ? 'C:\\MinGW\\msys\\1.0\\bin\\bash.exe' :
+ (isCygwin ? 'C:\\cygwin64\\bin\\bash.exe' : '/bin/sh')));
+
+exports.isCygwin = isCygwin;
+exports.isMinGW = isMinGW;
+exports.isMSYS2 = isMSYS2;
+console.error(`[debug] oclif MSYSSHELLPATH=${MSYSSHELLPATH} MSYSTEM=${process.env.MSYSTEM} OSTYPE=${process.env.OSTYPE} isMSYS2=${isMSYS2} isMingGW=${isMinGW} isCygwin=${isCygwin}`);
+
+const qq = require("qqjs");
+
+/* Convert a Windows path like 'C:\tmp' to a MSYS path like '/c/tmp' */
+function toMsysPath(windowsPath) {
+ // 'c:\myfolder' -> '/c/myfolder' or '/cygdrive/c/myfolder'
+ let msysPath = windowsPath.replace(/\\/g, '/');
+ if (isMSYS2 || isMinGW) {
+ msysPath = msysPath.replace(/^([a-zA-Z]):/, '/$1');
+ } else if (isCygwin) {
+ msysPath = msysPath.replace(/^([a-zA-Z]):/, '/cygdrive/$1');
+ }
+ console.error(`[debug] oclif toMsysPath before="${windowsPath}" after="${msysPath}"`);
+ return msysPath;
+}
+exports.toMsysPath = toMsysPath;
+
+/* Like qqjs qq.x(), but using MSYS bash on Windows instead of cmd.exe */
+async function msysExec(cmd, options = {}) {
+ if (process.platform !== 'win32') {
+ return qq.x(cmd, options);
+ }
+ const sh = MSYSSHELLPATH;
+ const args = ['-c', cmd];
+ console.error(`[debug] oclif msysExec sh="${sh}" args=${JSON.stringify(args)} options=${JSON.stringify(options)}`);
+ return qq.x(sh, args, options);
+}
+exports.msysExec = msysExec;

View File

@ -0,0 +1,294 @@
diff --git a/node_modules/oclif/lib/commands/pack/macos.js b/node_modules/oclif/lib/commands/pack/macos.js
index d06d0b3..4bdcd34 100644
--- a/node_modules/oclif/lib/commands/pack/macos.js
+++ b/node_modules/oclif/lib/commands/pack/macos.js
@@ -177,6 +177,7 @@ class PackMacos extends core_1.Command {
if (process.env.OSX_KEYCHAIN)
args.push('--keychain', process.env.OSX_KEYCHAIN);
args.push(dist);
+ console.error(`[debug] oclif pkgbuild "${args.join('" "')}"`);
await exec(`pkgbuild ${args.join(' ')}`);
};
const arches = _.uniq(buildConfig.targets
diff --git a/node_modules/oclif/lib/commands/pack/win.js b/node_modules/oclif/lib/commands/pack/win.js
index 360c34b..4e1047b 100644
--- a/node_modules/oclif/lib/commands/pack/win.js
+++ b/node_modules/oclif/lib/commands/pack/win.js
@@ -59,6 +59,13 @@ InstallDir "\$PROGRAMFILES${arch === 'x64' ? '64' : ''}\\${config.dirname}"
${customization}
Section "${config.name} CLI \${VERSION}"
+ ; First remove any old client files.
+ ; (Remnants of old versions were causing CLI errors)
+ ; Initially tried running the Uninstall.exe, but was
+ ; unable to make script wait for completion (despite using _?)
+ DetailPrint "Removing files from previous version."
+ RMDir /r "$INSTDIR\\client"
+
SetOutPath $INSTDIR
File /r bin
File /r client
@@ -203,7 +210,8 @@ class PackWin extends core_1.Command {
async run() {
await this.checkForNSIS();
const { flags } = await this.parse(PackWin);
- const buildConfig = await Tarballs.buildConfig(flags.root);
+ const $targets = flags.targets ? flags.targets.split(',') : undefined;
+ const buildConfig = await Tarballs.buildConfig(flags.root, { targets: $targets });
const { config } = buildConfig;
await Tarballs.build(buildConfig, { platform: 'win32', pack: false, tarball: flags.tarball, parallel: true });
const arches = buildConfig.targets.filter(t => t.platform === 'win32').map(t => t.arch);
@@ -225,7 +233,8 @@ class PackWin extends core_1.Command {
fs.writeFile(path.join(installerBase, 'bin', `${flags['additional-cli']}`), scripts.sh({ bin: flags['additional-cli'] })),
] : []));
await fs.move(buildConfig.workspace({ platform: 'win32', arch }), path.join(installerBase, 'client'));
- await exec(`makensis ${installerBase}/${config.bin}.nsi | grep -v "\\[compress\\]" | grep -v "^File: Descending to"`);
+ const { msysExec, toMsysPath } = require("../../util");
+ await msysExec(`makensis ${toMsysPath(installerBase)}/${config.bin}.nsi | grep -v "\\[compress\\]" | grep -v "^File: Descending to"`);
const templateKey = (0, upload_util_1.templateShortKey)('win32', { bin: config.bin, version: config.version, sha: buildConfig.gitSha, arch });
const o = buildConfig.dist(`win32/${templateKey}`);
await fs.move(path.join(installerBase, 'installer.exe'), o);
@@ -263,6 +272,10 @@ PackWin.flags = {
default: '.',
required: true,
}),
+ targets: core_1.Flags.string({
+ char: 't',
+ description: 'comma-separated targets to pack (e.g.: win32-x86,win32-x64)'
+ }),
'additional-cli': core_1.Flags.string({
description: `an Oclif CLI other than the one listed in config.bin that should be made available to the user
the CLI should already exist in a directory named after the CLI that is the root of the tarball produced by "oclif pack:tarballs"`,
diff --git a/node_modules/oclif/lib/tarballs/build.js b/node_modules/oclif/lib/tarballs/build.js
index 384ea4b..40998f6 100644
--- a/node_modules/oclif/lib/tarballs/build.js
+++ b/node_modules/oclif/lib/tarballs/build.js
@@ -21,7 +21,8 @@ const pack = async (from, to) => {
await exec(`tar cfJ ${to} ${(path.basename(from))}`, { cwd }));
};
async function build(c, options = {}) {
- const { xz, config } = c;
+ const { xz, config, tmp } = c;
+ console.error(`[debug] oclif cwd="${prevCwd}"\n c.root="${c.root}" c.workspace()="${c.workspace()}"`);
const packCLI = async () => {
const { stdout } = await exec('npm pack --unsafe-perm', { cwd: c.root });
return path.join(c.root, stdout.trim().split('\n').pop());
@@ -30,7 +31,8 @@ async function build(c, options = {}) {
await fs.emptyDir(c.workspace());
const tarballNewLocation = path.join(c.workspace(), path.basename(tarball));
await fs.move(tarball, tarballNewLocation);
- await exec(`tar -xzf "${tarballNewLocation}"`, { cwd: c.workspace() });
+ const { msysExec, toMsysPath } = require("../util");
+ await msysExec(`tar -xzf ${toMsysPath(tarballNewLocation)}`);
await Promise.all((await fs.promises.readdir(path.join(c.workspace(), 'package'), { withFileTypes: true }))
.map(i => fs.move(path.join(c.workspace(), 'package', i.name), path.join(c.workspace(), i.name))));
await Promise.all([
@@ -38,6 +40,13 @@ async function build(c, options = {}) {
fs.promises.rm(path.join(c.workspace(), path.basename(tarball)), { recursive: true }),
fs.remove(path.join(c.workspace(), 'bin', 'run.cmd')),
]);
+ // rename the original balena-cli ./bin/balena entry point for oclif compatibility
+ await fs.move('bin/balena', 'bin/run');
+ // The oclif installers are a production installation, while the source
+ // `bin` folder may contain a `.fast-boot.json` file of a dev installation.
+ // This has previously led to issues preventing the CLI from starting, so
+ // delete `.fast-boot.json` (if any) from the destination folder.
+ await fs.promises.rm('bin/.fast-boot.json');
};
const updatePJSON = async () => {
const pjsonPath = path.join(c.workspace(), 'package.json');
@@ -49,34 +58,19 @@ async function build(c, options = {}) {
await fs.writeJSON(pjsonPath, pjson, { spaces: 2 });
};
const addDependencies = async () => {
- const yarnRoot = findYarnWorkspaceRoot(c.root) || c.root;
- if (fs.existsSync(path.join(yarnRoot, 'yarn.lock'))) {
- await fs.copy(path.join(yarnRoot, 'yarn.lock'), path.join(c.workspace(), 'yarn.lock'));
- const yarnVersion = (await exec('yarn -v')).stdout.charAt(0);
- if (yarnVersion === '1') {
- await exec('yarn --no-progress --production --non-interactive', { cwd: c.workspace() });
- }
- else if (yarnVersion === '2') {
- throw new Error('Yarn 2 is not supported yet. Try using Yarn 1, or Yarn 3');
- }
- else {
- try {
- await exec('yarn workspaces focus --production', { cwd: c.workspace() });
- }
- catch (error) {
- if (error instanceof Error && error.message.includes('Command not found')) {
- throw new Error('Missing workspace tools. Run `yarn plugin import workspace-tools`.');
- }
- throw error;
- }
- }
- }
- else {
- const lockpath = fs.existsSync(path.join(c.root, 'package-lock.json')) ?
- path.join(c.root, 'package-lock.json') :
- path.join(c.root, 'npm-shrinkwrap.json');
- await fs.copy(lockpath, path.join(c.workspace(), path.basename(lockpath)));
- await exec('npm install --production', { cwd: c.workspace() });
+ const ws = c.workspace();
+ exec(`cd ${ws}`);
+ console.error(`[debug] oclif copying node_modules to "${ws}"`)
+ const source = path.join(c.root, 'node_modules');
+ if (process.platform === 'win32') {
+ await exec(`xcopy "${source}" "${ws}\\node_modules" /S /E /B /I /K /Q /Y`);
+ } else {
+ // use the shell's `cp` on macOS in order to preserve extended
+ // file attributes containing `codesign` digital signatures
+ await exec(`cp -pR "${source}" "${ws}"`);
+ console.error(`[debug] oclif running "npm prune --production" in "${ws}"`);
+ await exec('npm prune --production');
+ console.error(`[debug] oclif done`);
}
};
const pretarball = async () => {
@@ -115,7 +109,8 @@ async function build(c, options = {}) {
output: path.join(workspace, 'bin', 'node'),
platform: target.platform,
arch: target.arch,
- tmp: path.join(config.root, 'tmp'),
+ tmp,
+ projectRootPath: c.root
});
if (options.pack === false)
return;
diff --git a/node_modules/oclif/lib/tarballs/config.js b/node_modules/oclif/lib/tarballs/config.js
index 3334d3f..a7348d9 100644
--- a/node_modules/oclif/lib/tarballs/config.js
+++ b/node_modules/oclif/lib/tarballs/config.js
@@ -24,7 +24,10 @@ async function gitSha(cwd, options = {}) {
}
exports.gitSha = gitSha;
async function Tmp(config) {
- const tmp = path.join(config.root, 'tmp');
+ const tmp = process.env.BUILD_TMP
+ ? path.join(process.env.BUILD_TMP, 'oclif')
+ : path.join(config.root, 'tmp');
+ console.error(`[debug] oclif tmp="${tmp}"`);
await fs.promises.mkdir(tmp, { recursive: true });
return tmp;
}
@@ -61,7 +64,7 @@ async function buildConfig(root, options = {}) {
s3Config: updateConfig.s3,
nodeVersion,
workspace(target) {
- const base = path.join(config.root, 'tmp');
+ const base = tmp;
if (target && target.platform)
return path.join(base, [target.platform, target.arch].join('-'), (0, upload_util_1.templateShortKey)('baseDir', { bin: config.bin }));
return path.join(base, (0, upload_util_1.templateShortKey)('baseDir', { bin: config.bin }));
diff --git a/node_modules/oclif/lib/tarballs/node.js b/node_modules/oclif/lib/tarballs/node.js
index 1a4e09b..bb56759 100644
--- a/node_modules/oclif/lib/tarballs/node.js
+++ b/node_modules/oclif/lib/tarballs/node.js
@@ -11,9 +11,10 @@ const node_util_1 = require("node:util");
const got_1 = require("got");
const pipeline = (0, node_util_1.promisify)(node_stream_1.pipeline);
const exec = (0, node_util_1.promisify)(node_child_process_1.exec);
+const { isMSYS2, msysExec, toMsysPath } = require("../util");
async function checkFor7Zip() {
try {
- await exec('7z');
+ await msysExec('7z', { stdio: [0, null, 2] });
}
catch (error) {
if (error.code === 127)
@@ -51,8 +52,10 @@ async function fetchNodeBinary({ nodeVersion, output, platform, arch, tmp }) {
const basedir = path.dirname(tarball);
await fs.promises.mkdir(basedir, { recursive: true });
await pipeline(got_1.default.stream(url), fs.createWriteStream(tarball));
- if (platform !== 'win32')
- await exec(`grep "${path.basename(tarball)}" "${shasums}" | shasum -a 256 -c -`, { cwd: basedir });
+ if (platform !== 'win32') {
+ const shaCmd = isMSYS2 ? 'sha256sum -c -' : 'shasum -a 256 -c -';
+ await msysExec(`grep ${path.basename(tarball)} ${toMsysPath(shasums)} | ${shaCmd}`, { cwd: basedir });
+ }
};
const extract = async () => {
(0, log_1.log)(`extracting ${nodeBase}`);
@@ -60,7 +63,7 @@ async function fetchNodeBinary({ nodeVersion, output, platform, arch, tmp }) {
await fs.promises.mkdir(nodeTmp, { recursive: true });
await fs.promises.mkdir(path.dirname(cache), { recursive: true });
if (platform === 'win32') {
- await exec(`7z x -bd -y "${tarball}"`, { cwd: nodeTmp });
+ await msysExec(`7z x -bd -y ${toMsysPath(tarball)} > /dev/null`);
await fs.move(path.join(nodeTmp, nodeBase, 'node.exe'), path.join(cache, 'node.exe'));
}
else {
diff --git a/node_modules/oclif/lib/upload-util.js b/node_modules/oclif/lib/upload-util.js
index 0d5f705..46e2445 100644
--- a/node_modules/oclif/lib/upload-util.js
+++ b/node_modules/oclif/lib/upload-util.js
@@ -31,10 +31,10 @@ options = { root: '.' }) {
const templates = {
baseDir: '<%- bin %>',
unversioned: '<%- bin %>-<%- platform %>-<%- arch %><%- ext %>',
- versioned: '<%- bin %>-v<%- version %>-<%- sha %>-<%- platform %>-<%- arch %><%- ext %>',
- manifest: '<%- bin %>-v<%- version %>-<%- sha %>-<%- platform %>-<%- arch %>-buildmanifest',
- macos: '<%- bin %>-v<%- version %>-<%- sha %>-<%- arch %>.pkg',
- win32: '<%- bin %>-v<%- version %>-<%- sha %>-<%- arch %>.exe',
+ versioned: '<%- bin %>-v<%- version %>-<%- platform %>-<%- arch %><%- ext %>',
+ manifest: '<%- bin %>-v<%- version %>-<%- platform %>-<%- arch %>-buildmanifest',
+ macos: '<%- bin %>-v<%- version %>.pkg',
+ win32: '<%- bin %>-v<%- version %>-<%- arch %>.exe',
deb: '<%- bin %>_<%- versionShaRevision %>_<%- arch %>.deb',
};
return _.template(templates[type])(Object.assign({}, options));
diff --git a/node_modules/oclif/lib/util.js b/node_modules/oclif/lib/util.js
index 75bf3c6..ed5a92e 100644
--- a/node_modules/oclif/lib/util.js
+++ b/node_modules/oclif/lib/util.js
@@ -74,6 +74,50 @@ const sortVersionsObjectByKeysDesc = (input) => {
}
return result;
};
+
+// OSTYPE is 'msys' for MSYS 1.0 and for MSYS2, or 'cygwin' for Cygwin
+// but note that OSTYPE is not "exported" by default, so run: export OSTYPE=$OSTYPE
+// MSYSTEM is 'MINGW32' for MSYS 1.0, 'MSYS' for MSYS2, and undefined for Cygwin
+const isCygwin = process.env.OSTYPE === 'cygwin';
+const isMinGW = process.env.MSYSTEM && process.env.MSYSTEM.startsWith('MINGW');
+const isMSYS2 = process.env.MSYSTEM && process.env.MSYSTEM.startsWith('MSYS');
+const MSYSSHELLPATH = process.env.MSYSSHELLPATH ||
+ (isMSYS2 ? 'C:\\msys64\\usr\\bin\\bash.exe' :
+ (isMinGW ? 'C:\\MinGW\\msys\\1.0\\bin\\bash.exe' :
+ (isCygwin ? 'C:\\cygwin64\\bin\\bash.exe' : '/bin/sh')));
+
+exports.isCygwin = isCygwin;
+exports.isMinGW = isMinGW;
+exports.isMSYS2 = isMSYS2;
+console.error(`[debug] oclif MSYSSHELLPATH=${MSYSSHELLPATH} MSYSTEM=${process.env.MSYSTEM} OSTYPE=${process.env.OSTYPE} isMSYS2=${isMSYS2} isMingGW=${isMinGW} isCygwin=${isCygwin}`);
+
+const qq = require("qqjs");
+
+/* Convert a Windows path like 'C:\tmp' to a MSYS path like '/c/tmp' */
+function toMsysPath(windowsPath) {
+ // 'c:\myfolder' -> '/c/myfolder' or '/cygdrive/c/myfolder'
+ let msysPath = windowsPath.replace(/\\/g, '/');
+ if (isMSYS2 || isMinGW) {
+ msysPath = msysPath.replace(/^([a-zA-Z]):/, '/$1');
+ } else if (isCygwin) {
+ msysPath = msysPath.replace(/^([a-zA-Z]):/, '/cygdrive/$1');
+ }
+ console.error(`[debug] oclif toMsysPath before="${windowsPath}" after="${msysPath}"`);
+ return msysPath;
+}
+exports.toMsysPath = toMsysPath;
+
+/* Like qqjs qq.x(), but using MSYS bash on Windows instead of cmd.exe */
+async function msysExec(cmd, options = {}) {
+ if (process.platform !== 'win32') {
+ return qq.x(cmd, options);
+ }
+ const sh = MSYSSHELLPATH;
+ const args = ['-c', cmd];
+ console.error(`[debug] oclif msysExec sh="${sh}" args=${JSON.stringify(args)} options=${JSON.stringify(options)}`);
+ return qq.x(sh, args, options);
+}
+exports.msysExec = msysExec;
exports.sortVersionsObjectByKeysDesc = sortVersionsObjectByKeysDesc;
const homeRegexp = new RegExp(`\\B${os.homedir().replace('/', '\\/')}`, 'g');
const curRegexp = new RegExp(`\\B${process.cwd()}`, 'g');

View File

@ -1,15 +1,16 @@
diff --git a/node_modules/open/index.js b/node_modules/open/index.js
index 3bf5373..e042b64 100644
index 13147d0..ff161dd 100644
--- a/node_modules/open/index.js
+++ b/node_modules/open/index.js
@@ -11,7 +11,9 @@ const pAccess = promisify(fs.access);
const pExecFile = promisify(childProcess.execFile);
@@ -10,7 +10,10 @@ const pAccess = promisify(fs.access);
const pReadFile = promisify(fs.readFile);
// Path to included `xdg-open`.
-const localXdgOpenPath = path.join(__dirname, 'xdg-open');
+const localXdgOpenPath = process.pkg
+ ? path.join(path.dirname(process.execPath), 'xdg-open')
+ : path.join(__dirname, 'xdg-open');
+
// Convert a path from WSL format to Windows format:
// `/mnt/c/Program Files/Example/MyApp.exe` → `C:\Program Files\Example\MyApp.exe`
/**
Get the mount point for fixed drives in WSL.

View File

@ -1,8 +1,8 @@
diff --git a/node_modules/node-gyp-build/index.js b/node_modules/node-gyp-build/index.js
index b5096ed..7cd451a 100644
--- a/node_modules/node-gyp-build/index.js
+++ b/node_modules/node-gyp-build/index.js
@@ -29,6 +29,9 @@ load.path = function (dir) {
diff --git a/node_modules/node-gyp-build/node-gyp-build.js b/node_modules/node-gyp-build/node-gyp-build.js
index 61b398e..3cc3be8 100644
--- a/node_modules/node-gyp-build/node-gyp-build.js
+++ b/node_modules/node-gyp-build/node-gyp-build.js
@@ -30,6 +30,9 @@ load.resolve = load.path = function (dir) {
if (process.env[name + '_PREBUILD']) dir = process.env[name + '_PREBUILD']
} catch (err) {}

View File

@ -138,6 +138,7 @@ describe('balena envs', function () {
});
it('should successfully list env variables for a test device', async () => {
api.expectGetDevice({ shortUUID, fullUUID });
api.expectGetDevice({ fullUUID });
api.expectGetDeviceEnvVars();
api.expectGetApplication();
@ -145,20 +146,19 @@ describe('balena envs', function () {
api.expectGetAppServiceVars();
api.expectGetDeviceServiceVars();
const uuid = shortUUID;
const result = await runCommand(`envs -d ${uuid}`);
const result = await runCommand(`envs -d ${shortUUID}`);
let { out } = result;
let expected =
stripIndent`
ID NAME VALUE FLEET DEVICE SERVICE
120110 svar1 svar1-value org/test * service1
120111 svar2 svar2-value org/test * service2
120120 svar3 svar3-value org/test ${uuid} service1
120121 svar4 svar4-value org/test ${uuid} service2
120120 svar3 svar3-value org/test ${shortUUID} service1
120121 svar4 svar4-value org/test ${shortUUID} service2
120101 var1 var1-val org/test * *
120102 var2 22 org/test * *
120203 var3 var3-val org/test ${uuid} *
120204 var4 44 org/test ${uuid} *
120203 var3 var3-val org/test ${shortUUID} *
120204 var4 44 org/test ${shortUUID} *
` + '\n';
out = out.map((l) => l.replace(/ +/g, ' '));
@ -168,6 +168,7 @@ describe('balena envs', function () {
});
it('should successfully list env variables for a test device (JSON output)', async () => {
api.expectGetDevice({ shortUUID, fullUUID });
api.expectGetDevice({ fullUUID });
api.expectGetDeviceEnvVars();
api.expectGetApplication();
@ -192,6 +193,7 @@ describe('balena envs', function () {
});
it('should successfully list config variables for a test device', async () => {
api.expectGetDevice({ shortUUID, fullUUID });
api.expectGetDevice({ fullUUID });
api.expectGetDeviceConfigVars();
api.expectGetApplication();
@ -216,24 +218,24 @@ describe('balena envs', function () {
const serviceName = 'service2';
api.expectGetService({ serviceName });
api.expectGetApplication();
api.expectGetDevice({ shortUUID, fullUUID });
api.expectGetDevice({ fullUUID });
api.expectGetDeviceServiceVars();
api.expectGetAppEnvVars();
api.expectGetAppServiceVars();
api.expectGetDeviceEnvVars();
const uuid = shortUUID;
const result = await runCommand(`envs -d ${uuid} -s ${serviceName}`);
const result = await runCommand(`envs -d ${shortUUID} -s ${serviceName}`);
let { out } = result;
let expected =
stripIndent`
ID NAME VALUE FLEET DEVICE SERVICE
120111 svar2 svar2-value org/test * service2
120121 svar4 svar4-value org/test ${uuid} service2
120121 svar4 svar4-value org/test ${shortUUID} service2
120101 var1 var1-val org/test * *
120102 var2 22 org/test * *
120203 var3 var3-val org/test ${uuid} *
120204 var4 44 org/test ${uuid} *
120203 var3 var3-val org/test ${shortUUID} *
120204 var4 44 org/test ${shortUUID} *
` + '\n';
out = out.map((l) => l.replace(/ +/g, ' '));
@ -243,20 +245,20 @@ describe('balena envs', function () {
});
it('should successfully list env and service variables for a test device (unknown fleet)', async () => {
api.expectGetDevice({ shortUUID, fullUUID, inaccessibleApp: true });
api.expectGetDevice({ fullUUID, inaccessibleApp: true });
api.expectGetDeviceEnvVars();
api.expectGetDeviceServiceVars();
const uuid = shortUUID;
const result = await runCommand(`envs -d ${uuid}`);
const result = await runCommand(`envs -d ${shortUUID}`);
let { out } = result;
let expected =
stripIndent`
ID NAME VALUE FLEET DEVICE SERVICE
120120 svar3 svar3-value N/A ${uuid} service1
120121 svar4 svar4-value N/A ${uuid} service2
120203 var3 var3-val N/A ${uuid} *
120204 var4 44 N/A ${uuid} *
120120 svar3 svar3-value N/A ${shortUUID} service1
120121 svar4 svar4-value N/A ${shortUUID} service2
120203 var3 var3-val N/A ${shortUUID} *
120204 var4 44 N/A ${shortUUID} *
` + '\n';
out = out.map((l) => l.replace(/ +/g, ' '));
@ -271,22 +273,22 @@ describe('balena envs', function () {
api.expectGetApplication();
api.expectGetAppEnvVars();
api.expectGetAppServiceVars();
api.expectGetDevice({ shortUUID, fullUUID });
api.expectGetDevice({ fullUUID });
api.expectGetDeviceEnvVars();
api.expectGetDeviceServiceVars();
const uuid = shortUUID;
const result = await runCommand(`envs -d ${uuid} -s ${serviceName}`);
const result = await runCommand(`envs -d ${shortUUID} -s ${serviceName}`);
let { out } = result;
let expected =
stripIndent`
ID NAME VALUE FLEET DEVICE SERVICE
120110 svar1 svar1-value org/test * ${serviceName}
120120 svar3 svar3-value org/test ${uuid} ${serviceName}
120120 svar3 svar3-value org/test ${shortUUID} ${serviceName}
120101 var1 var1-val org/test * *
120102 var2 22 org/test * *
120203 var3 var3-val org/test ${uuid} *
120204 var4 44 org/test ${uuid} *
120203 var3 var3-val org/test ${shortUUID} *
120204 var4 44 org/test ${shortUUID} *
` + '\n';
out = out.map((l) => l.replace(/ +/g, ' '));
@ -301,6 +303,7 @@ describe('balena envs', function () {
api.expectGetApplication();
api.expectGetAppEnvVars();
api.expectGetAppServiceVars();
api.expectGetDevice({ shortUUID, fullUUID });
api.expectGetDevice({ fullUUID });
api.expectGetDeviceEnvVars();
api.expectGetDeviceServiceVars();

View File

@ -178,7 +178,7 @@ async function startMockSshServer(): Promise<[Server, number]> {
});
return await new Promise<[Server, number]>((resolve, reject) => {
// TODO: remove 'as any' below. According to @types/node v14.18.36, the
// TODO: remove 'as any' below. According to @types/node v16.18.25, the
// callback type is `() => void`, but our code assumes `(err: Error) => void`
const listener = (server.listen as any)(0, '127.0.0.1', (err: Error) => {
// this callback is called for the 'listening' event

View File

@ -212,13 +212,20 @@ export class BalenaAPIMock extends NockMock {
public expectGetDevice(opts: {
fullUUID: string;
shortUUID?: string;
inaccessibleApp?: boolean;
isOnline?: boolean;
optional?: boolean;
persist?: boolean;
}) {
const id = 7654321;
this.optGet(/^\/v\d+\/device($|\?)/, opts).reply(200, {
const providedUuid = opts.shortUUID ?? opts.fullUUID;
this.optGet(
providedUuid.length !== 32
? /^\/v\d+\/device($|\?)/
: /^\/v\d+\/device\(uuid=%27[0-9a-f]{32}%27\)/,
opts,
).reply(200, {
d: [
{
id,

View File

@ -113,7 +113,7 @@ async function createProxyServer(): Promise<[number, number]> {
let proxyPort = 0; // TCP port number, 0 means automatic allocation
await new Promise<void>((resolve, reject) => {
// TODO: remove 'as any' below. According to @types/node v14.18.36, the
// TODO: remove 'as any' below. According to @types/node v16.18.25, the
// callback type is `() => void`, but our code assumes `(err: Error) => void`
const listener = (server.listen as any)(0, '127.0.0.1', (err: Error) => {
if (err) {
@ -197,7 +197,7 @@ async function createInterceptorServer(): Promise<number> {
let interceptorPort = 0;
await new Promise<void>((resolve, reject) => {
// TODO: remove 'as any' below. According to @types/node v14.18.36, the
// TODO: remove 'as any' below. According to @types/node v16.18.25, the
// callback type is `() => void`, but our code assumes `(err: Error) => void`
const listener = (server.listen as any)(0, '127.0.0.1', (err: Error) => {
if (err) {

View File

@ -0,0 +1,91 @@
> Warning Cannot include file %1 into executable.
The file must be distributed with executable as %2.
%1: node_modules/open/xdg-open
%2: path-to-executable/xdg-open
> Warning Cannot include file %1 into executable.
The file must be distributed with executable as %2.
%1: node_modules/open/xdg-open
%2: path-to-executable/xdg-open
> Warning Cannot include file %1 into executable.
The file must be distributed with executable as %2.
%1: node_modules/etcher-sdk/node_modules/drivelist/build/Release/drivelist.node
%2: path-to-executable/drivelist.node
> Warning Cannot include file %1 into executable.
The file must be distributed with executable as %2.
%1: node_modules/etcher-sdk/node_modules/drivelist/scripts/darwin.sh
%2: path-to-executable/drivelist/darwin.sh
> Warning Cannot include file %1 into executable.
The file must be distributed with executable as %2.
%1: node_modules/etcher-sdk/node_modules/drivelist/scripts/linux.sh
%2: path-to-executable/drivelist/linux.sh
> Warning Cannot include file %1 into executable.
The file must be distributed with executable as %2.
%1: node_modules/etcher-sdk/node_modules/drivelist/scripts/win32.bat
%2: path-to-executable/drivelist/win32.bat
> Warning Cannot include file %1 into executable.
The file must be distributed with executable as %2.
%1: node_modules/etcher-sdk/node_modules/drivelist/build/Release/drivelist.node
%2: path-to-executable/drivelist.node
> Warning Cannot include file %1 into executable.
The file must be distributed with executable as %2.
%1: node_modules/etcher-sdk/node_modules/drivelist/scripts/darwin.sh
%2: path-to-executable/drivelist/darwin.sh
> Warning Cannot include file %1 into executable.
The file must be distributed with executable as %2.
%1: node_modules/etcher-sdk/node_modules/drivelist/scripts/linux.sh
%2: path-to-executable/drivelist/linux.sh
> Warning Cannot include file %1 into executable.
The file must be distributed with executable as %2.
%1: node_modules/etcher-sdk/node_modules/drivelist/scripts/win32.bat
%2: path-to-executable/drivelist/win32.bat
> Warning Cannot resolve 'path'
node_modules/@balena/compose/dist/parse/schemas/index.js
Dynamic require may fail at run time, because the requested file
is unknown at compilation time and not included into executable.
Use a string literal as an argument for 'require', or leave it
as is and specify the resolved file name in 'scripts' option.
> Warning Cannot include file %1 into executable.
The file must be distributed with executable as %2.
%1: node_modules/drivelist/build/Release/drivelist.node
%2: path-to-executable/drivelist.node
> Warning Cannot include file %1 into executable.
The file must be distributed with executable as %2.
%1: node_modules/drivelist/scripts/darwin.sh
%2: path-to-executable/drivelist/darwin.sh
> Warning Cannot include file %1 into executable.
The file must be distributed with executable as %2.
%1: node_modules/drivelist/scripts/linux.sh
%2: path-to-executable/drivelist/linux.sh
> Warning Cannot include file %1 into executable.
The file must be distributed with executable as %2.
%1: node_modules/drivelist/scripts/win32.bat
%2: path-to-executable/drivelist/win32.bat
> Warning Cannot include file %1 into executable.
The file must be distributed with executable as %2.
%1: node_modules/drivelist/build/Release/drivelist.node
%2: path-to-executable/drivelist.node
> Warning Cannot include file %1 into executable.
The file must be distributed with executable as %2.
%1: node_modules/drivelist/scripts/darwin.sh
%2: path-to-executable/drivelist/darwin.sh
> Warning Cannot include file %1 into executable.
The file must be distributed with executable as %2.
%1: node_modules/drivelist/scripts/linux.sh
%2: path-to-executable/drivelist/linux.sh
> Warning Cannot include file %1 into executable.
The file must be distributed with executable as %2.
%1: node_modules/drivelist/scripts/win32.bat
%2: path-to-executable/drivelist/win32.bat
> Warning Cannot include file %1 into executable.
The file must be distributed with executable as %2.
%1: node_modules/opn/xdg-open
%2: path-to-executable/xdg-open
> Warning Cannot include file %1 into executable.
The file must be distributed with executable as %2.
%1: node_modules/opn/xdg-open
%2: path-to-executable/xdg-open
prebuild-install warn install No prebuilt binaries found (target=v16.16.0 runtime=node arch=x64 libc= platform=darwin)
prebuild-install warn install No prebuilt binaries found (target=v16.16.0 runtime=node arch=x64 libc= platform=darwin)
prebuild-install warn install No prebuilt binaries found (target=v16.16.0 runtime=node arch=x64 libc= platform=darwin)
prebuild-install warn install No prebuilt binaries found (target=v16.16.0 runtime=node arch=x64 libc= platform=darwin)
prebuild-install warn install No prebuilt binaries found (target=v16.16.0 runtime=node arch=x64 libc= platform=darwin)

View File

@ -38,14 +38,6 @@
The file must be distributed with executable as %2.
%1: node_modules/etcher-sdk/node_modules/drivelist/scripts/win32.bat
%2: path-to-executable/drivelist/win32.bat
> Warning Cannot include file %1 into executable.
The file must be distributed with executable as %2.
%1: node_modules/patch-package/node_modules/open/xdg-open
%2: path-to-executable/xdg-open
> Warning Cannot include file %1 into executable.
The file must be distributed with executable as %2.
%1: node_modules/patch-package/node_modules/open/xdg-open
%2: path-to-executable/xdg-open
> Warning Cannot resolve 'path'
node_modules/@balena/compose/dist/parse/schemas/index.js
Dynamic require may fail at run time, because the requested file
@ -92,3 +84,7 @@
The file must be distributed with executable as %2.
%1: node_modules/opn/xdg-open
%2: path-to-executable/xdg-open
prebuild-install warn install No prebuilt binaries found (target=v16.16.0 runtime=node arch=arm64 libc= platform=linux)
prebuild-install warn install No prebuilt binaries found (target=v16.16.0 runtime=node arch=arm64 libc= platform=linux)
prebuild-install warn install No prebuilt binaries found (target=v16.16.0 runtime=node arch=arm64 libc= platform=linux)
prebuild-install warn install No prebuilt binaries found (target=v16.16.0 runtime=node arch=arm64 libc= platform=linux)

View File

@ -38,14 +38,6 @@
The file must be distributed with executable as %2.
%1: node_modules/etcher-sdk/node_modules/drivelist/scripts/win32.bat
%2: path-to-executable/drivelist/win32.bat
> Warning Cannot include file %1 into executable.
The file must be distributed with executable as %2.
%1: node_modules/patch-package/node_modules/open/xdg-open
%2: path-to-executable/xdg-open
> Warning Cannot include file %1 into executable.
The file must be distributed with executable as %2.
%1: node_modules/patch-package/node_modules/open/xdg-open
%2: path-to-executable/xdg-open
> Warning Cannot resolve 'path'
node_modules/@balena/compose/dist/parse/schemas/index.js
Dynamic require may fail at run time, because the requested file
@ -92,3 +84,7 @@
The file must be distributed with executable as %2.
%1: node_modules/opn/xdg-open
%2: path-to-executable/xdg-open
prebuild-install warn install No prebuilt binaries found (target=v16.16.0 runtime=node arch=x64 libc= platform=linux)
prebuild-install warn install No prebuilt binaries found (target=v16.16.0 runtime=node arch=x64 libc= platform=linux)
prebuild-install warn install No prebuilt binaries found (target=v16.16.0 runtime=node arch=x64 libc= platform=linux)
prebuild-install warn install No prebuilt binaries found (target=v16.16.0 runtime=node arch=x64 libc= platform=linux)

View File

@ -38,14 +38,6 @@
The file must be distributed with executable as %2.
%1: node_modules\etcher-sdk\node_modules\drivelist\scripts\win32.bat
%2: path-to-executable/drivelist/win32.bat
> Warning Cannot include file %1 into executable.
The file must be distributed with executable as %2.
%1: node_modules\patch-package\node_modules\open\xdg-open
%2: path-to-executable/xdg-open
> Warning Cannot include file %1 into executable.
The file must be distributed with executable as %2.
%1: node_modules\patch-package\node_modules\open\xdg-open
%2: path-to-executable/xdg-open
> Warning Cannot resolve 'path'
node_modules\@balena\compose\dist\parse\schemas\index.js
Dynamic require may fail at run time, because the requested file

View File

@ -63,8 +63,6 @@ describe('detectEncoding() function', function () {
it('should correctly detect the encoding of a few selected files', async () => {
const sampleBinary = [
'drivelist/build/Release/drivelist.node',
'@balena.io/usb/build/Release/usb_bindings.node',
'xxhash/build/Release/hash.node',
'mountutils/build/Release/MountUtils.node',
];
const sampleText = [

View File

@ -1,27 +0,0 @@
/**
* @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.
*/
declare module 'balena-image-manager' {
export function cleanCache(): Promise<void>;
export function get(
deviceType: string,
versionOrRange: string,
): Promise<NodeJS.ReadableStream & { mime: string }>;
export function isESR(version: string): boolean;
}

View File

@ -1,19 +0,0 @@
declare module 'balena-preload' {
export class Preloader {
constructor(...args: any[]);
cleanup(): Promise<void>;
on(...args: any[]): void;
preload(): Promise<void>;
prepare(): Promise<void>;
setAppIdAndCommit(appId: string | number, commit: string): Promise<void>;
config: any;
stderr: any;
}
}

View File

@ -1,23 +0,0 @@
declare module 'docker-toolbelt' {
import * as Docker from 'dockerode';
interface ImageSpec {
registry?: string;
imageName: string;
tagName: string;
digest?: string;
}
type ProgressCallback = (event: any) => void;
class DockerToolbelt extends Docker {
public getRegistryAndName(image: string): Promise<ImageSpec>;
public createDeltaAsync(
src: string,
dest: string,
onProgress?: ProgressCallback,
): Promise<string>;
}
export = DockerToolbelt;
}