Compare commits

..

84 Commits

Author SHA1 Message Date
ba4b9bd447 v21.0.0 2025-03-11 14:42:31 +00:00
02c0ea5b59 Merge pull request #2921 from balena-io/major-21-second-attempt
Major 21
2025-03-11 10:41:28 -04:00
bc3558dd8e Address SDK major v21 breaking changes 2025-03-11 08:19:10 -04:00
aad62d1ccd Drop support for OS versions <2.14.0
Change-type: major
2025-03-11 08:19:10 -04:00
ecc6f80164 api-key generate: Add required argument expiryDate
Change-type: major
2025-03-11 08:19:10 -04:00
c0fd1e3886 Deduplicate dependencies 2025-03-11 08:18:56 -04:00
9d3120b144 Update balena-preload to 18.0.1
Change-type: patch
2025-03-11 08:17:27 -04:00
ed0e03ddb2 Add dependency date-fns
Change-type: patch
2025-03-11 08:16:50 -04:00
8fe6d6c026 Update balena-sdk to 21.2.1
Change-type: patch
2025-03-11 08:16:31 -04:00
727033ae14 v20.2.10 2025-03-10 17:33:13 +00:00
c19ce6a905 Merge pull request #2923 from balena-io/bump-typescript-to-5.8.2
Bump typescript to 5.8.2
2025-03-10 17:32:13 +00:00
1a33029738 Deduplicate dependencies 2025-03-10 17:51:18 +02:00
043bc48a1c Add a "deduplicate-dependencies" npm script to standardize such commits 2025-03-04 08:48:31 +02:00
a10156a441 Update TypeScript to 5.8.2
Change-type: patch
2025-03-04 08:48:31 +02:00
4f665f43d2 v20.2.9 2025-02-26 12:52:10 +00:00
9f097a96f5 Merge pull request #2920 from balena-io/x-balena-client-fix
Fix CORS issue with X-Balena-Client header
2025-02-26 12:51:20 +00:00
64d1943804 Fix CORS issue with X-Balena-Client header
Change-type: patch
See: https://balena.fibery.io/Work/Project/Extend-the-X-Balena-Client-header-to-include-the-UI-CLI-version-as-well-1174
2025-02-26 14:24:57 +02:00
666ce876e6 v20.2.8 2025-02-26 00:22:16 +00:00
e01184080f Merge pull request #2915 from balena-io/fix_os_configure_test
Update balena-config-json dependency and fix test
2025-02-25 19:21:25 -05:00
93039b010d Update balena-config-json dependency and fix test
Change-type: patch
Signed-off-by: Ken Bannister <kb2ma@runbox.com>
2025-02-25 18:43:12 -05:00
795259bf30 v20.2.7 2025-02-25 20:21:03 +00:00
fa134d2d39 Merge pull request #2917 from balena-io/x-balena-client
Use the CLI version in the X-Balena-Client header
2025-02-25 20:20:10 +00:00
bef5221ed8 Use the CLI version in the X-Balena-Client header
Change-type: patch
See: https://balena.fibery.io/Work/Project/Extend-the-X-Balena-Client-header-to-include-the-UI-CLI-version-as-well-1174
2025-02-25 21:59:35 +02:00
72d6db796c v20.2.6 2025-02-25 19:13:51 +00:00
e848eb63ee Merge pull request #2918 from balena-io/renovate/actions-upload-artifact-digest
Update actions/upload-artifact digest to 4cec3d8
2025-02-25 19:12:56 +00:00
6f0f7350cf Update actions/upload-artifact digest to 4cec3d8
Update actions/upload-artifact

Change-type: patch
2025-02-25 18:51:27 +00:00
07a88c700e v20.2.5 2025-02-25 18:10:50 +00:00
9cae66bd92 Merge pull request #2913 from balena-io/renovate/actions-setup-node-digest
Update actions/setup-node digest to 1d0ff46
2025-02-25 18:09:54 +00:00
cddea24cef Update actions/setup-node digest to 1d0ff46
Update actions/setup-node

Change-type: patch
2025-02-25 17:45:52 +00:00
b1c246c0b4 v20.2.4 2025-02-25 17:17:03 +00:00
00b4d57a03 Merge pull request #2916 from balena-io/pin_docker-modem_regression
Pin docker-modem and dockerode to avoid regression in docker-modem v5.0.6
2025-02-25 12:16:10 -05:00
2cba82e914 Pin docker-modem and dockerode to avoid regression
Change-type: patch
Signed-off-by: Ken Bannister <kb2ma@runbox.com>
2025-02-24 20:22:59 -05:00
1352c5c823 v20.2.3 2025-01-15 18:21:09 +00:00
c86eb97010 Merge pull request #2910 from balena-io/clean-eslint
Remove unused old eslint version files
2025-01-15 18:19:22 +00:00
53be743b9d Remove unused old eslint version files
Change-type: patch
2025-01-15 08:37:35 -03:00
d9f21b4c3f v20.2.2 2025-01-12 14:49:07 +00:00
261ab398dd Merge pull request #2906 from balena-io/bump-balena-image-fs
Use the promises namespace of balena-image-fs
2025-01-12 16:48:08 +02:00
f28a9992e4 Deduplicate dependencies 2025-01-09 23:33:54 +02:00
29e7827eb1 Use the promises namespace of balena-image-fs
Change-type: patch
2025-01-09 23:33:54 +02:00
1d77cf3665 Update balena-device-init to 8.1.3 & balena-image-fs to 7.3.0
Update balena-device-init from 8.1.0 to 8.1.3
Update balena-image-fs from 7.0.6 to 7.3.0

Change-type: patch
2025-01-09 23:33:54 +02:00
017c767f61 v20.2.1 2025-01-01 18:05:02 +00:00
7d79c4e24b Merge pull request #2905 from balena-io/update-balena-preload-17.0.0
Update balena-preload to 17.0.0
2025-01-01 20:04:01 +02:00
60bc5092e0 Update balena-preload to 17.0.0
Update balena-preload from 16.0.0 to 17.0.0

Change-type: patch
2024-12-31 20:42:54 +02:00
a33a794931 v20.2.0 2024-12-31 18:41:50 +00:00
f0ede6fca2 Merge pull request #2901 from balena-io/balena-device-init-v-8-1-0
os configure: Locate the boot partition w/o using the device-type.json's partition field
2024-12-31 10:40:54 -08:00
dbe177e766 os configure: Give precedence to the boot partition located in the image over the device-type.json contents
Update balena-device-init from 8.0.0 to 8.1.0

Change-type: minor
2024-12-31 19:48:11 +02:00
09f80730a8 Deduplicate dependencies 2024-12-31 19:47:11 +02:00
327d28c103 v20.1.6 2024-12-30 17:16:14 +00:00
56ab785a82 Merge pull request #2903 from balena-io/os-configure-tests
Add more realistic os configure tests
2024-12-30 17:15:09 +00:00
305d65d5ed Add more realistic os configure tests
Change-type: patch
2024-12-30 18:44:07 +02:00
c4d3686a34 Deduplicate dependencies 2024-12-30 15:57:12 +02:00
ce06854b55 v20.1.5 2024-12-20 17:55:24 +00:00
8db05cc8a7 Merge pull request #2902 from oskarwilliams/express-version-4.21.2
Update shrinkwrapped express to v4.21.2
2024-12-20 17:54:36 +00:00
7a22c987d2 Update shrinkwrapped express to v4.21.2
Change-type: patch
2024-12-20 17:23:28 +00:00
45efbcdfe3 v20.1.4 2024-12-20 16:57:38 +00:00
d6a9b78b3e Merge pull request #2899 from balena-io/balena-device-init-v8
Update balena-device-init to 8.0.0
2024-12-20 16:56:43 +00:00
e8ac3ea960 Update balena-device-init to 8.0.0
Update balena-device-init from 7.0.1 to 8.0.0

Change-type: patch
See: https://github.com/balena-io-modules/balena-device-init/pull/46
2024-12-20 18:36:50 +02:00
0ffa0f85a2 v20.1.3 2024-12-20 14:33:05 +00:00
5e7479f60e Merge pull request #2898 from balena-io/bump-oclif-and-oclif-core
Update oclif to 4.17.0 and @oclif/core 4.1.0
2024-12-20 11:32:01 -03:00
07365c45f2 Update oclif to 4.17.0 and @oclif/core 4.1.0
Change-type: patch
2024-12-20 11:09:29 -03:00
e5076434c6 v20.1.2 2024-12-17 11:35:00 +00:00
5d687f5a55 Merge pull request #2896 from balena-io/unnecessary-promise-resolve-reject
Remove unnecessary `Promise.resolve` and `Promise.reject`
2024-12-17 11:34:05 +00:00
e192767156 Remove unnecessary Promise.resolve and Promise.reject
Change-type: patch
2024-12-16 21:12:02 +00:00
5a8d2fad5f v20.1.1 2024-12-16 17:36:44 +00:00
45f482fad1 Merge pull request #2895 from balena-io/bump-balena-lint-v9
Update @balena/lint to v9.1.3
2024-12-16 17:35:45 +00:00
c0e7ae9c91 Update @balena/lint to v9.1.3
Update @balena/lint from 8.0.0 to 9.1.3

Change-type: patch
2024-12-16 14:09:47 -03:00
36077cacda v20.1.0 2024-12-12 14:17:22 +00:00
82b9983450 Merge pull request #2890 from balena-io/takeover
`device os-update`: Add support for updates that require takeover
2024-12-12 09:16:14 -05:00
703dbd01c9 device os-update: Add handling for updates that require takeover
Change-type: minor
2024-12-11 12:04:53 -05:00
602e63c8a9 Fix typings in release and release list commands 2024-12-11 12:00:08 -05:00
2ab635f49a Update expected warnings 2024-12-11 12:00:08 -05:00
322736a145 Deduplicate dependencies 2024-12-11 12:00:08 -05:00
c347b67b25 Update balena-sdk
Change-type: patch
2024-12-11 12:00:08 -05:00
4022beeb56 Update @balena/compose
Change-type: patch
2024-12-11 12:00:08 -05:00
ccf97cfc9f v20.0.9 2024-12-05 12:08:28 +00:00
9c5fe14f2e Merge pull request #2889 from oskarwilliams/express-version-4.21.1
Update shrinkwrapped express to v4.21.1
2024-12-05 12:07:23 +00:00
38e29251e7 Update shrinkwrapped express to v4.21.1
Change-type: patch
2024-12-05 09:59:57 +00:00
bfc7a14646 v20.0.8 2024-12-04 19:37:38 +00:00
610db81fcb Merge pull request #2893 from balena-io/bump-macos-13
Run test and publish with macos-13
2024-12-04 16:36:42 -03:00
d1f7d6d07f Run test and publish with macos-13
Change-type: patch
2024-12-04 16:16:52 -03:00
694eb78aaa v20.0.7 2024-11-23 17:38:10 +00:00
1caccafbcd Merge pull request #2891 from balena-io/bump-typescript-to-5.7.2
Bump typescript to 5.7.2
2024-11-23 17:37:10 +00:00
61d4d1f1e7 Update TypeScript to 5.7.2
Change-type: patch
2024-11-23 00:40:43 +02:00
a01c85bc15 Deduplicate dependencies 2024-11-23 00:40:28 +02:00
97 changed files with 5839 additions and 2497 deletions

View File

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

View File

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

View File

@ -39,7 +39,7 @@ runs:
run: tar -xf ${{ runner.temp }}/custom.tgz
- name: Setup Node.js
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4
with:
node-version: ${{ inputs.NODE_VERSION }}
cache: npm
@ -135,7 +135,7 @@ runs:
XCODE_APP_LOADER_TEAM_ID: ${{ inputs.XCODE_APP_LOADER_TEAM_ID }}
- name: Upload artifacts
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
with:
name: gh-release-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ strategy.job-index }}
path: dist

View File

@ -26,7 +26,7 @@ runs:
steps:
# https://github.com/actions/setup-node#caching-global-packages-data
- name: Setup Node.js
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4
with:
node-version: ${{ inputs.NODE_VERSION }}
cache: npm
@ -58,7 +58,7 @@ runs:
run: tar --exclude-vcs -acf ${{ runner.temp }}/custom.tgz .
- name: Upload custom artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4
with:
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}-${{ runner.arch }}
path: ${{ runner.temp }}/custom.tgz

View File

@ -26,7 +26,7 @@ jobs:
"os": [
["self-hosted", "X64"],
["self-hosted", "ARM64"],
["macos-12"],
["macos-13"],
["windows-2019"],
["macos-latest-xlarge"]
]
@ -36,7 +36,7 @@ jobs:
"os": [
["self-hosted", "X64"],
["self-hosted", "ARM64"],
["macos-12"],
["macos-13"],
["windows-2019"],
["macos-latest-xlarge"]
]

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,228 @@ 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/).
## 21.0.0 - 2025-03-11
* Drop support for OS versions <2.14.0 [myarmolinsky]
* api-key generate: Add required argument `expiryDate` [myarmolinsky]
* Update `balena-preload` to 18.0.1 [myarmolinsky]
* Add dependency `date-fns` [myarmolinsky]
* Update `balena-sdk` to 21.2.1 [myarmolinsky]
## 20.2.10 - 2025-03-10
* Update TypeScript to 5.8.2 [Thodoris Greasidis]
## 20.2.9 - 2025-02-26
* Fix CORS issue with X-Balena-Client header [Thodoris Greasidis]
## 20.2.8 - 2025-02-26
* Update balena-config-json dependency and fix test [Ken Bannister]
## 20.2.7 - 2025-02-25
* Use the CLI version in the X-Balena-Client header [Thodoris Greasidis]
## 20.2.6 - 2025-02-25
* Update actions/upload-artifact digest to 4cec3d8 [balena-renovate[bot]]
## 20.2.5 - 2025-02-25
* Update actions/setup-node digest to 1d0ff46 [balena-renovate[bot]]
## 20.2.4 - 2025-02-25
* Pin docker-modem and dockerode to avoid regression [Ken Bannister]
## 20.2.3 - 2025-01-15
* Remove unused old eslint version files [Otavio Jacobi]
## 20.2.2 - 2025-01-12
* Use the promises namespace of balena-image-fs [Thodoris Greasidis]
<details>
<summary> Update balena-device-init to 8.1.3 & balena-image-fs to 7.3.0 [Thodoris Greasidis] </summary>
> ### balena-image-fs-7.3.0 - 2025-01-06
>
> * Drop Bluebird from devDependencies [Thodoris Greasidis]
> * flowzone: Remove empty with clause [Thodoris Greasidis]
> * Add the promises namespace as part of the exposed fs [Thodoris Greasidis]
>
> ### balena-image-fs-7.2.2 - 2024-01-02
>
> * Update dependency @types/node to v20 [Self-hosted Renovate Bot]
>
> ### balena-image-fs-7.2.1 - 2023-12-19
>
> * Remove repo config from flowzone.yml [Kyle Harding]
>
> ### balena-image-fs-7.2.0 - 2023-01-20
>
> * Add support for Node 18 [Akis Kesoglou]
>
> ### balena-image-fs-7.1.2 - 2023-01-05
>
> * Update dependencies [ab77]
>
> ### balena-image-fs-7.1.1 - 2022-12-20
>
> * Update dependency jsdoc-to-markdown to 8.0.0 [Renovate Bot]
>
> ### balena-image-fs-7.1.0 - 2022-12-13
>
> * update dependencies [Zane Hitchcox]
>
> ### balena-device-init-8.1.3 - 2025-01-09
>
> * README: Remove the travisci badge [Thodoris Greasidis]
>
> ### balena-device-init-8.1.2 - 2025-01-09
>
>
> <details>
> <summary> Use the promises namespace of balena-image-fs [Thodoris Greasidis] </summary>
>
>> #### balena-image-fs-7.3.0 - 2025-01-06
>>
>> * Drop Bluebird from devDependencies [Thodoris Greasidis]
>> * flowzone: Remove empty with clause [Thodoris Greasidis]
>> * Add the promises namespace as part of the exposed fs [Thodoris Greasidis]
>>
>> #### balena-image-fs-7.2.2 - 2024-01-02
>>
>> * Update dependency @types/node to v20 [Self-hosted Renovate Bot]
>>
>> #### balena-image-fs-7.2.1 - 2023-12-19
>>
>> * Remove repo config from flowzone.yml [Kyle Harding]
>>
>> #### balena-image-fs-7.2.0 - 2023-01-20
>>
>> * Add support for Node 18 [Akis Kesoglou]
>>
>> #### balena-image-fs-7.1.2 - 2023-01-05
>>
>> * Update dependencies [ab77]
>>
>> #### balena-image-fs-7.1.1 - 2022-12-20
>>
>> * Update dependency jsdoc-to-markdown to 8.0.0 [Renovate Bot]
>>
>> #### balena-image-fs-7.1.0 - 2022-12-13
>>
>> * update dependencies [Zane Hitchcox]
>>
>
> </details>
>
>
> ### balena-device-init-8.1.1 - 2025-01-06
>
> * Convert some parts to async await and simplify [Thodoris Greasidis]
> * Avoid unnecessary destructuring [Thodoris Greasidis]
>
</details>
## 20.2.1 - 2025-01-01
<details>
<summary> Update balena-preload to 17.0.0 [Thodoris Greasidis] </summary>
> ### balena-preload-17.0.0 - 2024-10-21
>
> * Improve typings [Thodoris Greasidis]
> * Stop returning Bluebird promises & drop it from the dependencies [Thodoris Greasidis]
>
</details>
## 20.2.0 - 2024-12-31
<details>
<summary> os configure: Give precedence to the boot partition located in the image over the device-type.json contents [Thodoris Greasidis] </summary>
> ### balena-device-init-8.1.0 - Invalid date
>
> * Try to find the boot partition by inspecting the image [Thodoris Greasidis]
>
> ### balena-device-init-8.0.1 - 2024-12-19
>
> * Drop the unnecessary eslint.config.js [Thodoris Greasidis]
> * packacke.json: Explicitly set type commonjs [Thodoris Greasidis]
>
</details>
## 20.1.6 - 2024-12-30
* Add more realistic os configure tests [Thodoris Greasidis]
## 20.1.5 - 2024-12-20
* Update shrinkwrapped express to v4.21.2 [Oskar Williams]
## 20.1.4 - 2024-12-20
<details>
<summary> Update balena-device-init to 8.0.0 [Thodoris Greasidis] </summary>
> ### balena-device-init-8.0.0 - 2024-12-18
>
> * Avoid running linting in the custom tests [Thodoris Greasidis]
> * Stop returning Bluebird promises [Thodoris Greasidis]
> * package: Publish only the build & typings folders [Thodoris Greasidis]
> * Convert to TypeScript with es6 module notation [Thodoris Greasidis]
> * Replace gulp, coffeelint & mochainon with tsc, @balena/lint, mocha, chai & sinon [Thodoris Greasidis]
> * Drop support for node <20.6.0 [Thodoris Greasidis]
>
> ### balena-device-init-7.0.2 - 2024-12-17
>
> * flowzone: Update runner versions [Thodoris Greasidis]
> * Pin etcher-sdk to 9.0.8 to match resin-device-operations and fix tests [Thodoris Greasidis]
>
</details>
## 20.1.3 - 2024-12-20
* Update oclif to 4.17.0 and @oclif/core 4.1.0 [Otavio Jacobi]
## 20.1.2 - 2024-12-17
* Remove unnecessary `Promise.resolve` and `Promise.reject` [Pagan Gazzard]
## 20.1.1 - 2024-12-16
* Update @balena/lint to v9.1.3 [Otavio Jacobi]
## 20.1.0 - 2024-12-12
* `device os-update`: Add handling for updates that require takeover [myarmolinsky]
* Update `balena-sdk` [myarmolinsky]
* Update `@balena/compose` [myarmolinsky]
## 20.0.9 - 2024-12-05
* Update shrinkwrapped express to v4.21.1 [Oskar Williams]
## 20.0.8 - 2024-12-04
* Run test and publish with macos-13 [Otavio Jacobi]
## 20.0.7 - 2024-11-23
* Update TypeScript to 5.7.2 [Thodoris Greasidis]
## 20.0.6 - 2024-11-08
* Refactor balena build for clarity [Thodoris Greasidis]

View File

@ -305,7 +305,7 @@ async function zipPkg() {
);
}
await fs.mkdirp(path.dirname(outputFile));
await new Promise((resolve, reject) => {
await new Promise<void>((resolve, reject) => {
console.log(`Zipping standalone package to "${outputFile}"...`);
const archive = archiver('zip', {

View File

@ -145,7 +145,7 @@ export async function getCapitanoDoc(): Promise<typeof capitanoDoc> {
throw new Error(`Error parsing section title`);
}
// match[1] has the title, match[2] has the rest
return match && match[2];
return match?.[2];
}),
mdParser.getSectionOfTitle('Installation'),
mdParser.getSectionOfTitle('Choosing a shell (command prompt/terminal)'),

View File

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

View File

@ -19,6 +19,7 @@ import { spawn } from 'child_process';
import * as path from 'path';
import * as fs from 'fs';
import { diffTrimmedLines } from 'diff';
import * as whichMod from 'which';
export const ROOT = path.join(__dirname, '..');
@ -101,7 +102,6 @@ export function loadPackageJson() {
* @returns The program's full path, e.g. 'C:\WINDOWS\System32\OpenSSH\ssh.EXE'
*/
export async function which(program: string): Promise<string> {
const whichMod = await import('which');
let programPath: string;
try {
programPath = await whichMod(program);
@ -132,7 +132,7 @@ export async function whichSpawn(
.on('error', reject)
.on('close', resolve);
} catch (err) {
reject(err);
reject(err as Error);
}
});
} catch (err) {

View File

@ -57,7 +57,10 @@ require('ts-node').register({
project: path.join(rootDir, 'tsconfig.json'),
transpileOnly: true,
});
require('../src/app').run(undefined, { dir: __dirname, development: true });
void require('../src/app').run(undefined, {
dir: __dirname,
development: true,
});
// Modify package.json oclif paths from build/ -> src/, or vice versa
function modifyOclifPaths(revert) {

View File

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

View File

@ -326,6 +326,8 @@ or to authenticate requests to the API with an 'Authorization: Bearer <key>' hea
Examples:
$ balena api-key generate "Jenkins Key"
$ balena api-key generate "Jenkins Key" 2025-10-30
$ balena api-key generate "Jenkins Key" never
### Arguments
@ -333,6 +335,10 @@ Examples:
the API key name
#### EXPIRYDATE
the expiry date of the API key as an ISO date string, or "never" for no expiry
## api-key list
### Aliases

32
eslint.config.js Normal file
View File

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

5728
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "balena-cli",
"version": "20.0.6",
"version": "21.0.0",
"description": "The official balena Command Line Interface",
"main": "./build/app.js",
"homepage": "https://github.com/balena-io/balena-cli",
@ -58,6 +58,7 @@
"build:completion": "node completion/generate-completion.js",
"build:standalone": "ts-node --transpile-only automation/run.ts build:standalone",
"build:installer": "ts-node --transpile-only automation/run.ts build:installer",
"deduplicate-dependencies": "npm dd && git add npm-shrinkwrap.json && git commit --message \"Deduplicate dependencies\"",
"package": "npm run build:fast && npm run build:standalone && npm run build:installer",
"pretest": "npm run build",
"test": "npm run test:shrinkwrap && npm run test:core",
@ -111,7 +112,7 @@
}
},
"devDependencies": {
"@balena/lint": "^8.0.0",
"@balena/lint": "^9.1.3",
"@electron/notarize": "^2.0.0",
"@types/archiver": "^6.0.2",
"@types/bluebird": "^3.5.36",
@ -180,27 +181,27 @@
"mock-fs": "^5.2.0",
"mock-require": "^3.0.3",
"nock": "^13.2.1",
"oclif": "^4.14.0",
"oclif": "^4.17.0",
"rewire": "^7.0.0",
"simple-git": "^3.14.1",
"sinon": "^19.0.0",
"string-to-stream": "^3.0.1",
"ts-node": "^10.4.0",
"typescript": "^5.6.2"
"typescript": "^5.8.2"
},
"dependencies": {
"@balena/compose": "^5.0.0",
"@balena/compose": "^6.0.0",
"@balena/dockerignore": "^1.0.2",
"@balena/env-parsing": "^1.1.8",
"@balena/es-version": "^1.0.1",
"@oclif/core": "^4.0.31",
"@oclif/core": "^4.1.0",
"@sentry/node": "^6.16.1",
"balena-config-json": "^4.2.0",
"balena-device-init": "^7.0.1",
"balena-config-json": "^4.2.2",
"balena-device-init": "^8.1.3",
"balena-errors": "^4.7.3",
"balena-image-fs": "^7.0.6",
"balena-preload": "^16.0.0",
"balena-sdk": "^20.3.0",
"balena-image-fs": "^7.3.0",
"balena-preload": "^18.0.1",
"balena-sdk": "^21.2.1",
"balena-semver": "^2.3.0",
"balena-settings-client": "^5.0.2",
"balena-settings-storage": "^8.1.0",
@ -211,10 +212,11 @@
"cli-truncate": "^2.1.0",
"color-hash": "^1.1.1",
"common-tags": "^1.7.2",
"date-fns": "^4.1.0",
"denymount": "^2.3.0",
"docker-modem": "^5.0.3",
"docker-modem": "5.0.5",
"docker-progress": "^5.1.3",
"dockerode": "^4.0.2",
"dockerode": "4.0.3",
"ejs": "^3.1.6",
"etcher-sdk": "9.1.0",
"express": "^4.17.2",
@ -274,6 +276,6 @@
}
},
"versionist": {
"publishedAt": "2024-11-08T08:10:09.471Z"
"publishedAt": "2025-03-11T14:42:29.446Z"
}
}

View File

@ -1,8 +1,8 @@
diff --git a/node_modules/oclif/lib/commands/pack/win.js b/node_modules/oclif/lib/commands/pack/win.js
index ef7f90e..8264b7c 100644
index bfe9205..482519e 100644
--- a/node_modules/oclif/lib/commands/pack/win.js
+++ b/node_modules/oclif/lib/commands/pack/win.js
@@ -76,6 +76,12 @@ InstallDir "\$PROGRAMFILES${arch === 'x64' ? '64' : ''}\\${config.dirname}"
@@ -86,6 +86,12 @@ InstallDir "\$PROGRAMFILES${arch === 'x64' ? '64' : ''}\\${config.dirname}"
${customization}
Section "${config.name} CLI \${VERSION}"
@ -16,20 +16,18 @@ index ef7f90e..8264b7c 100644
File /r bin
File /r client
diff --git a/node_modules/oclif/lib/tarballs/build.js b/node_modules/oclif/lib/tarballs/build.js
index 14d5a6e..7b42a6f 100644
index f0c8d95..a72400e 100644
--- a/node_modules/oclif/lib/tarballs/build.js
+++ b/node_modules/oclif/lib/tarballs/build.js
@@ -200,6 +200,13 @@ const extractCLI = async (tarball, c) => {
@@ -218,6 +218,11 @@ const extractCLI = async (tarball, c) => {
(0, promises_1.rm)(path.join(workspace, path.basename(tarball)), { recursive: true }),
(0, fs_extra_1.remove)(path.join(workspace, 'bin', 'run.cmd')),
]);
+
+ // 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 (0, fs_extra_1.remove)(path.join(workspace, 'bin', '.fast-boot.json'));
+
};
const buildTarget = async (target, c, options) => {
const workspace = c.workspace(target);
if (target.platform === 'win32' && target.arch === 'arm64' && (0, semver_1.lt)(c.nodeVersion, '20.0.0')) {

View File

@ -6,6 +6,10 @@ upstream:
url: 'https://github.com/balena-io/balena-sdk'
- repo: 'balena-config-json'
url: 'https://github.com/balena-io-modules/balena-config-json'
- repo: 'balena-image-fs'
url: 'https://github.com/balena-io-modules/balena-image-fs'
- repo: 'balena-device-init'
url: 'https://github.com/balena-io-modules/balena-device-init'
- repo: 'balena-image-manager'
url: 'https://github.com/balena-io-modules/balena-image-manager'
- repo: 'balena-preload'

View File

@ -101,11 +101,9 @@ async function init() {
/** Execute the oclif parser and the CLI command. */
async function oclifRun(command: string[], options: AppOptions) {
let deprecationPromise: Promise<void>;
let deprecationPromise: Promise<void> | undefined;
// check and enforce the CLI's deprecation policy
if (unsupportedFlag || process.env.BALENARC_UNSUPPORTED) {
deprecationPromise = Promise.resolve();
} else {
if (!(unsupportedFlag || process.env.BALENARC_UNSUPPORTED)) {
const { DeprecationChecker } = await import('./deprecation');
const deprecationChecker = new DeprecationChecker(packageJSON.version);
// warnAndAbortIfDeprecated uses previously cached data only

View File

@ -17,7 +17,16 @@
import { Args, Command } from '@oclif/core';
import { ExpectedError } from '../../errors';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { getBalenaSdk, getCliForm, stripIndent } from '../../utils/lazy';
import {
formatDuration,
intervalToDuration,
isValid,
parseISO,
} from 'date-fns';
// In days
const durations = [1, 7, 30, 90];
async function isLoggedInWithJwt() {
const balena = getBalenaSdk();
@ -41,13 +50,21 @@ export default class GenerateCmd extends Command {
This key can be used to log into the CLI using 'balena login --token <key>',
or to authenticate requests to the API with an 'Authorization: Bearer <key>' header.
`;
public static examples = ['$ balena api-key generate "Jenkins Key"'];
public static examples = [
'$ balena api-key generate "Jenkins Key"',
'$ balena api-key generate "Jenkins Key" 2025-10-30',
'$ balena api-key generate "Jenkins Key" never',
];
public static args = {
name: Args.string({
description: 'the API key name',
required: true,
}),
expiryDate: Args.string({
description:
'the expiry date of the API key as an ISO date string, or "never" for no expiry',
}),
};
public static authenticated = true;
@ -55,9 +72,61 @@ export default class GenerateCmd extends Command {
public async run() {
const { args: params } = await this.parse(GenerateCmd);
let expiryDateResponse: string | number | undefined = params.expiryDate;
let key;
try {
key = await getBalenaSdk().models.apiKey.create(params.name);
if (!expiryDateResponse) {
expiryDateResponse = await getCliForm().ask({
message: 'Please pick an expiry date for the API key',
type: 'list',
choices: [...durations, 'custom', 'never'].map((duration) => ({
name:
duration === 'never'
? 'No expiration'
: typeof duration === 'number'
? formatDuration(
intervalToDuration({
start: 0,
end: duration * 24 * 60 * 60 * 1000,
}),
)
: 'Custom expiration',
value: duration,
})),
});
}
let expiryDate: Date | null;
if (expiryDateResponse === 'never') {
expiryDate = null;
} else if (expiryDateResponse === 'custom') {
do {
expiryDate = parseISO(
await getCliForm().ask({
message:
'Please enter an expiry date for the API key as an ISO date string',
type: 'input',
}),
);
if (!isValid(expiryDate)) {
console.error('Invalid date format');
}
} while (!isValid(expiryDate));
} else if (typeof expiryDateResponse === 'string') {
expiryDate = parseISO(expiryDateResponse);
if (!isValid(expiryDate)) {
throw new Error(
'Invalid date format, please use a valid ISO date string',
);
}
} else {
expiryDate = new Date(
Date.now() + expiryDateResponse * 24 * 60 * 60 * 1000,
);
}
key = await getBalenaSdk().models.apiKey.create({
name: params.name,
expiryDate: expiryDate === null ? null : expiryDate.toISOString(),
});
} catch (e) {
if (e.name === 'BalenaNotLoggedIn') {
if (await isLoggedInWithJwt()) {

View File

@ -50,9 +50,9 @@ export default class RevokeCmd extends Command {
return;
}
await Promise.all(
apiKeyIds.map(
async (id) => await getBalenaSdk().models.apiKey.revoke(Number(id)),
),
apiKeyIds.map(async (id) => {
await getBalenaSdk().models.apiKey.revoke(Number(id));
}),
);
console.log('Successfully revoked the given API keys');
}

View File

@ -91,7 +91,7 @@ export default class DeviceDetectCmd extends Command {
try {
await docker.ping();
return true;
} catch (err) {
} catch {
return false;
}
}),

View File

@ -155,7 +155,7 @@ export default class DeviceInitCmd extends Command {
try {
logger.logDebug(`Process failed, removing device ${device.uuid}`);
await balena.models.device.remove(device.uuid);
} catch (e) {
} catch {
// Ignore removal failures, and throw original error
}
throw e;

View File

@ -135,7 +135,7 @@ export default class DeviceLogsCmd extends Command {
logger.logDebug('Checking we can access device');
try {
await deviceApi.ping();
} catch (e) {
} catch {
const { ExpectedError } = await import('../../errors');
throw new ExpectedError(
`Cannot access device at address ${params.device}. Device may not be in local mode.`,

View File

@ -20,6 +20,7 @@ import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy';
import type { Device } from 'balena-sdk';
import { ExpectedError } from '../../errors';
import { getExpandedProp } from '../../utils/pine';
export default class DeviceOsUpdateCmd extends Command {
public static description = stripIndent`
@ -126,20 +127,46 @@ export default class DeviceOsUpdateCmd extends Command {
);
}
} else {
const choices = await Promise.all(
hupVersionInfo.versions.map(async (version) => {
const takeoverRequired =
(await sdk.models.os.getOsUpdateType(
getExpandedProp(is_of__device_type, 'slug')!,
currentOsVersion,
version,
)) === 'takeover';
return {
name: `${version}${hupVersionInfo.recommended === version ? ' (recommended)' : ''}${takeoverRequired ? ' ADVANCED UPDATE: Requires disk re-partitioning with no rollback option' : ''}`,
value: version,
};
}),
);
targetOsVersion = await getCliForm().ask({
message: 'Target OS version',
type: 'list',
choices: hupVersionInfo.versions.map((version) => ({
name:
hupVersionInfo.recommended === version
? `${version} (recommended)`
: version,
value: version,
})),
choices,
});
}
const takeoverRequired =
(await sdk.models.os.getOsUpdateType(
getExpandedProp(is_of__device_type, 'slug')!,
currentOsVersion,
targetOsVersion,
)) === 'takeover';
const patterns = await import('../../utils/patterns');
// Warn the user if the update requires a takeover
if (takeoverRequired) {
await patterns.confirm(
options.yes || false,
stripIndent`Before you proceed, note that this update process is different from a regular HostOS Update:
DATA LOSS: This update requires disk re-partitioning, which will erase all data stored on the device.
NO ROLLBACK: Unlike our HostOS update mechanism, this process does not allow reverting to a previous version in case of failure.
Make sure to back up all important data before continuing. For more details, check our documentation: https://docs.balena.io/reference/OS/updates/update-process/
`,
);
}
// Confirm and start update
await patterns.confirm(
options.yes || false,

View File

@ -76,6 +76,6 @@ export default class DeviceRegisterCmd extends Command {
options.deviceType,
);
return result && result.uuid;
return result.uuid;
}
}

View File

@ -82,7 +82,7 @@ export default class DeviceSSHCmd extends Command {
SSH server port number (default 22222) if the target is an IP address or .local
hostname. Otherwise, port number for the balenaCloud gateway (default 22).`,
char: 'p',
parse: async (p) => parseAsInteger(p, 'port'),
parse: (p) => parseAsInteger(p, 'port'),
}),
tty: Flags.boolean({
default: false,
@ -110,13 +110,14 @@ export default class DeviceSSHCmd extends Command {
// Local connection
if (validateLocalHostnameOrIp(params.fleetOrDevice)) {
const { performLocalDeviceSSH } = await import('../../utils/device/ssh');
return await performLocalDeviceSSH({
await performLocalDeviceSSH({
hostname: params.fleetOrDevice,
port: options.port || 'local',
forceTTY: options.tty,
verbose: options.verbose,
service: params.service,
});
return;
}
// Remote connection
@ -132,7 +133,7 @@ export default class DeviceSSHCmd extends Command {
const useProxy = !!proxyConfig && !options.noproxy;
// this will be a tunnelled SSH connection...
await checkNotUsingOfflineMode();
checkNotUsingOfflineMode();
await checkLoggedIn();
const deviceUuid = await getOnlineTargetDeviceUuid(
sdk,

View File

@ -41,7 +41,7 @@ export default class EnvRenameCmd extends Command {
id: Args.integer({
required: true,
description: "variable's numeric database ID",
parse: async (input) => parseAsInteger(input, 'id'),
parse: (input) => parseAsInteger(input, 'id'),
}),
value: Args.string({
required: true,

View File

@ -46,7 +46,7 @@ export default class EnvRmCmd extends Command {
id: Args.integer({
required: true,
description: "variable's numeric database ID",
parse: async (input) => parseAsInteger(input, 'id'),
parse: (input) => parseAsInteger(input, 'id'),
}),
};

View File

@ -16,7 +16,6 @@
*/
import { Args, Command } from '@oclif/core';
import { promisify } from 'util';
import { stripIndent } from '../../utils/lazy';
export default class LocalConfigureCmd extends Command {
@ -237,7 +236,7 @@ export default class LocalConfigureCmd extends Command {
const bootPartition = await getBootPartition(target);
const files = await imagefs.interact(target, bootPartition, async (_fs) => {
return await promisify(_fs.readdir)(this.CONNECTIONS_FOLDER);
return await _fs.promises.readdir(this.CONNECTIONS_FOLDER);
});
let connectionFileName;
@ -246,13 +245,11 @@ export default class LocalConfigureCmd extends Command {
} else if (_.includes(files, 'resin-sample.ignore')) {
// Fresh image, new mode, accoding to https://github.com/balena-os/meta-balena/pull/770/files
await imagefs.interact(target, bootPartition, async (_fs) => {
const readFileAsync = promisify(_fs.readFile);
const writeFileAsync = promisify(_fs.writeFile);
const contents = await readFileAsync(
const contents = await _fs.promises.readFile(
`${this.CONNECTIONS_FOLDER}/resin-sample.ignore`,
{ encoding: 'utf8' },
);
return await writeFileAsync(
await _fs.promises.writeFile(
`${this.CONNECTIONS_FOLDER}/resin-wifi`,
contents,
);
@ -269,13 +266,13 @@ export default class LocalConfigureCmd extends Command {
} else {
// In case there's no file at all (shouldn't happen normally, but the file might have been removed)
await imagefs.interact(target, bootPartition, async (_fs) => {
return await promisify(_fs.writeFile)(
await _fs.promises.writeFile(
`${this.CONNECTIONS_FOLDER}/resin-wifi`,
this.CONNECTION_FILE,
);
});
}
return await this.getConfigurationSchema(bootPartition, connectionFileName);
return this.getConfigurationSchema(bootPartition, connectionFileName);
}
async removeHostname(schema: any) {

View File

@ -132,7 +132,7 @@ export default class LoginCmd extends Command {
// We can safely assume this won't be undefined as doLogin will throw if this call fails
// We also don't need to worry too much about the amount of calls to whoami
// as these are cached by the SDK
const whoamiResult = (await balena.auth.whoami()) as WhoamiResult;
const whoamiResult = (await balena.auth.whoami())!;
if (whoamiResult.actorType !== 'user' && !options.hideExperimentalWarning) {
console.info(stripIndent`
@ -168,7 +168,7 @@ ${messages.reachingOut}`);
async doLogin(
loginOptions: FlagsDef,
balenaUrl: string = 'balena-cloud.com',
balenaUrl = 'balena-cloud.com',
token?: string,
): Promise<void> {
// Token

View File

@ -18,7 +18,6 @@
import { Flags, Args, Command } from '@oclif/core';
import type { Interfaces } from '@oclif/core';
import type * as BalenaSdk from 'balena-sdk';
import { promisify } from 'util';
import * as _ from 'lodash';
import { ExpectedError } from '../../errors';
import * as cf from '../../utils/common-flags';
@ -292,7 +291,7 @@ export default class OsConfigureCmd extends Command {
for (const { name, content } of files) {
await imagefs.interact(image, bootPartition, async (_fs) => {
return await promisify(_fs.writeFile)(
await _fs.promises.writeFile(
path.join(CONNECTIONS_FOLDER, name),
content,
);

View File

@ -109,7 +109,7 @@ https://github.com/balena-io-examples/staged-releases\
'additional-space': Flags.integer({
description:
'expand the image by this amount of bytes instead of automatically estimating the required amount',
parse: async (x) => parseAsInteger(x, 'additional-space'),
parse: (x) => parseAsInteger(x, 'additional-space'),
}),
'add-certificate': Flags.string({
description: `\
@ -126,7 +126,7 @@ Can be repeated to add multiple certificates.\
dockerPort: Flags.integer({
description:
'Docker daemon TCP port number (hint: 2375 for balena devices)',
parse: async (p) => parseAsInteger(p, 'dockerPort'),
parse: (p) => parseAsInteger(p, 'dockerPort'),
}),
};
@ -155,7 +155,7 @@ Can be repeated to add multiple certificates.\
------------------------------------------------------------------------------
`);
}
} catch (error) {
} catch {
throw new ExpectedError(
`The provided image path does not exist: ${params.image}`,
);
@ -192,11 +192,11 @@ Can be repeated to add multiple certificates.\
event.name,
));
if (event.action === 'start') {
return spinner.start();
} else {
console.log();
return spinner.stop();
spinner.start();
return;
}
console.log();
spinner.stop();
};
const commit = this.isCurrentCommit(options.commit || '')
@ -295,7 +295,7 @@ Can be repeated to add multiple certificates.\
owns__release: {
$select: ['id', 'commit', 'end_timestamp', 'composition'],
$expand: {
contains__image: {
release_image: {
$select: ['image'],
$expand: {
image: {

View File

@ -24,7 +24,7 @@ import { tryAsInteger } from '../../utils/validation';
import { jsonInfo } from '../../utils/messages';
export const commitOrIdArg = Args.custom({
parse: async (commitOrId: string) => tryAsInteger(commitOrId),
parse: tryAsInteger,
});
type FlagsDef = Interfaces.InferredFlags<typeof ReleaseCmd.flags>;
@ -86,7 +86,7 @@ export default class ReleaseCmd extends Command {
balena: BalenaSdk.BalenaSDK,
options: FlagsDef,
) {
const fields: Array<keyof BalenaSdk.Release> = [
const fields = [
'id',
'commit',
'created_at',
@ -96,7 +96,7 @@ export default class ReleaseCmd extends Command {
'build_log',
'start_timestamp',
'end_timestamp',
];
] satisfies BalenaSdk.PineOptions<BalenaSdk.Release>['$select'];
const release = await balena.models.release.get(commitOrId, {
...(!options.json && { $select: fields }),

View File

@ -56,14 +56,14 @@ export default class ReleaseListCmd extends Command {
public async run() {
const { args: params, flags: options } = await this.parse(ReleaseListCmd);
const fields: Array<keyof BalenaSdk.Release> = [
const fields = [
'id',
'commit',
'created_at',
'status',
'semver',
'is_final',
];
] satisfies BalenaSdk.PineOptions<BalenaSdk.Release>['$select'];
const balena = getBalenaSdk();
const { getFleetSlug } = await import('../../utils/sdk');

View File

@ -34,7 +34,7 @@ export default class SSHKeyCmd extends Command {
public static args = {
id: Args.integer({
description: 'balenaCloud ID for the SSH key',
parse: async (x) => parseAsInteger(x, 'id'),
parse: (x) => parseAsInteger(x, 'id'),
required: true,
}),
};

View File

@ -40,7 +40,7 @@ export default class SSHKeyRmCmd extends Command {
public static args = {
id: Args.integer({
description: 'balenaCloud ID for the SSH key',
parse: async (x) => parseAsInteger(x, 'id'),
parse: (x) => parseAsInteger(x, 'id'),
required: true,
}),
};

View File

@ -69,10 +69,9 @@ export default class VersionCmd extends Command {
const { flags: options } = await this.parse(VersionCmd);
const versions: JsonVersions = {
'balena-cli': (await import('../../../package.json')).version,
'Node.js':
process.version && process.version.startsWith('v')
? process.version.slice(1)
: process.version,
'Node.js': process.version.startsWith('v')
? process.version.slice(1)
: process.version,
};
if (options.json) {
console.log(JSON.stringify(versions, null, 4));

View File

@ -93,7 +93,7 @@ function interpret(error: Error): string {
if (hasCode(error)) {
const errorCodeHandler = messages[error.code];
const message = errorCodeHandler && errorCodeHandler(error);
const message = errorCodeHandler?.(error);
if (message) {
return message;
@ -229,7 +229,7 @@ async function sentryCaptureException(error: Error) {
Sentry.captureException(error);
try {
await Sentry.close(1000);
} catch (e) {
} catch {
if (process.env.DEBUG) {
console.error('[debug] Timeout reporting error to sentry.io');
}

View File

@ -209,12 +209,12 @@ See: https://git.io/JRHUW#deprecation-policy`,
return indent(body, 2);
}
protected formatDescription(desc: string = '') {
protected formatDescription(desc = '') {
const chalk = getChalk();
desc = desc.split('\n')[0];
// Remove any ending .
if (desc[desc.length - 1] === '.') {
if (desc.endsWith('.')) {
desc = desc.substring(0, desc.length - 1);
}
// Lowercase first letter if second char is lowercase, to preserve e.g. 'SSH ...')

View File

@ -103,7 +103,7 @@ const hook: Hook<'prerun'> = async function (options) {
.offlineCompatible ?? DEFAULT_OFFLINE_COMPATIBLE
)
) {
await checkNotUsingOfflineMode();
checkNotUsingOfflineMode();
}
} catch (error) {
this.error(error);

View File

@ -48,7 +48,7 @@ export async function preparseArgs(argv: string[]): Promise<string[]> {
if (
cmdSlice.length > 1 &&
cmdSlice[0] === 'help' &&
cmdSlice[1][0] !== '-'
!cmdSlice[1].startsWith('-')
) {
cmdSlice.shift();
cmdSlice.push('--help');

View File

@ -164,9 +164,8 @@ export async function downloadOSImage(
stream.on('progress', (state: any) => {
if (state != null) {
return bar.update(state);
} else {
return spinner.start();
}
spinner.start();
});
stream.on('end', () => {

View File

@ -240,7 +240,7 @@ export const getPreviousRepos = (
status: 'success',
},
$expand: {
contains__image: {
release_image: {
$select: 'image',
$expand: { image: { $select: 'is_stored_at__image_location' } },
},
@ -252,7 +252,7 @@ export const getPreviousRepos = (
.then(function (release) {
// grab all images from the latest release, return all image locations in the registry
if (release.length > 0) {
const images = release[0].contains__image as Array<{
const images = release[0].release_image as Array<{
image: [SDK.Image];
}>;
const { getRegistryAndName } =
@ -386,7 +386,7 @@ export class BuildProgressUI implements Renderer {
.map(function (service) {
const stream = through.obj(function (event, _enc, cb) {
eventHandler(service, event);
return cb();
cb();
});
stream.pipe(tty.stream, { end: false });
return [service, stream];
@ -471,17 +471,20 @@ export class BuildProgressUI implements Renderer {
const { status, progress, error } = serviceToDataMap[service] ?? {};
if (error) {
return `${error}`;
} else if (progress) {
}
if (progress) {
const bar = renderProgressBar(progress, 20);
if (status) {
return `${bar} ${status}`;
}
return `${bar}`;
} else if (status) {
return `${status}`;
} else {
return 'Waiting...';
return bar;
}
if (status) {
return status;
}
return 'Waiting...';
})
.map((data, index) => [services[index], data])
.fromPairs()
@ -552,7 +555,7 @@ export class BuildProgressInline implements Renderer {
.map(function (service) {
const stream = through.obj(function (event, _enc, cb) {
eventHandler(service, event);
return cb();
cb();
});
stream.pipe(outStream, { end: false });
return [service, stream];
@ -606,11 +609,11 @@ export class BuildProgressInline implements Renderer {
const { status, error } = event;
if (error) {
return `${error}`;
} else if (status) {
return `${status}`;
} else {
return 'Waiting...';
}
if (status) {
return status;
}
return 'Waiting...';
})();
const prefix = _.padEnd(getChalk().bold(service), this._prefixWidth);

View File

@ -966,7 +966,7 @@ export async function makeBuildTasks(
deviceInfo: DeviceInfo,
logger: Logger,
projectName: string,
releaseHash: string = 'unavailable',
releaseHash = 'unavailable',
preprocessHook?: (dockerfile: string) => string,
): Promise<MultiBuild.BuildTask[]> {
const multiBuild = await import('@balena/compose/dist/multibuild');
@ -1492,7 +1492,7 @@ export function createRunLoop(tick: (...args: any[]) => void) {
},
end() {
clearInterval(timerId);
return runloop.onEnd();
runloop.onEnd();
},
};
return runloop;
@ -1549,7 +1549,7 @@ function dropEmptyLinesStream() {
if (str.trim()) {
this.push(str);
}
return cb();
cb();
});
}
@ -1570,7 +1570,7 @@ function buildLogCapture(objectMode: boolean, buffer: string[]) {
buffer.push(data);
}
return cb(null, data);
cb(null, data);
});
}
@ -1585,14 +1585,16 @@ function buildProgressAdapter(inline: boolean) {
return through({ objectMode: true }, function (str, _enc, cb) {
if (str == null) {
return cb(null, str);
cb(null, str);
return;
}
if (inline) {
return cb(null, { status: str });
cb(null, { status: str });
return;
}
if (!/^Successfully tagged /.test(str)) {
if (!str.startsWith('Successfully tagged ')) {
const match = stepRegex.exec(str);
if (match) {
step = match[1];
@ -1607,7 +1609,7 @@ function buildProgressAdapter(inline: boolean) {
}
}
return cb(null, { status: str, progress });
cb(null, { status: str, progress });
});
}

View File

@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import type * as BalenaSdk from 'balena-sdk';
import * as semver from 'balena-semver';
import { getBalenaSdk, stripIndent } from './lazy';
export interface ImgConfig {
@ -81,7 +80,7 @@ export async function generateApplicationConfig(
)) as ImgConfig;
// merge sshKeys to config, when they have been specified
if (options.os && options.os.sshKeys) {
if (options.os?.sshKeys) {
// Create config.os object if it does not exist
config.os = config.os ? config.os : {};
config.os.sshKeys = config.os.sshKeys
@ -122,16 +121,10 @@ export function generateDeviceConfig(
// os.getConfig always returns a config for an app
delete config.apiKey;
if (deviceApiKey == null && semver.satisfies(options.version, '<2.0.3')) {
config.apiKey = await sdk.models.application.generateApiKey(
application.id,
);
} else {
config.deviceApiKey =
typeof deviceApiKey === 'string' && deviceApiKey
? deviceApiKey
: await sdk.models.device.generateDeviceKey(device.uuid);
}
config.deviceApiKey =
typeof deviceApiKey === 'string' && deviceApiKey
? deviceApiKey
: await sdk.models.device.generateDeviceKey(device.uuid);
return config;
})

View File

@ -86,7 +86,7 @@ const uploadToPromise = (uploadRequest: Request, logger: Logger) =>
obj = JSON.parse(data);
} catch (e) {
logger.logError('Error parsing reply from remote side');
reject(e);
reject(e as Error);
return;
}
@ -232,7 +232,7 @@ export const deployLegacy = async function (
username,
appName,
]);
await uploadLogs(...args);
uploadLogs(...args);
}
return buildId;

View File

@ -74,7 +74,7 @@ export class DeviceAPI {
public constructor(
private logger: Logger,
addr: string,
port: number = 48484,
port = 48484,
) {
this.deviceAddress = `http://${addr}:${port}/`;
}
@ -201,7 +201,7 @@ export class DeviceAPI {
return new Promise((resolve, reject) => {
const req = request.get(url);
req.on('error', reject).on('response', async (res) => {
req.on('error', reject).on('response', (res) => {
if (res.statusCode !== 200) {
reject(
new ApiErrors.DeviceAPIError(
@ -213,7 +213,7 @@ export class DeviceAPI {
try {
res.socket.setKeepAlive(true, 1000);
} catch (error) {
reject(error);
reject(error as Error);
}
resolve(res);
});
@ -238,7 +238,7 @@ export class DeviceAPI {
if (_.isObject(opts) && (opts as ObjectWithUrl).url != null) {
// the `as string` shouldn't be necessary, but the type system
// is getting a little confused
url = (opts as ObjectWithUrl).url as string;
url = (opts as ObjectWithUrl).url!;
} else if (typeof opts === 'string') {
url = opts;
}
@ -252,21 +252,26 @@ export class DeviceAPI {
return await new Promise((resolve, reject) => {
return request(opts, (err, response, body) => {
if (err) {
return reject(err);
reject(err as Error);
return;
}
switch (response.statusCode) {
case 200:
return resolve(body);
case 400:
return reject(
new ApiErrors.BadRequestDeviceAPIError(body.message),
);
case 503:
return reject(
new ApiErrors.ServiceUnavailableAPIError(body.message),
);
default:
return reject(new ApiErrors.DeviceAPIError(body.message));
case 200: {
resolve(body);
return;
}
case 400: {
reject(new ApiErrors.BadRequestDeviceAPIError(body.message));
return;
}
case 503: {
reject(new ApiErrors.ServiceUnavailableAPIError(body.message));
return;
}
default: {
reject(new ApiErrors.DeviceAPIError(body.message));
return;
}
}
});
});

View File

@ -74,11 +74,11 @@ interface ParsedEnvironment {
[serviceName: string]: { [key: string]: string };
}
async function environmentFromInput(
function environmentFromInput(
envs: string[],
serviceNames: string[],
logger: Logger,
): Promise<ParsedEnvironment> {
): ParsedEnvironment {
// A normal environment variable regex, with an added part
// to find a colon followed servicename at the start
const varRegex = /^(?:([^\s:]+):)?([^\s]+?)=(.*)$/;
@ -143,7 +143,7 @@ export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
try {
globalLogger.logDebug('Checking we can access device');
await api.ping();
} catch (e) {
} catch {
throw new ExpectedError(stripIndent`
Could not communicate with device supervisor at address ${opts.deviceHost}:${port}.
Device may not have local mode enabled. Check with:
@ -191,10 +191,7 @@ export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
});
// Attempt to attach to the device's docker daemon
const docker = connectToDocker(
opts.deviceHost,
opts.devicePort != null ? opts.devicePort : 2375,
);
const docker = connectToDocker(opts.deviceHost, opts.devicePort ?? 2375);
await checkBuildSecretsRequirements(docker, opts.source);
globalLogger.logDebug('Tarring all non-ignored files...');
@ -231,7 +228,7 @@ export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
// Print a newline to clearly separate build time and runtime
console.log();
const envs = await environmentFromInput(
const envs = environmentFromInput(
opts.env,
Object.getOwnPropertyNames(project.composition.services),
globalLogger,
@ -388,7 +385,7 @@ async function performBuilds(
);
// Check for failures
await inspectBuildResults(localImages);
inspectBuildResults(localImages);
const imagesToRemove: string[] = [];
@ -497,7 +494,7 @@ export async function rebuildSingleTask(
}
await assignDockerBuildOpts(docker, [task], opts);
await assignOutputHandlers([task], logger, logHandler);
assignOutputHandlers([task], logger, logHandler);
const [localImage] = await multibuild.performBuilds(
[task],
@ -568,7 +565,7 @@ async function assignDockerBuildOpts(
globalLogger.logDebug(`Using ${images.length} on-device images for cache...`);
await Promise.all(
buildTasks.map(async (task: BuildTask) => {
buildTasks.map((task: BuildTask) => {
task.dockerOpts = {
...(task.dockerOpts || {}),
...{
@ -666,7 +663,7 @@ export function generateTargetState(
return targetState;
}
async function inspectBuildResults(images: LocalImage[]): Promise<void> {
function inspectBuildResults(images: LocalImage[]): void {
const failures: LocalPushErrors.BuildFailure[] = [];
_.each(images, (image) => {
@ -679,6 +676,6 @@ async function inspectBuildResults(images: LocalImage[]): Promise<void> {
});
if (failures.length > 0) {
throw new LocalPushErrors.BuildError(failures).toString();
throw new LocalPushErrors.BuildError(failures);
}
}

View File

@ -191,8 +191,8 @@ export class LivepushManager {
);
const eventQueue = this.updateEventsWaiting[$serviceName];
eventQueue.push(changedPath);
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.getDebouncedEventHandler($serviceName)();
void this.getDebouncedEventHandler($serviceName)();
};
const monitor = this.setupFilesystemWatcher(
@ -252,7 +252,7 @@ export class LivepushManager {
try {
// sync because chokidar defines a sync interface
stats = fs.lstatSync(filePath);
} catch (err) {
} catch {
// OK: the file may have been deleted. See also:
// https://github.com/paulmillr/chokidar/blob/3.4.3/lib/fsevents-handler.js#L326-L328
// https://github.com/paulmillr/chokidar/blob/3.4.3/lib/nodefs-handler.js#L364
@ -267,15 +267,15 @@ export class LivepushManager {
return dockerignore.ignores(relPath);
},
});
monitor.on('add', (changedPath: string) =>
changedPathHandler(serviceName, changedPath),
);
monitor.on('change', (changedPath: string) =>
changedPathHandler(serviceName, changedPath),
);
monitor.on('unlink', (changedPath: string) =>
changedPathHandler(serviceName, changedPath),
);
monitor.on('add', (changedPath: string) => {
changedPathHandler(serviceName, changedPath);
});
monitor.on('change', (changedPath: string) => {
changedPathHandler(serviceName, changedPath);
});
monitor.on('unlink', (changedPath: string) => {
changedPathHandler(serviceName, changedPath);
});
return monitor;
}

View File

@ -57,7 +57,7 @@ export const dockerConnectionCliFlags = {
description:
'Docker daemon TCP port number (hint: 2375 for balena devices)',
char: 'p',
parse: async (p) => parseAsInteger(p, 'dockerPort'),
parse: (p) => parseAsInteger(p, 'dockerPort'),
}),
ca: Flags.string({
description: 'Docker host TLS certificate authority file',
@ -169,9 +169,7 @@ export async function isBalenaEngine(docker: dockerode): Promise<boolean> {
// version of balenaEngine, but it was at one point (mis)spelt 'balaena':
// https://github.com/balena-os/balena-engine/pull/32/files
const dockerVersion = (await docker.version()) as BalenaEngineVersion;
return !!(
dockerVersion.Engine && dockerVersion.Engine.match(/balena|balaena/)
);
return !!dockerVersion.Engine?.match(/balena|balaena/);
}
export async function getDocker(

View File

@ -84,7 +84,7 @@ export async function readFileWithEolConversion(
}
// Analyse encoding
const encoding = await detectEncoding(fileBuffer);
const encoding = detectEncoding(fileBuffer);
// Skip further processing of non-convertible encodings
if (!CONVERTIBLE_ENCODINGS.includes(encoding)) {
@ -132,10 +132,10 @@ export async function readFileWithEolConversion(
* @param fileBuffer File contents whose encoding should be detected
* @param bytesRead Optional "file size" if smaller than the buffer size
*/
export async function detectEncoding(
export function detectEncoding(
fileBuffer: Buffer,
bytesRead = fileBuffer.length,
): Promise<string> {
): string {
// empty file
if (bytesRead === 0) {
return '';

View File

@ -110,6 +110,27 @@ export async function getManifest(
const init = await import('balena-device-init');
const sdk = getBalenaSdk();
const manifest = await init.getImageManifest(image);
if (manifest != null) {
const config = manifest.configuration?.config;
if (config?.partition != null) {
const { getBootPartition } = await import('balena-config-json');
// Find the device-type.json property that holds the boot partition number for
// this device type (config.partition or config.partition.primary) and overwrite it
// with the boot partition number that was found by inspecting the image.
// since it's deprecated & no longer updated for newer releases.
if (typeof config.partition === 'number') {
config.partition = await getBootPartition(image);
} else if (config.partition.primary != null) {
config.partition.primary = await getBootPartition(image);
}
// TODO: Add handling for when we no longer include a `config.partition` at all.
}
} else {
// TODO: Change this in the next major to throw, after confirming that this works for all supported OS versions.
console.error(
`[warn] Error while finding a device-type.json on the provided image path. Attempting to fetch from the API.`,
);
}
if (
manifest != null &&
manifest.slug !== deviceType &&
@ -281,8 +302,7 @@ export function isWindowsComExeShell() {
// neither bash nor sh (e.g. not MSYS, MSYS2, Cygwin, WSL)
process.env.SHELL == null &&
// Windows cmd.exe or PowerShell
process.env.ComSpec != null &&
process.env.ComSpec.endsWith('cmd.exe')
process.env.ComSpec?.endsWith('cmd.exe')
);
}
@ -366,7 +386,7 @@ export function getProxyConfig(): ProxyConfig | undefined {
let url: InstanceType<typeof URL>;
try {
url = new URL(proxyUrl);
} catch (_e) {
} catch {
return;
}
return {
@ -469,7 +489,7 @@ export function pickAndRename<T extends Dictionary<any>>(
let renameFrom = f;
let renameTo = f;
const match = f.match(/(?<from>\S+)\s+=>\s+(?<to>\S+)/);
if (match && match.groups) {
if (match?.groups) {
renameFrom = match.groups.from;
renameTo = match.groups.to;
}

View File

@ -76,38 +76,28 @@ export const getImagePath = async (deviceType: string, version?: string) => {
};
/**
* @summary Determine if a device image is fresh
* @summary Determine if a device image is cached
*
* @description
* If the device image does not exist, return false.
*
* @param {String} deviceType - device type slug or alias
* @param {String} version - the exact balenaOS version number
* @returns {Promise<Boolean>} is image fresh
* @returns {Promise<Boolean>} is image cached
*
* @example
* isImageFresh('raspberry-pi', '1.2.3').then (isFresh) ->
* if isFresh
* console.log('The Raspberry Pi image v1.2.3 is fresh!')
* isImageCached ('raspberry-pi', '1.2.3').then (isCached) ->
* if isCached
* console.log('The Raspberry Pi image v1.2.3 is cached!')
*/
export const isImageFresh = async (deviceType: string, version: string) => {
export const isImageCached = async (deviceType: string, version: string) => {
const imagePath = await getImagePath(deviceType, version);
let createdDate;
try {
createdDate = await getFileCreatedDate(imagePath);
const createdDate = await getFileCreatedDate(imagePath);
return createdDate != null;
} catch {
// Swallow errors from getFileCreatedTime.
}
if (createdDate == null) {
return false;
}
const balena = getBalenaSdk();
const lastModifiedDate = await balena.models.os.getLastModified(
deviceType,
version,
);
return lastModifiedDate < createdDate;
};
/**
@ -118,7 +108,7 @@ export const isImageFresh = async (deviceType: string, version: string) => {
*/
export const isESR = (version: string) => {
const match = version.match(/^v?(\d+)\.\d+\.\d+/);
const major = parseInt((match && match[1]) || '', 10);
const major = parseInt(match?.[1] || '', 10);
return major >= 2018; // note: (NaN >= 2018) is false
};
@ -286,7 +276,7 @@ export const getStream = async (
versionOrRange = 'latest';
}
const version = await resolveVersion(deviceType, versionOrRange);
const isFresh = await isImageFresh(deviceType, version);
const isFresh = await isImageCached(deviceType, version);
const $stream = isFresh
? await getImage(deviceType, version)
: await doDownload({ ...options, deviceType, version });

View File

@ -21,6 +21,7 @@ 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 '@oclif/core';
import { version } from '../../package.json';
// Equivalent of _.once but avoiding the need to import lodash for lazy deps
const once = <T>(fn: () => T) => {
@ -43,9 +44,26 @@ export const onceAsync = <T>(fn: () => Promise<T>) => {
};
};
export const getBalenaSdk = once(() =>
(require('balena-sdk') as typeof BalenaSdk).fromSharedOptions(),
);
const cliXBalenaClientHeaderInterceptor: BalenaSdk.Interceptor = {
request($request) {
if ($request.headers['X-Balena-Client']) {
// We intentionally overwrite the sdk version string from the header
// to conserve bandwidth. We only do that when the SDK already has specified
// the X-Balena-Client header, since that signals that this is a safe url to
// include the extra header and will not cause CORS errors.
$request.headers['X-Balena-Client'] = `balena-cli/${version}`;
}
return $request;
},
};
export const getBalenaSdk = once(() => {
const sdk = (require('balena-sdk') as typeof BalenaSdk).fromSharedOptions();
if (!sdk.interceptors.includes(cliXBalenaClientHeaderInterceptor)) {
sdk.interceptors.push(cliXBalenaClientHeaderInterceptor);
}
return sdk;
});
export const getVisuals = once(
() => require('resin-cli-visuals') as typeof visuals,

View File

@ -39,7 +39,7 @@ export async function disambiguateReleaseParam(
// Accepting short hashes of 7,8,9 chars.
const possibleUuidHashLength = [7, 8, 9, 32, 40, 62].includes(release.length);
const hasLeadingZero = release[0] === '0';
const hasLeadingZero = release.startsWith('0');
const isOnlyNumerical = /^[0-9]+$/.test(release);
// Reject non-numerical values with invalid uuid/hash lengths
@ -75,17 +75,19 @@ export async function disambiguateReleaseParam(
return (await balena.models.release.get(release, { $select: 'id' })).id;
}
/* eslint-disable @typescript-eslint/require-await -- oclif parse functions require a Promise return */
/**
* Convert to lowercase if looks like slug
*/
export async function lowercaseIfSlug(s: string) {
/* eslint-enable @typescript-eslint/require-await */
return s.includes('/') ? s.toLowerCase() : s;
}
export function normalizeOsVersion(version: string) {
// Note that `version` may also be 'latest', 'recommended', 'default'
if (/^v?\d+\.\d+\.\d+/.test(version)) {
if (version[0] === 'v') {
if (version.startsWith('v')) {
version = version.slice(1);
}
}

View File

@ -25,7 +25,7 @@ export function capitanoizeOclifUsage(
.toLowerCase();
}
export async function getCommandsFromManifest() {
export function getCommandsFromManifest() {
const manifest = require('../../oclif.manifest.json');
if (manifest.commands == null) {

View File

@ -119,7 +119,7 @@ export const checkLoggedInIf = async (doCheck: boolean) => {
*
* @throws {NotAvailableInOfflineModeError}
*/
export const checkNotUsingOfflineMode = async () => {
export const checkNotUsingOfflineMode = () => {
if (process.env.BALENARC_OFFLINE_MODE) {
throw new NotAvailableInOfflineModeError(stripIndent`
This command requires an internet connection, and cannot be used in offline mode.

View File

@ -390,13 +390,12 @@ async function createApplication(
try {
const userInfo = await sdk.auth.getUserInfo();
username = userInfo.username;
} catch (err) {
} catch {
throw new sdk.errors.BalenaNotLoggedIn();
}
// eslint-disable-next-line no-async-promise-executor
const applicationName = await new Promise<string>(async (resolve, reject) => {
// eslint-disable-next-line no-constant-condition
while (true) {
try {
const appName = await getCliForm().ask({
@ -418,11 +417,13 @@ async function createApplication(
'You already have a fleet with that name; please choose another.',
);
continue;
} catch (err) {
return resolve(appName);
} catch {
resolve(appName);
return;
}
} catch (err) {
return reject(err);
reject(err as Error);
return;
}
}
});
@ -452,9 +453,7 @@ async function generateApplicationConfig(
const manifest = await sdk.models.config.getDeviceTypeManifestBySlug(
app.is_for__device_type[0].slug,
);
const opts =
manifest.options &&
manifest.options.filter((opt) => opt.name !== 'network');
const opts = manifest.options?.filter((opt) => opt.name !== 'network');
const override = {
appUpdatePollInterval: options.appUpdatePollInterval,

View File

@ -50,7 +50,7 @@ export function copyQemu(context: string, arch: string) {
.then(() => getQemuPath(arch))
.then(
(qemu) =>
new Promise(function (resolve, reject) {
new Promise<void>(function (resolve, reject) {
const read = fs.createReadStream(qemu);
const write = fs.createWriteStream(binPath);
@ -114,7 +114,7 @@ async function installQemu(arch: string, qemuPath: string) {
stream.resume();
}
} catch (err) {
reject(err);
reject(err as Error);
}
});
request(qemuUrl)

View File

@ -110,7 +110,7 @@ export async function startRemoteBuild(
const [buildRequest, stream] = await getRemoteBuildStream(build);
// Setup CTRL-C handler so the user can interrupt the build
let cancellationPromise = Promise.resolve();
let cancellationPromise: Promise<void> | undefined;
const sigintHandler = () => {
process.exitCode = 130;
console.error('\nReceived SIGINT, cleaning up. Please wait.');
@ -246,7 +246,8 @@ function getBuilderMessageHandler(
console.error(`[debug] handling message: ${JSON.stringify(obj)}`);
}
if (obj.type != null && obj.type === 'metadata') {
return handleBuilderMetadata(obj, build);
handleBuilderMetadata(obj, build);
return;
}
if (obj.message) {
readline.clearLine(process.stdout, 0);
@ -423,10 +424,20 @@ async function getRemoteBuildStream(
stream = buildRequest.pipe(JSONStream.parse('*')) as NodeJS.ReadStream;
}
stream = stream
.once('error', () => uploadSpinner.stop())
.once('close', () => uploadSpinner.stop())
.once('data', () => uploadSpinner.stop())
.once('end', () => uploadSpinner.stop())
.once('finish', () => uploadSpinner.stop());
.once('error', () => {
uploadSpinner.stop();
})
.once('close', () => {
uploadSpinner.stop();
})
.once('data', () => {
uploadSpinner.stop();
})
.once('end', () => {
uploadSpinner.stop();
})
.once('finish', () => {
uploadSpinner.stop();
});
return [buildRequest, stream];
}

View File

@ -73,7 +73,7 @@ export function sshArgsForRemoteCommand({
...['-o', 'LogLevel=ERROR'],
...['-o', 'StrictHostKeyChecking=no'],
...['-o', 'UserKnownHostsFile=/dev/null'],
...(proxyCommand && proxyCommand.length
...(proxyCommand?.length
? ['-o', `ProxyCommand=${proxyCommand.join(' ')}`]
: []),
`${username}@${hostname}`,
@ -155,9 +155,9 @@ export async function runRemoteCommand({
[exitCode, exitSignal] = await new Promise((resolve, reject) => {
const ps = spawn(program, args, { stdio })
.on('error', reject)
.on('close', (code, signal) =>
resolve([code ?? undefined, signal ?? undefined]),
);
.on('close', (code, signal) => {
resolve([code ?? undefined, signal ?? undefined]);
});
if (ps.stdin && stdin && typeof stdin !== 'string') {
stdin.pipe(ps.stdin);
@ -272,7 +272,7 @@ export async function getLocalDeviceCmdStdout(
export const isRootUserGood = _.memoize(async (hostname: string, port) => {
try {
await runRemoteCommand({ cmd: 'exit 0', hostname, port, ...stdioIgnore });
} catch (e) {
} catch {
return false;
}
return true;

View File

@ -29,7 +29,11 @@ export function buffer(
new Promise(function (resolve, reject) {
const fstream = fs.createReadStream(bufferFile);
fstream.on('open', () => resolve(fstream)).on('error', reject);
fstream
.on('open', () => {
resolve(fstream);
})
.on('error', reject);
}),
);
}

View File

@ -105,7 +105,7 @@ async function spawnAndPipe(
});
}
async function windosuExec(
function windosuExec(
escapedArgs: string[],
stderr?: NodeJS.WritableStream,
): Promise<void> {

View File

@ -42,9 +42,9 @@ export = (stream: NodeJS.WriteStream = process.stdout) => {
const showCursor = () => stream.write('\u001B[?25h');
const cursorUp = (rows: number = 0) => stream.write(`\u001B[${rows}A`);
const cursorUp = (rows = 0) => stream.write(`\u001B[${rows}A`);
const cursorDown = (rows: number = 0) => stream.write(`\u001B[${rows}B`);
const cursorDown = (rows = 0) => stream.write(`\u001B[${rows}B`);
const write = (str: string) => stream.write(str);

View File

@ -61,11 +61,11 @@ export const tunnelConnectionToDevice = (
client.pipe(remote);
remote.pipe(client);
remote.on('error', (err) => {
console.error('Remote: ' + err);
console.error(`Remote: ${err}`);
client.end();
});
client.on('error', (err) => {
console.error('Client: ' + err);
console.error(`Client: ${err}`);
remote.end();
});
remote.on('close', () => {

View File

@ -87,7 +87,8 @@ export function looksLikeInteger(input: string) {
return /^(?:0|[1-9][0-9]*)$/.test(input);
}
export function parseAsInteger(input: string, paramName?: string) {
// eslint-disable-next-line @typescript-eslint/require-await -- oclif parse functions require a Promise return
export async function parseAsInteger(input: string, paramName?: string) {
if (!looksLikeInteger(input)) {
const message =
paramName == null
@ -100,14 +101,15 @@ export function parseAsInteger(input: string, paramName?: string) {
return Number(input);
}
export function tryAsInteger(input: string): number | string {
export async function tryAsInteger(input: string): Promise<number | string> {
try {
return parseAsInteger(input);
return await parseAsInteger(input);
} catch {
return input;
}
}
// eslint-disable-next-line @typescript-eslint/require-await -- oclif parse functions require a Promise return
export async function parseAsLocalHostnameOrIp(input: string) {
if (input && !validateLocalHostnameOrIp(input)) {
throw new ExpectedError(

View File

@ -31,7 +31,7 @@ chai.use(chaiAsPromised);
const { expect } = chai;
async function getPage(name: string): Promise<string> {
function getPage(name: string): string {
const pagePath = path.join(
__dirname,
'..',
@ -88,7 +88,7 @@ describe('Login server:', function () {
expect(body).to.equal(opt.expectedBody);
resolve();
} catch (err) {
reject(err);
reject(err as Error);
}
},
);
@ -134,7 +134,7 @@ describe('Login server:', function () {
describe('given the token authenticates with the server', function () {
beforeEach(function () {
this.loginIfTokenValidStub = sinon.stub(utils, 'loginIfTokenValid');
this.loginIfTokenValidStub.returns(Promise.resolve(true));
this.loginIfTokenValidStub.resolves(true);
});
afterEach(function () {
@ -143,7 +143,7 @@ describe('Login server:', function () {
it('should eventually be the token', async () => {
await testLogin({
expectedBody: await getPage('success'),
expectedBody: getPage('success'),
expectedStatusCode: 200,
expectedToken: tokens.johndoe.token,
});
@ -153,7 +153,7 @@ describe('Login server:', function () {
describe('given the token does not authenticate with the server', function () {
beforeEach(function () {
this.loginIfTokenValidStub = sinon.stub(utils, 'loginIfTokenValid');
return this.loginIfTokenValidStub.returns(Promise.resolve(false));
return this.loginIfTokenValidStub.resolves(false);
});
afterEach(function () {
@ -162,7 +162,7 @@ describe('Login server:', function () {
it('should be rejected', async () => {
await testLogin({
expectedBody: await getPage('error'),
expectedBody: getPage('error'),
expectedStatusCode: 401,
expectedToken: tokens.johndoe.token,
expectedErrorMsg: 'Invalid token',
@ -171,7 +171,7 @@ describe('Login server:', function () {
it('should be rejected if no token', async () => {
await testLogin({
expectedBody: await getPage('error'),
expectedBody: getPage('error'),
expectedStatusCode: 401,
expectedToken: '',
expectedErrorMsg: 'No token',
@ -180,7 +180,7 @@ describe('Login server:', function () {
it('should be rejected if token is malformed', async () => {
await testLogin({
expectedBody: await getPage('error'),
expectedBody: getPage('error'),
expectedStatusCode: 401,
expectedToken: 'asdf',
expectedErrorMsg: 'Invalid token',

View File

@ -267,15 +267,15 @@ describe('balena build', function () {
...fsMod,
promises: {
...fsMod.promises,
access: async (p: string) =>
access: (p: string) =>
p === qemuBinPath ? undefined : fsMod.promises.access(p),
stat: async (p: string) =>
stat: (p: string) =>
p === qemuBinPath ? { size: 1 } : fsMod.promises.stat(p),
},
});
mock(qemuModPath, {
...qemuMod,
copyQemu: async () => '',
copyQemu: () => '',
});
mock.reRequire('../../build/utils/qemu');
docker.expectGetInfo({ OperatingSystem: 'balenaOS 2.44.0+rev1' });

View File

@ -169,8 +169,12 @@ async function startMockSshServer(): Promise<[Server, number]> {
console.error(`[debug] mock ssh server: ${msg}`);
}
};
c.on('error', (err) => handler(err.message));
c.on('end', () => handler('client disconnected'));
c.on('error', (err) => {
handler(err.message);
});
c.on('end', () => {
handler('client disconnected');
});
c.end();
});
server.on('error', (err) => {

View File

@ -21,6 +21,8 @@ import * as process from 'process';
import { runCommand } from '../../helpers';
import { promisify } from 'util';
import * as tmp from 'tmp';
import type * as $imagefs from 'balena-image-fs';
import * as stripIndent from 'common-tags/lib/stripIndent';
tmp.setGracefulCleanup();
const tmpNameAsync = promisify(tmp.tmpName);
@ -29,30 +31,94 @@ import { BalenaAPIMock } from '../../nock/balena-api-mock';
if (process.platform !== 'win32') {
describe('balena os configure', function () {
let imagefs: typeof $imagefs;
let api: BalenaAPIMock;
let tmpPath: string;
let tmpDummyPath: string;
let tmpMatchingDtJsonPartitionPath: string;
let tmpNonMatchingDtJsonPartitionPath: string;
beforeEach(async () => {
before(async function () {
// We conditionally import balena-image-fs, since when imported on top level then unrelated tests on win32 failed with:
// EPERM: operation not permitted, rename 'C:\Users\RUNNER~1\AppData\Local\Temp\tmp-<...>.inprogress' -> 'C:\Users\RUNNER~1\AppData\Local\Temp\tmp-<...>'
// at async Object.rename (node:internal/fs/promises:782:10) {
imagefs = await import('balena-image-fs');
tmpDummyPath = (await tmpNameAsync()) as string;
await fs.copyFile('./tests/test-data/dummy.img', tmpDummyPath);
tmpMatchingDtJsonPartitionPath = (await tmpNameAsync()) as string;
await fs.copyFile(
'./tests/test-data/mock-jetson-nano-6.0.13.with-boot-partition-12.img',
tmpMatchingDtJsonPartitionPath,
);
tmpNonMatchingDtJsonPartitionPath = (await tmpNameAsync()) as string;
// Create an image with a device-type.json that mentions a non matching boot partition.
// We copy the pre-existing image and modify it, since including a separate one
// would add 18MB more to the repository.
await fs.copyFile(
'./tests/test-data/mock-jetson-nano-6.0.13.with-boot-partition-12.img',
tmpNonMatchingDtJsonPartitionPath,
);
await imagefs.interact(
tmpNonMatchingDtJsonPartitionPath,
12,
async (_fs) => {
const dtJson = JSON.parse(
await _fs.promises.readFile('/device-type.json', {
encoding: 'utf8',
}),
);
expect(dtJson).to.have.nested.property(
'configuration.config.partition',
12,
);
dtJson.configuration.config.partition = 999;
await _fs.promises.writeFile(
'/device-type.json',
JSON.stringify(dtJson),
);
await _fs.promises.writeFile(
'/os-release',
stripIndent`
ID="balena-os"
NAME="balenaOS"
VERSION="6.1.25"
VERSION_ID="6.1.25"
PRETTY_NAME="balenaOS 6.1.25"
DISTRO_CODENAME="kirkstone"
MACHINE="jetson-nano"
META_BALENA_VERSION="6.1.25"`,
);
},
);
});
beforeEach(() => {
api = new BalenaAPIMock();
api.expectGetWhoAmI({ optional: true, persist: true });
tmpPath = (await tmpNameAsync()) as string;
await fs.copyFile('./tests/test-data/dummy.img', tmpPath);
});
afterEach(async () => {
afterEach(() => {
api.done();
await fs.unlink(tmpPath);
});
it('should inject a valid config.json file', async () => {
after(async () => {
await fs.unlink(tmpDummyPath);
await fs.unlink(tmpMatchingDtJsonPartitionPath);
await fs.unlink(tmpNonMatchingDtJsonPartitionPath);
});
it('should detect the OS version and inject a valid config.json file to a 6.0.13 image with partition 12 as boot & matching device-type.json', async () => {
api.expectGetApplication();
api.expectGetConfigDeviceTypes();
api.expectGetDeviceTypes();
// It should not reach to /config or /device-types/v1 but instead find
// everything required from the device-type.json in the image.
// api.expectGetConfigDeviceTypes();
api.expectDownloadConfig();
const command: string[] = [
`os configure ${tmpPath}`,
'--device-type raspberrypi3',
'--version 2.47.0+rev1',
`os configure ${tmpMatchingDtJsonPartitionPath}`,
'--device-type jetson-nano',
'--fleet testApp',
'--config-app-update-poll-interval 10',
'--config-network ethernet',
@ -65,9 +131,124 @@ if (process.platform !== 'win32') {
expect(err.join('')).to.equal('');
// confirm the image contains a config.json...
const imagefs = await import('balena-image-fs');
const config = await imagefs.interact(tmpPath, 1, async (_fs) => {
return await promisify(_fs.readFile)('/config.json');
const config = await imagefs.interact(
tmpMatchingDtJsonPartitionPath,
12,
async (_fs) => {
const dtJson = JSON.parse(
await _fs.promises.readFile('/device-type.json', {
encoding: 'utf8',
}),
);
// confirm that the device-type.json mentions the expected partition
expect(dtJson).to.have.nested.property(
'configuration.config.partition',
12,
);
return await _fs.promises.readFile('/config.json');
},
);
expect(config).to.not.be.empty;
// confirm the image has the correct config.json values...
const configObj = JSON.parse(config.toString('utf8'));
expect(configObj).to.have.property('deviceType', 'jetson-nano');
expect(configObj).to.have.property('initialDeviceName', 'testDeviceName');
});
it('should detect the OS version and inject a valid config.json file to a 6.1.25 image with partition 12 as boot & a non-matching device-type.json', async () => {
api.expectGetApplication();
api.expectGetDeviceTypes();
// It should not reach to /config or /device-types/v1 but instead find
// everything required from the device-type.json in the image.
// api.expectGetConfigDeviceTypes();
api.expectDownloadConfig();
const command: string[] = [
`os configure ${tmpNonMatchingDtJsonPartitionPath}`,
'--device-type jetson-nano',
'--fleet testApp',
'--config-app-update-poll-interval 10',
'--config-network ethernet',
'--initial-device-name testDeviceName',
'--provisioning-key-name testKey',
'--provisioning-key-expiry-date 2050-12-12',
];
const { err } = await runCommand(command.join(' '));
expect(err.join('')).to.equal('');
// confirm the image contains a config.json...
const config = await imagefs.interact(
tmpNonMatchingDtJsonPartitionPath,
12,
async (_fs) => {
const dtJson = JSON.parse(
await _fs.promises.readFile('/device-type.json', {
encoding: 'utf8',
}),
);
// confirm that the device-type.json mentions the expected partition
expect(dtJson).to.have.nested.property(
'configuration.config.partition',
999,
);
return await _fs.promises.readFile('/config.json');
},
);
expect(config).to.not.be.empty;
// confirm the image has the correct config.json values...
const configObj = JSON.parse(config.toString('utf8'));
expect(configObj).to.have.property('deviceType', 'jetson-nano');
expect(configObj).to.have.property('initialDeviceName', 'testDeviceName');
});
// TODO: In the next major consider just failing when we can't find a device-types.json in the image.
it('should inject a valid config.json file to a dummy image', async () => {
api.expectGetApplication();
// Since the dummy image doesn't include a device-type.json
// we have to reach to the API to fetch the manifest of the device type.
api.expectGetConfigDeviceTypes();
api.expectDownloadConfig();
const command: string[] = [
`os configure ${tmpDummyPath}`,
'--device-type raspberrypi3',
'--version 2.47.0+rev1',
'--fleet testApp',
'--config-app-update-poll-interval 10',
'--config-network ethernet',
'--initial-device-name testDeviceName',
'--provisioning-key-name testKey',
'--provisioning-key-expiry-date 2050-12-12',
];
const { err } = await runCommand(command.join(' '));
// Once we replace the dummy.img with one that includes a os-release & device-type.json
// then we should be able to change this to expect no errors.
expect(
err.flatMap((line) => line.split('\n')).filter((line) => line !== ''),
).to.deep.equal(
stripIndent`
[warn] "${tmpDummyPath}":
[warn] Found partition table with 1 partitions,
[warn] but none with a name/label in ['resin-boot', 'flash-boot', 'balena-boot'].
[warn] Will scan all partitions for contents.
[warn] "${tmpDummyPath}":
[warn] 1 partition(s) found, but none containing file "/device-type.json".
[warn] Assuming default boot partition number '1'.
[warn] "${tmpDummyPath}":
[warn] Could not find a previous "/config.json" file in partition '1'.
[warn] Proceeding anyway, but this is unexpected.
[warn] Error while finding a device-type.json on the provided image path. Attempting to fetch from the API.`.split(
'\n',
),
);
// confirm the image contains a config.json...
const config = await imagefs.interact(tmpDummyPath, 1, async (_fs) => {
return await _fs.promises.readFile('/config.json');
});
expect(config).to.not.be.empty;

View File

@ -462,7 +462,13 @@ describe('balena push', function () {
tmp.setGracefulCleanup();
const projectPath = await new Promise<string>((resolve, reject) => {
const opts = { template: 'tmp-XXXXXX', unsafeCleanup: true };
tmp.dir(opts, (e, p) => (e ? reject(e) : resolve(p)));
tmp.dir(opts, (e, p) => {
if (e) {
reject(e);
} else {
resolve(p);
}
});
});
console.error(`[debug] Temp project dir: ${projectPath}`);
@ -475,7 +481,7 @@ describe('balena push', function () {
try {
server.listen(socketPath, resolve);
} catch (e) {
reject(e);
reject(e as Error);
}
});
console.error(`[debug] Checking existence of socket at '${socketPath}'`);
@ -505,7 +511,13 @@ describe('balena push', function () {
// Terminate Unix Domain Socket server
await new Promise<void>((resolve, reject) => {
server.close((e) => (e ? reject(e) : resolve()));
server.close((e) => {
if (e) {
reject(e);
} else {
resolve();
}
});
});
expect(await exists(socketPath), 'Socket existence').to.be.false;

View File

@ -82,7 +82,7 @@ describe('balena release', function () {
expect(err).to.be.empty;
const json = JSON.parse(out.join(''));
expect(json[0].commit).to.equal('90247b54de4fa7a0a3cbc85e73c68039');
expect(json[0].contains__image[0].image[0].start_timestamp).to.equal(
expect(json[0].release_image[0].image[0].start_timestamp).to.equal(
'2020-01-04T01:13:08.583Z',
);
});

View File

@ -26,7 +26,7 @@ describe('balena whoami', function () {
api = new BalenaAPIMock();
});
this.afterEach(async () => {
this.afterEach(() => {
// Check all expected api calls have been made and clean up.
api.done();
});

View File

@ -26,7 +26,6 @@ import * as tar from 'tar-stream';
import { streamToBuffer } from 'tar-utils';
import { URL } from 'url';
import { diff } from 'deep-object-diff';
import { makeImageName } from '../build/utils/compose_ts';
import { stripIndent } from '../build/utils/lazy';
import type { BuilderMock } from './nock/builder-mock';
@ -77,13 +76,13 @@ export async function inspectTarStream(
type: header.type,
};
const expected = expectedFiles[header.name];
if (expected && expected.testStream) {
if (expected?.testStream) {
await expected.testStream(header, stream, expected);
} else {
await defaultTestStream(header, stream, expected, projectPath);
}
} catch (err) {
reject(err);
reject(err as Error);
}
next();
},
@ -144,9 +143,8 @@ export async function expectStreamNoCRLF(
_header: tar.Headers,
stream: Readable,
): Promise<void> {
const chai = await import('chai');
const buf = await streamToBuffer(stream);
await chai.expect(buf.includes('\r\n')).to.be.false;
expect(buf.includes('\r\n')).to.be.false;
}
/**
@ -184,7 +182,7 @@ export async function testDockerBuildStream(o: {
o.dockerMock.expectPostBuild({
...o,
checkURI: async (uri: string) => {
checkURI: (uri: string) => {
const url = new URL(uri, 'http://test.net/');
const queryParams = Array.from(url.searchParams.entries());
expect(deepJsonParse(queryParams)).to.have.deep.members(
@ -241,7 +239,7 @@ export async function testPushBuildStream(o: {
o.builderMock.expectPostBuild({
...o,
checkURI: async (uri: string) => {
checkURI: (uri: string) => {
const url = new URL(uri, 'http://test.net/');
const queryParams = Array.from(url.searchParams.entries());
expect(deepJsonParse(queryParams)).to.have.deep.members(

View File

@ -287,15 +287,14 @@ export function monochrome(text: string): string {
*/
export function fillTemplate(
templateString: string,
templateVars: object,
templateVars: Record<string, unknown>,
): string {
const escaped = templateString.replace(/\\/g, '\\\\').replace(/`/g, '\\`');
const resolved = new Function(
...Object.keys(templateVars),
`return \`${escaped}\`;`,
).call(null, ...Object.values(templateVars));
const unescaped = resolved.replace(/\\`/g, '`').replace(/\\\\/g, '\\');
return unescaped;
return templateString.replace(/\$\{(\w+)\}/g, (_, key) => {
if (key in templateVars) {
return String(templateVars[key]);
}
throw new Error(`Missing template variable: ${key}`);
});
}
/**

View File

@ -61,22 +61,25 @@ export class BalenaAPIMock extends NockMock {
}
public expectDownloadConfig(opts: ScopeOpts = {}) {
this.optPost('/download-config', opts).reply(
200,
JSON.parse(`{
"applicationId":1301645,
"deviceType":"raspberrypi3",
"userId":43699,
"appUpdatePollInterval":600000,
"listenPort":48484,
"vpnPort":443,
"apiEndpoint":"https://api.balena-cloud.com",
"vpnEndpoint":"vpn.balena-cloud.com",
"registryEndpoint":"registry2.balena-cloud.com",
"deltaEndpoint":"https://delta.balena-cloud.com",
"apiKey":"nothingtoseehere"
}`),
);
this.optPost('/download-config', opts).reply(200, (_uri, body) => {
let deviceType = 'raspberrypi3';
if (typeof body === 'object' && 'deviceType' in body) {
deviceType = body.deviceType;
}
return JSON.parse(`{
"applicationId":1301645,
"deviceType":"${deviceType}",
"userId":43699,
"appUpdatePollInterval":600000,
"listenPort":48484,
"vpnPort":443,
"apiEndpoint":"https://api.balena-cloud.com",
"vpnEndpoint":"vpn.balena-cloud.com",
"registryEndpoint":"registry2.balena-cloud.com",
"deltaEndpoint":"https://delta.balena-cloud.com",
"apiKey":"nothingtoseehere"
}`);
});
}
public expectApplicationProvisioning(opts: ScopeOpts = {}) {
@ -283,7 +286,7 @@ export class BalenaAPIMock extends NockMock {
this.optGet(/^\/v\d+\/service_environment_variable($|\?)/, opts).reply(
function (uri, _requestBody) {
const match = uri.match(/service_name%20eq%20%27(.+?)%27/);
const serviceName = (match && match[1]) || undefined;
const serviceName = match?.[1] || undefined;
let varArray: any[];
if (serviceName) {
const varObj = appServiceVarsByService[serviceName];
@ -331,7 +334,7 @@ export class BalenaAPIMock extends NockMock {
opts,
).reply(function (uri, _requestBody) {
const match = uri.match(/service_name%20eq%20%27(.+?)%27/);
const serviceName = (match && match[1]) || undefined;
const serviceName = match?.[1] || undefined;
let varArray: any[];
if (serviceName) {
const varObj = deviceServiceVarsByService[serviceName];

View File

@ -37,7 +37,7 @@ export class BuilderMock extends NockMock {
persist?: boolean;
responseBody: any;
responseCode: number;
checkURI: (uri: string) => Promise<void>;
checkURI: (uri: string) => Promise<void> | void;
checkBuildRequestBody: (requestBody: string | Buffer) => Promise<void>;
}) {
this.optPost(/^\/v3\/build($|[(?])/, opts).reply(

View File

@ -75,7 +75,7 @@ export class DockerMock extends NockMock {
responseBody: any;
responseCode: number;
tag: string;
checkURI: (uri: string) => Promise<void>;
checkURI: (uri: string) => Promise<void> | void;
checkBuildRequestBody: (requestBody: string) => Promise<void>;
}) {
this.optPost(

View File

@ -37,7 +37,7 @@ export class NockMock {
constructor(
public basePathPattern: string | RegExp,
public allowUnmocked: boolean = false,
public allowUnmocked = false,
) {
if (NockMock.instanceCount === 0) {
if (!nock.isActive()) {

View File

@ -50,7 +50,7 @@ export async function exists(fPath: string) {
try {
await fs.stat(fPath);
return true;
} catch (e) {
} catch {
return false;
}
}

View File

@ -10,7 +10,7 @@
"build_log": null,
"start_timestamp": "2021-08-25T22:18:33.624Z",
"end_timestamp": "2021-08-25T22:18:48.820Z",
"contains__image": [
"release_image": [
{
"image": [
{

View File

@ -373,6 +373,8 @@
> Warning Failed to make bytecode node20-arm64 for file node_modules/axios/lib/platform/node/index.js
> Warning Failed to make bytecode node20-arm64 for file node_modules/axios/lib/platform/node/classes/FormData.js
> Warning Failed to make bytecode node20-arm64 for file node_modules/axios/lib/platform/node/classes/URLSearchParams.js
> Warning Failed to make bytecode node20-arm64 for file node_modules/form-data-encoder/lib/index.js
> Warning Failed to make bytecode node20-arm64 for file node_modules/formdata-node/lib/form-data.js
> Warning Failed to make bytecode node20-arm64 for file node_modules/@isaacs/cliui/node_modules/string-width/index.js
> Warning Failed to make bytecode node20-arm64 for file node_modules/@isaacs/cliui/node_modules/strip-ansi/index.js
> Warning Failed to make bytecode node20-arm64 for file node_modules/@isaacs/cliui/node_modules/wrap-ansi/index.js

View File

@ -373,6 +373,8 @@
> Warning Failed to make bytecode node20-x64 for file node_modules/axios/lib/platform/node/index.js
> Warning Failed to make bytecode node20-x64 for file node_modules/axios/lib/platform/node/classes/FormData.js
> Warning Failed to make bytecode node20-x64 for file node_modules/axios/lib/platform/node/classes/URLSearchParams.js
> Warning Failed to make bytecode node20-x64 for file node_modules/form-data-encoder/lib/index.js
> Warning Failed to make bytecode node20-x64 for file node_modules/formdata-node/lib/form-data.js
> Warning Failed to make bytecode node20-x64 for file node_modules/@isaacs/cliui/node_modules/string-width/index.js
> Warning Failed to make bytecode node20-x64 for file node_modules/@isaacs/cliui/node_modules/strip-ansi/index.js
> Warning Failed to make bytecode node20-x64 for file node_modules/@isaacs/cliui/node_modules/wrap-ansi/index.js

View File

@ -373,6 +373,8 @@
> Warning Failed to make bytecode node20-arm64 for file node_modules/axios/lib/platform/node/index.js
> Warning Failed to make bytecode node20-arm64 for file node_modules/axios/lib/platform/node/classes/FormData.js
> Warning Failed to make bytecode node20-arm64 for file node_modules/axios/lib/platform/node/classes/URLSearchParams.js
> Warning Failed to make bytecode node20-arm64 for file node_modules/form-data-encoder/lib/index.js
> Warning Failed to make bytecode node20-arm64 for file node_modules/formdata-node/lib/form-data.js
> Warning Failed to make bytecode node20-arm64 for file node_modules/@isaacs/cliui/node_modules/string-width/index.js
> Warning Failed to make bytecode node20-arm64 for file node_modules/@isaacs/cliui/node_modules/strip-ansi/index.js
> Warning Failed to make bytecode node20-arm64 for file node_modules/@isaacs/cliui/node_modules/wrap-ansi/index.js

View File

@ -373,6 +373,8 @@
> Warning Failed to make bytecode node20-x64 for file node_modules/axios/lib/platform/node/index.js
> Warning Failed to make bytecode node20-x64 for file node_modules/axios/lib/platform/node/classes/FormData.js
> Warning Failed to make bytecode node20-x64 for file node_modules/axios/lib/platform/node/classes/URLSearchParams.js
> Warning Failed to make bytecode node20-x64 for file node_modules/form-data-encoder/lib/index.js
> Warning Failed to make bytecode node20-x64 for file node_modules/formdata-node/lib/form-data.js
> Warning Failed to make bytecode node20-x64 for file node_modules/@isaacs/cliui/node_modules/string-width/index.js
> Warning Failed to make bytecode node20-x64 for file node_modules/@isaacs/cliui/node_modules/strip-ansi/index.js
> Warning Failed to make bytecode node20-x64 for file node_modules/@isaacs/cliui/node_modules/wrap-ansi/index.js

View File

@ -373,6 +373,8 @@
> Warning Failed to make bytecode node20-x64 for file node_modules\axios\lib\platform\node\index.js
> Warning Failed to make bytecode node20-x64 for file node_modules\axios\lib\platform\node\classes\FormData.js
> Warning Failed to make bytecode node20-x64 for file node_modules\axios\lib\platform\node\classes\URLSearchParams.js
> Warning Failed to make bytecode node20-x64 for file node_modules\form-data-encoder\lib\index.js
> Warning Failed to make bytecode node20-x64 for file node_modules\formdata-node\lib\form-data.js
> Warning Failed to make bytecode node20-x64 for file node_modules\@isaacs\cliui\node_modules\string-width\index.js
> Warning Failed to make bytecode node20-x64 for file node_modules\@isaacs\cliui\node_modules\strip-ansi\index.js
> Warning Failed to make bytecode node20-x64 for file node_modules\@isaacs\cliui\node_modules\wrap-ansi\index.js

View File

@ -75,12 +75,12 @@ describe('detectEncoding() function', function () {
for (const fname of sampleBinary) {
const buf = await fs.readFile(path.join('node_modules', fname));
const encoding = await detectEncoding(buf);
const encoding = detectEncoding(buf);
expect(encoding).to.equal('binary');
}
for (const fname of sampleText) {
const buf = await fs.readFile(fname);
const encoding = await detectEncoding(buf);
const encoding = detectEncoding(buf);
expect(encoding).to.equal('utf-8');
}
});

View File

@ -1,5 +1,5 @@
import * as stream from 'stream';
import { AssertionError, expect } from 'chai';
import { expect } from 'chai';
import { stub } from 'sinon';
import * as tmp from 'tmp';
import { delay } from '../../utils';
@ -32,9 +32,7 @@ describe('image-manager', function () {
fs.writeSync(this.image.fd, 'Cache image', 0, 'utf8');
this.cacheGetImagePathStub = stub(imageManager, 'getImagePath');
return this.cacheGetImagePathStub.returns(
Promise.resolve(this.image.name),
);
return this.cacheGetImagePathStub.resolves(this.image.name);
});
afterEach(function () {
@ -44,8 +42,8 @@ describe('image-manager', function () {
describe('given the image is fresh', function () {
beforeEach(function () {
this.cacheIsImageFresh = stub(imageManager, 'isImageFresh');
return this.cacheIsImageFresh.returns(Promise.resolve(true));
this.cacheIsImageFresh = stub(imageManager, 'isImageCached');
return this.cacheIsImageFresh.resolves(true);
});
afterEach(function () {
@ -58,11 +56,11 @@ describe('image-manager', function () {
void imageManager.getStream('raspberry-pi').then(function (stream) {
let result = '';
stream.on('data', (chunk) => (result += chunk.toString()));
stream.on('data', (chunk: string) => (result += chunk.toString()));
return stream.on('end', function () {
expect(result).to.equal('Cache image');
return done();
done();
});
});
});
@ -70,8 +68,8 @@ describe('image-manager', function () {
describe('given the image is not fresh', function () {
beforeEach(function () {
this.cacheIsImageFresh = stub(imageManager, 'isImageFresh');
return this.cacheIsImageFresh.returns(Promise.resolve(false));
this.cacheIsImageFresh = stub(imageManager, 'isImageCached');
return this.cacheIsImageFresh.resolves(false);
});
afterEach(function () {
@ -82,9 +80,7 @@ describe('image-manager', function () {
describe.skip('given a valid download endpoint', function () {
beforeEach(function () {
this.osDownloadStub = stub(balena.models.os, 'download');
this.osDownloadStub.returns(
Promise.resolve(stringToStream('Download image')),
);
this.osDownloadStub.resolves(stringToStream('Download image'));
});
afterEach(function () {
@ -95,7 +91,7 @@ describe('image-manager', function () {
void imageManager.getStream('raspberry-pi').then((stream) => {
let result = '';
stream.on('data', (chunk) => (result += chunk));
stream.on('data', (chunk: string) => (result += chunk));
stream.on('end', async () => {
expect(result).to.equal('Download image');
@ -131,7 +127,7 @@ describe('image-manager', function () {
beforeEach(function () {
this.osDownloadStream = new stream.PassThrough();
this.osDownloadStub = stub(balena.models.os, 'download');
this.osDownloadStub.returns(Promise.resolve(this.osDownloadStream));
this.osDownloadStub.resolves(this.osDownloadStream);
});
afterEach(function () {
@ -153,9 +149,7 @@ describe('image-manager', function () {
const contents = await fsAsync
.stat(this.image.name + '.inprogress')
.then(function () {
throw new AssertionError(
'Image cache should be deleted on failure',
);
throw new Error('Image cache should be deleted on failure');
})
.catch((err) => {
if (err.code !== 'ENOENT') {
@ -174,7 +168,7 @@ describe('image-manager', function () {
});
});
describe('given a stream with the mime property', async function () {
describe('given a stream with the mime property', function () {
beforeEach(function () {
this.osDownloadStub = stub(balena.models.os, 'download');
const message = 'Lorem ipsum dolor sit amet';
@ -184,7 +178,7 @@ describe('image-manager', function () {
mime?: string;
};
mockResultStream.mime = 'application/zip';
this.osDownloadStub.returns(Promise.resolve(mockResultStream));
this.osDownloadStub.resolves(mockResultStream);
});
afterEach(function () {
@ -209,12 +203,10 @@ describe('image-manager', function () {
this.balenaSettingsGetStub
.withArgs('cacheDirectory')
.returns(
Promise.resolve(
os.platform() === 'win32'
? 'C:\\Users\\johndoe\\_balena\\cache'
: '/Users/johndoe/.balena/cache',
),
.resolves(
os.platform() === 'win32'
? 'C:\\Users\\johndoe\\_balena\\cache'
: '/Users/johndoe/.balena/cache',
);
});
@ -228,21 +220,21 @@ describe('image-manager', function () {
balena.models.config,
'getDeviceTypeManifestBySlug',
);
this.getDeviceTypeManifestBySlugStub.withArgs('raspberry-pi').returns(
Promise.resolve({
this.getDeviceTypeManifestBySlugStub
.withArgs('raspberry-pi')
.resolves({
yocto: {
fstype: 'resin-sdcard',
},
}),
);
});
this.getDeviceTypeManifestBySlugStub.withArgs('intel-edison').returns(
Promise.resolve({
this.getDeviceTypeManifestBySlugStub
.withArgs('intel-edison')
.resolves({
yocto: {
fstype: 'zip',
},
}),
);
});
});
afterEach(function () {
@ -288,20 +280,18 @@ describe('image-manager', function () {
});
});
describe('.isImageFresh()', () => {
describe('.isImageCached()', () => {
describe('given the raspberry-pi manifest', function () {
beforeEach(function () {
this.getDeviceTypeManifestBySlugStub = stub(
balena.models.config,
'getDeviceTypeManifestBySlug',
);
this.getDeviceTypeManifestBySlugStub.returns(
Promise.resolve({
yocto: {
fstype: 'balena-sdcard',
},
}),
);
this.getDeviceTypeManifestBySlugStub.resolves({
yocto: {
fstype: 'balena-sdcard',
},
});
});
afterEach(function () {
@ -314,8 +304,8 @@ describe('image-manager', function () {
imageManager,
'getFileCreatedDate',
);
this.utilsGetFileCreatedDate.returns(
Promise.reject(new Error("ENOENT, stat 'raspberry-pi'")),
this.utilsGetFileCreatedDate.rejects(
new Error("ENOENT, stat 'raspberry-pi'"),
);
});
@ -324,78 +314,8 @@ describe('image-manager', function () {
});
it('should return false', async function () {
expect(await imageManager.isImageFresh('raspberry-pi', '1.2.3')).to.be
.false;
});
});
describe('given a fixed created time', function () {
beforeEach(function () {
this.utilsGetFileCreatedDate = stub(
imageManager,
'getFileCreatedDate',
);
this.utilsGetFileCreatedDate.returns(
Promise.resolve(new Date('2014-01-01T00:00:00.000Z')),
);
});
afterEach(function () {
this.utilsGetFileCreatedDate.restore();
});
describe('given the file was created before the os last modified time', function () {
beforeEach(function () {
this.osGetLastModified = stub(balena.models.os, 'getLastModified');
this.osGetLastModified.returns(
Promise.resolve(new Date('2014-02-01T00:00:00.000Z')),
);
});
afterEach(function () {
this.osGetLastModified.restore();
});
it('should return false', function () {
const promise = imageManager.isImageFresh('raspberry-pi', '1.2.3');
return expect(promise).to.eventually.be.false;
});
});
describe('given the file was created after the os last modified time', function () {
beforeEach(function () {
this.osGetLastModified = stub(balena.models.os, 'getLastModified');
this.osGetLastModified.returns(
Promise.resolve(new Date('2013-01-01T00:00:00.000Z')),
);
});
afterEach(function () {
this.osGetLastModified.restore();
});
it('should return true', function () {
const promise = imageManager.isImageFresh('raspberry-pi', '1.2.3');
return expect(promise).to.eventually.be.true;
});
});
describe('given the file was created just at the os last modified time', function () {
beforeEach(function () {
this.osGetLastModified = stub(balena.models.os, 'getLastModified');
this.osGetLastModified.returns(
Promise.resolve(new Date('2014-00-01T00:00:00.000Z')),
);
});
afterEach(function () {
this.osGetLastModified.restore();
});
it('should return false', function () {
const promise = imageManager.isImageFresh('raspberry-pi', '1.2.3');
return expect(promise).to.eventually.be.false;
});
expect(await imageManager.isImageCached('raspberry-pi', '1.2.3')).to
.be.false;
});
});
});
@ -408,7 +328,7 @@ describe('image-manager', function () {
fs.writeSync(this.image.fd, 'Lorem ipsum dolor sit amet', 0, 'utf8');
this.cacheGetImagePathStub = stub(imageManager, 'getImagePath');
this.cacheGetImagePathStub.returns(Promise.resolve(this.image.name));
this.cacheGetImagePathStub.resolves(this.image.name);
});
afterEach(function (done) {
@ -422,7 +342,7 @@ describe('image-manager', function () {
.then(function (stream) {
let result = '';
stream.on('data', (chunk) => (result += chunk));
stream.on('data', (chunk) => (result += chunk as string));
stream.on('end', function () {
expect(result).to.equal('Lorem ipsum dolor sit amet');
@ -445,7 +365,7 @@ describe('image-manager', function () {
beforeEach(function () {
this.image = tmp.fileSync();
this.cacheGetImagePathStub = stub(imageManager, 'getImagePath');
this.cacheGetImagePathStub.returns(Promise.resolve(this.image.name));
this.cacheGetImagePathStub.resolves(this.image.name);
});
afterEach(function (done) {
@ -488,9 +408,7 @@ describe('image-manager', function () {
beforeEach(function () {
this.date = new Date(2014, 1, 1);
this.fsStatStub = stub(fs.promises, 'stat');
this.fsStatStub
.withArgs('foo')
.returns(Promise.resolve({ ctime: this.date }));
this.fsStatStub.withArgs('foo').resolves({ ctime: this.date });
});
afterEach(function () {
@ -508,7 +426,7 @@ describe('image-manager', function () {
this.fsStatStub = stub(fs.promises, 'stat');
this.fsStatStub
.withArgs('foo')
.returns(Promise.reject(new Error("ENOENT, stat 'foo'")));
.rejects(new Error("ENOENT, stat 'foo'"));
});
afterEach(function () {
@ -554,7 +472,9 @@ describe('image-manager', function () {
mockFs({});
});
afterEach(() => mockFs.restore());
afterEach(() => {
mockFs.restore();
});
it('should keep the cache directory removed', async function () {
const exists = await fsExistsAsync(this.cacheDirectory);

View File

@ -117,7 +117,7 @@ describe('disambiguateReleaseParam() function', () => {
it('should return id from SDK on first call, if match is found', async () => {
const input = '1234';
const output = 1234;
const getRelease = sinon.stub().returns(Promise.resolve({ id: output }));
const getRelease = sinon.stub().resolves({ id: output });
const sdk: any = {
models: {
release: {
@ -139,9 +139,9 @@ describe('disambiguateReleaseParam() function', () => {
const getRelease = sinon
.stub()
.onCall(0)
.returns(Promise.reject(new BalenaReleaseNotFound(input)))
.rejects(new BalenaReleaseNotFound(input))
.onCall(1)
.returns(Promise.resolve({ id: output }));
.resolves({ id: output });
const sdk: any = {
models: {
@ -161,9 +161,7 @@ describe('disambiguateReleaseParam() function', () => {
it('should throw error if no match found', async () => {
const input = '1234';
const getRelease = sinon
.stub()
.returns(Promise.reject(new BalenaReleaseNotFound(input)));
const getRelease = sinon.stub().rejects(new BalenaReleaseNotFound(input));
const sdk: any = {
models: {
@ -185,9 +183,7 @@ describe('disambiguateReleaseParam() function', () => {
it('should throw error if unknown error returned from SDK', async () => {
const input = '1234';
const getRelease = sinon
.stub()
.returns(Promise.reject(new Error('some error')));
const getRelease = sinon.stub().rejects(new Error('some error'));
const sdk: any = {
models: {

View File

@ -15,10 +15,14 @@
* limitations under the License.
*/
import { expect } from 'chai';
import * as chaiAsPromised from 'chai-as-promised';
import * as chai from 'chai';
import { ExpectedError } from '../../build/errors';
import * as v from '../../build/utils/validation';
chai.use(chaiAsPromised);
const { expect } = chai;
describe('validateEmail() function', () => {
it('should reject invalid email addresses with a message', () => {
const errorMessage = 'Email is not valid';
@ -186,55 +190,55 @@ describe('validateUuid() function', () => {
describe('parseAsInteger() function', () => {
it('should reject non-numeric characters', () => {
expect(() => v.parseAsInteger('abc')).to.throw(ExpectedError);
expect(() => v.parseAsInteger('1a')).to.throw(ExpectedError);
expect(() => v.parseAsInteger('a1')).to.throw(ExpectedError);
expect(() => v.parseAsInteger('a')).to.throw(ExpectedError);
expect(() => v.parseAsInteger('1.0')).to.throw(ExpectedError);
expect(v.parseAsInteger('abc')).to.be.rejectedWith(ExpectedError);
expect(v.parseAsInteger('1a')).to.be.rejectedWith(ExpectedError);
expect(v.parseAsInteger('a1')).to.be.rejectedWith(ExpectedError);
expect(v.parseAsInteger('a')).to.be.rejectedWith(ExpectedError);
expect(v.parseAsInteger('1.0')).to.be.rejectedWith(ExpectedError);
});
it('should reject leading zeros', () => {
expect(() => v.parseAsInteger('01')).to.throw(ExpectedError);
expect(() => v.parseAsInteger('001')).to.throw(ExpectedError);
expect(v.parseAsInteger('01')).to.be.rejectedWith(ExpectedError);
expect(v.parseAsInteger('001')).to.be.rejectedWith(ExpectedError);
});
it('should throw with specific message when param name passed', () => {
expect(() => v.parseAsInteger('abc')).to.throw(
expect(v.parseAsInteger('abc')).to.be.rejectedWith(
'The parameter must be an integer.',
);
});
it('should throw with general message when no param name passed', () => {
expect(() => v.parseAsInteger('abc', 'foo')).to.throw(
expect(v.parseAsInteger('abc', 'foo')).to.be.rejectedWith(
"The parameter 'foo' must be an integer.",
);
});
it('should parse integers to number type', () => {
expect(v.parseAsInteger('100')).to.equal(100);
expect(v.parseAsInteger('100')).to.be.a('number');
expect(v.parseAsInteger('0')).to.equal(0);
expect(v.parseAsInteger('0')).to.be.a('number');
it('should parse integers to number type', async () => {
expect(await v.parseAsInteger('100')).to.equal(100);
expect(await v.parseAsInteger('100')).to.be.a('number');
expect(await v.parseAsInteger('0')).to.equal(0);
expect(await v.parseAsInteger('0')).to.be.a('number');
});
});
describe('tryAsInteger() function', () => {
it('should return string with non-numeric characters as string', () => {
expect(v.tryAsInteger('abc')).to.be.a('string');
expect(v.tryAsInteger('1a')).to.be.a('string');
expect(v.tryAsInteger('a1')).to.be.a('string');
expect(v.tryAsInteger('a')).to.be.a('string');
expect(v.tryAsInteger('1.0')).to.be.a('string');
it('should return string with non-numeric characters as string', async () => {
expect(await v.tryAsInteger('abc')).to.be.a('string');
expect(await v.tryAsInteger('1a')).to.be.a('string');
expect(await v.tryAsInteger('a1')).to.be.a('string');
expect(await v.tryAsInteger('a')).to.be.a('string');
expect(await v.tryAsInteger('1.0')).to.be.a('string');
});
it('should return numerical strings with leading zeros as string', () => {
expect(v.tryAsInteger('01')).to.be.a('string');
expect(v.tryAsInteger('001')).to.be.a('string');
it('should return numerical strings with leading zeros as string', async () => {
expect(await v.tryAsInteger('01')).to.be.a('string');
expect(await v.tryAsInteger('001')).to.be.a('string');
});
it('should return numerical strings without leading zeros as number', () => {
expect(v.tryAsInteger('100')).to.be.a('number');
expect(v.tryAsInteger('256')).to.be.a('number');
expect(v.tryAsInteger('0')).to.be.a('number');
it('should return numerical strings without leading zeros as number', async () => {
expect(await v.tryAsInteger('100')).to.be.a('number');
expect(await v.tryAsInteger('256')).to.be.a('number');
expect(await v.tryAsInteger('0')).to.be.a('number');
});
});

View File

@ -9,6 +9,7 @@
"./src/**/*",
"./patches/*",
"./tests/**/*",
"./typings/**/*"
"./typings/**/*",
".mocharc-standalone.js",
]
}

View File

@ -32,7 +32,7 @@ declare module 'JSONStream' {
recurse: boolean;
}
export function parse(pattern: any | any[]): NodeJS.ReadWriteStream;
export function parse(pattern: any): NodeJS.ReadWriteStream;
type NewlineOnlyIndicator = false;

View File

@ -1,105 +0,0 @@
/**
* @license
* Copyright 2019 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
declare module 'balena-device-init' {
import { DeviceTypeJson } from 'balena-sdk';
interface OperationState {
operation:
| CopyOperation
| ReplaceOperation
| RunScriptOperation
| BurnOperation;
percentage: number;
}
interface Operation {
command: string;
}
interface CopyOperation extends Operation {
command: 'copy';
from: { path: string };
to: { path: string };
}
interface ReplaceOperation extends Operation {
command: 'replace';
copy: string;
replace: string;
file: {
path: string;
};
}
interface RunScriptOperation extends Operation {
command: 'run-script';
script: string;
arguments?: string[];
}
interface BurnOperation extends Operation {
command: 'burn';
image?: string;
}
interface BurnProgress {
type: 'write' | 'check';
percentage: number;
transferred: number;
length: number;
remaining: number;
eta: number;
runtime: number;
delta: number;
speed: number;
}
interface InitializeEmitter {
on(event: 'stdout' | 'stderr', callback: (msg: string) => void): void;
on(event: 'state', callback: (state: OperationState) => void): void;
on(event: 'burn', callback: (state: BurnProgress) => void): void;
on(event: 'end', callback: () => void): void;
on(event: 'error', callback: (error: Error) => void): void;
}
// As of writing this, these are Bluebird promises, but we are typing then
// as normal Promises so that we do not rely on Bluebird specific methods,
// so that the CLI will not require any change once the package drops Bluebird.
export function configure(
image: string,
manifest: BalenaSdk.DeviceTypeJson.DeviceType.DeviceType,
config: object,
options?: object,
): Promise<InitializeEmitter>;
export function initialize(
image: string,
manifest: BalenaSdk.DeviceTypeJson.DeviceType.DeviceType,
config: object,
): Promise<InitializeEmitter>;
export function getImageOsVersion(
image: string,
manifest: BalenaSdk.DeviceTypeJson.DeviceType.DeviceType,
): Promise<string | null>;
export function getImageManifest(
image: string,
): Promise<BalenaSdk.DeviceTypeJson.DeviceType.DeviceType | null>;
}