Compare commits

...

58 Commits

Author SHA1 Message Date
519ac0383a v17.1.1 2023-09-05 15:06:14 +00:00
3d0ef9bc4f Merge pull request #2669 from balena-io/fixes-local-flash-on-unix
Fix local flash on unix environments
2023-09-05 15:05:16 +00:00
49e23464f9 Fix local flash on unix environments
Update etcher-sdk from 8.5.3 to 8.7.0
Change-type: patch
2023-09-05 11:26:06 -03:00
a1c9b4b80e v17.1.0 2023-09-05 13:01:03 +00:00
2b1be3e5d9 Merge pull request #2647 from balena-io/update-oclif
Update oclif, improve help command
2023-09-05 08:59:52 -04:00
e46378ec51 Update oclif, improve help command
Change-type: minor
2023-09-04 21:07:05 -03:00
27ee9c85e7 v17.0.0 2023-08-29 11:45:54 +00:00
21b6ec46e3 Merge pull request #2667 from balena-io/bump-cli-to-node-v18
Update to Node 18
2023-08-29 11:44:48 +00:00
817ce5dc96 Update to Node 18
Change-type: major
2023-08-29 07:35:53 -03:00
d9af28bca7 v16.8.0 2023-08-25 17:47:44 +00:00
8646be7979 Merge pull request #2665 from balena-io/accept-device-application-keys-as-experimental-feature
Accept device & application keys on login as experimental feature
2023-08-25 17:46:52 +00:00
14ba287e0d Accept device & application keys on login as experimental feature
Change-type: minor
2023-08-23 11:12:06 -03:00
1671e46d99 v16.7.9 2023-08-22 13:33:30 +00:00
507333c463 Merge pull request #2663 from balena-io/bumps-sdk-to-v18
Update balena-sdk to v18
2023-08-22 10:32:34 -03:00
8b320d3e9e Update balena-sdk to v18
Update balena-sdk from 17.21.1 to 18.0.0
Update balena-preload from 14.0.0 to 14.0.2
Update balena-image-manager from 9.0.0 to 9.0.2
Change-type: patch
2023-08-22 09:38:39 -03:00
e1be268749 v16.7.8 2023-08-22 11:16:38 +00:00
1a0019e6d0 Merge pull request #2662 from balena-io/otaviojacobi/bumps-to-sdk-17-12-1
Update balena-sdk to ^17.12.1
2023-08-22 11:15:33 +00:00
e79cdb671f Update balena-settings-storage to 8.1.0
Update balena-settings-storage from 7.0.0 to 8.1.0
Change-type: patch
2023-08-21 14:42:28 -03:00
f38e643cf0 env: Stop fetching unnecessary app fields
Change-type: patch
2023-08-21 14:39:16 -03:00
b8e190cd1d Remove redundant envs documentation
Change-type: patch
2023-08-21 14:39:16 -03:00
9cca654bd5 Update balena-sdk to 17.12.1
Update balena-sdk from 17.8.0 to 17.12.1
Change-type: patch
2023-08-21 14:39:16 -03:00
35177e2d2f v16.7.7 2023-08-21 16:56:33 +00:00
1a24b193e7 Merge pull request #2664 from balena-io/otavio-revert-flowzone-to-master
Revert flowzone to master
2023-08-21 16:55:31 +00:00
272915192b Revert flowzone to master
Change-type: patch
2023-08-17 20:22:53 -03:00
96774f4c52 v16.7.6 2023-07-24 13:38:06 +00:00
a034f585ba Merge pull request #2656 from balena-io/fix-app-create
app create: Fix halting with a deprecation warning
2023-07-24 13:37:00 +00:00
365d95c36b app create: Fix halting with a deprecation warning
Change-type: patch
2023-07-21 10:02:32 +03:00
c6313c08ae v16.7.5 2023-07-21 06:59:22 +00:00
f5764c4659 Merge pull request #2655 from balena-io/abstract-fleet-app-block-create
Abstract the fleet/app/block create commands
2023-07-21 09:58:36 +03:00
aff094575b Abstract the fleet/app/block create commands
Change-type: patch
2023-07-20 16:14:52 +03:00
4aaaf64f8d v16.7.4 2023-07-20 10:41:09 +00:00
7b88ce273f Merge pull request #2654 from balena-io/move-discontinued-dt
move: Include fleets of discontinued device types in the fleet selection
2023-07-20 10:40:12 +00:00
b011af89ad move: Include fleets of discontinued device types in the fleet selection
Change-type: patch
2023-07-20 13:03:54 +03:00
1bf8c1bfe7 v16.7.3 2023-07-20 08:30:09 +00:00
2b39d5d111 Merge pull request #2653 from balena-io/promote-discontinued-dt
promote: Allow joining fleets of discontinued device types
2023-07-20 08:29:20 +00:00
98663af7f6 Rerun npm-shrinkwrap.json deduplication 2023-07-19 19:44:43 +03:00
5628824bee promote: Allow joining fleets of discontinued device types
Change-type: patch
2023-07-19 19:17:27 +03:00
d12d7996bc v16.7.2 2023-07-19 01:43:55 +00:00
0dcf4cbff6 Merge pull request #2650 from balena-io/bump-balena-compose
Update balena-compose to v3.0.2
2023-07-19 01:42:56 +00:00
884e37d242 Update balena-compose to v3.0.2
Update balena-compose to v3.0.2

That release removes the use of the `cachefrom` on pull tasks, which
there is good evidence to suggest is the cause of #2165

Change-type: patch
2023-07-18 18:14:56 -04:00
f4a24e26c3 v16.7.1 2023-07-18 20:27:07 +00:00
122eccf3dc Merge pull request #2652 from balena-io/update-balena-sdk-17.8.0
Update balena-sdk to 17.8.0
2023-07-18 20:26:16 +00:00
bd598788dc Update balena-sdk to 17.8.0
Update balena-sdk from 17.0.0 to 17.8.0

Change-type: patch
2023-07-18 22:48:54 +03:00
406482b4da v16.7.0 2023-07-17 19:59:52 +00:00
a381c97ca9 Merge pull request #2649 from balena-io/preload-no-pin-device-to-release
preload: Add the --no-pin-device-to-release flag to avoid interactive questions
2023-07-17 19:59:04 +00:00
8ce78ba33c Rerun npm-shrinkwrap.json deduplication 2023-07-17 11:25:02 +03:00
f53f148c89 preload: Add the --no-pin-device-to-release flag to avoid interactive questions
Change-type: minor
See: https://balena.zulipchat.com/#narrow/stream/345746-aspect.2Fproduct/topic/Level.20-.20need.20thoughts.20on.20questions.20.26.20feature.20suggestions
2023-07-17 11:19:03 +03:00
0086feb645 v16.6.6 2023-07-10 17:16:08 +00:00
4ee55b049f Merge pull request #2646 from balena-io/reduce-lodash-usage
Reduce lodash usage in common user interaction patterns
2023-07-10 17:15:14 +00:00
90c6f121cc Rerun npm-shrinkwrap.json deduplication 2023-07-10 19:36:36 +03:00
d3c27ae859 Reduce lodash usage in common user interaction patterns
Change-type: patch
2023-07-10 17:19:01 +03:00
8f39c1de6c v16.6.5 2023-07-09 21:29:55 +00:00
4df1831187 Merge pull request #2645 from balena-io/application-create-hostApp-based-supported-DTs
fleet/block/app create: Fetch the supported device types using the hostApps
2023-07-09 21:29:02 +00:00
2bce761ace Rerun npm-shrinkwrap.json deduplication 2023-07-07 20:17:53 +03:00
d78b76aceb fleet/block/app create: Fetch the supported device types using the hostApps
Change-type: patch
See: https://balena.zulipchat.com/#narrow/stream/360838-balena-io.2Fos.2Fdevices/topic/state.20field.20in.20device-type.2Ejson
See: https://balena.fibery.io/Organisation/Improvements-849#Improvements/Stop-relying-on-device-types-v1-device-type.json-for-unrelated-things-257
2023-07-07 19:57:36 +03:00
f07f6b84d4 v16.6.4 2023-07-06 13:58:27 +00:00
d297a10570 Merge pull request #2643 from balena-io/bump-balena-compose
Bump balena-compose to v2.3.0
2023-07-06 13:57:37 +00:00
9d0b82122a Bump balena-compose to v2.3.0
This allows the the CLI to use docker registry config when querying the
images manifest.

Relates-to: balena-io-modules/balena-compose#31
Change-type: patch
2023-07-05 15:46:42 -04:00
54 changed files with 17048 additions and 9784 deletions

4
.gitattributes vendored
View File

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

View File

@ -18,7 +18,7 @@ inputs:
default: 'accounts+apple@balena.io'
NODE_VERSION:
type: string
default: '16.x'
default: '18.x'
VERBOSE:
type: string
default: 'true'

View File

@ -15,7 +15,7 @@ inputs:
# --- custom environment
NODE_VERSION:
type: string
default: '16.x'
default: '18.x'
VERBOSE:
type: string
default: "true"
@ -49,7 +49,7 @@ runs:
- name: Compress custom source
shell: pwsh
run: tar -acf ${{ runner.temp }}/custom.tgz .
run: tar --exclude-vcs -acf ${{ runner.temp }}/custom.tgz .
- name: Upload custom artifact
uses: actions/upload-artifact@v3

View File

@ -11,7 +11,7 @@ on:
jobs:
flowzone:
name: Flowzone
uses: product-os/flowzone/.github/workflows/flowzone.yml@v4.7.1
uses: product-os/flowzone/.github/workflows/flowzone.yml@master
# prevent duplicate workflow executions for pull_request and pull_request_target
if: |
(

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -17,7 +17,7 @@
import type { JsonVersions } from '../lib/commands/version';
import { run as oclifRun } from 'oclif';
import { run as oclifRun } from '@oclif/core';
import * as archiver from 'archiver';
import * as Bluebird from 'bluebird';
import { execFile } from 'child_process';
@ -30,6 +30,7 @@ import * as path from 'path';
import * as rimraf from 'rimraf';
import * as semver from 'semver';
import { promisify } from 'util';
import { notarize } from '@electron/notarize';
import { stripIndent } from '../build/utils/lazy';
import {
@ -206,7 +207,6 @@ async function buildPkg() {
const paths: Array<[string, string[], string[]]> = [
// [platform, [source path], [destination path]]
['*', ['open', 'xdg-open'], ['xdg-open']],
['*', ['opn', 'xdg-open'], ['xdg-open-402']],
['darwin', ['denymount', 'bin', 'denymount'], ['denymount']],
];
await Promise.all(
@ -471,8 +471,6 @@ async function notarizeMacInstaller(): Promise<void> {
const appleIdPassword = process.env.XCODE_APP_LOADER_PASSWORD;
if (appleIdPassword && teamId) {
const { notarize } = await import('@electron/notarize');
// https://github.com/electron/notarize#readme
await notarize({
tool: 'notarytool',
teamId,
@ -494,9 +492,10 @@ export async function buildOclifInstaller() {
let packOpts = ['-r', ROOT];
if (process.platform === 'darwin') {
packOS = 'macos';
packOpts = packOpts.concat('--targets', 'darwin-x64');
} else if (process.platform === 'win32') {
packOS = 'win';
packOpts = packOpts.concat('-t', 'win32-x64');
packOpts = packOpts.concat('--targets', 'win32-x64');
}
if (packOS) {
console.log(`Building oclif installer for CLI ${version}`);
@ -514,10 +513,11 @@ export async function buildOclifInstaller() {
await signFilesForNotarization();
}
console.log('=======================================================');
console.log(`oclif "${packCmd}" "${packOpts.join('" "')}"`);
console.log(`oclif ${packCmd} ${packOpts.join(' ')}`);
console.log(`cwd="${process.cwd()}" ROOT="${ROOT}"`);
console.log('=======================================================');
await oclifRun([packCmd].concat(...packOpts));
const oclifPath = path.join(ROOT, 'node_modules', 'oclif');
await oclifRun([packCmd].concat(...packOpts), oclifPath);
await renameInstallerFiles();
// The Windows installer is explicitly signed here (oclif doesn't do it).
// The macOS installer is automatically signed by oclif (which runs the

View File

@ -621,6 +621,10 @@ password
TCP port number of local HTTP login server (--web auth only)
#### -H, --hideExperimentalWarning
Hides warning for experimental features
## logout
Logout from your balena account.
@ -1276,7 +1280,6 @@ Examples:
$ balena envs --fleet myorg/myfleet
$ balena envs --fleet MyFleet --json
$ balena envs --fleet MyFleet --service MyService
$ balena envs --fleet MyFleet --service MyService
$ balena envs --fleet MyFleet --config
$ balena envs --device 7cf02a6
$ balena envs --device 7cf02a6 --json

View File

@ -62,7 +62,7 @@ export default class ApiKeysCmd extends Command {
$select: 'actor',
})
).actor
: await getBalenaSdk().auth.getUserActorId();
: await getBalenaSdk().auth.getActorId();
const keys = await getBalenaSdk().pine.get({
resource: 'api_key',
options: {

View File

@ -18,19 +18,9 @@
import { flags } from '@oclif/command';
import Command from '../../command';
import { ExpectedError } from '../../errors';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
interface FlagsDef {
organization?: string;
type?: string; // application device type
help: void;
}
interface ArgsDef {
name: string;
}
import { stripIndent } from '../../utils/lazy';
import { ArgsDef, FlagsDef } from '../../utils/application-create';
export default class AppCreateCmd extends Command {
public static description = stripIndent`
@ -90,61 +80,8 @@ export default class AppCreateCmd extends Command {
AppCreateCmd,
);
// Ascertain device type
const deviceType =
options.type ||
(await (await import('../../utils/patterns')).selectDeviceType());
// Ascertain organization
const organization =
options.organization?.toLowerCase() || (await this.getOrganization());
// Create application
try {
const application = await getBalenaSdk().models.application.create({
name: params.name,
deviceType,
organization,
applicationClass: 'app',
});
// Output
console.log(
`App created: slug "${application.slug}", device type "${deviceType}"`,
);
} catch (err) {
if ((err.message || '').toLowerCase().includes('unique')) {
// BalenaRequestError: Request error: "organization" and "app_name" must be unique.
throw new ExpectedError(
`Error: An app or block or fleet with the name "${params.name}" already exists in organization "${organization}".`,
);
} else if ((err.message || '').toLowerCase().includes('unauthorized')) {
// BalenaRequestError: Request error: Unauthorized
throw new ExpectedError(
`Error: You are not authorized to create apps in organization "${organization}".`,
);
}
throw err;
}
}
async getOrganization() {
const { getOwnOrganizations } = await import('../../utils/sdk');
const organizations = await getOwnOrganizations(getBalenaSdk(), {
$select: ['name', 'handle'],
});
if (organizations.length === 0) {
// User is not a member of any organizations (should not happen).
throw new Error('This account is not a member of any organizations');
} else if (organizations.length === 1) {
// User is a member of only one organization - use this.
return organizations[0].handle;
} else {
// User is a member of multiple organizations -
const { selectOrganization } = await import('../../utils/patterns');
return selectOrganization(organizations);
}
await (
await import('../../utils/application-create')
).applicationCreateBase('app', options, params);
}
}

View File

@ -18,9 +18,8 @@
import { flags } from '@oclif/command';
import Command from '../../command';
import { ExpectedError } from '../../errors';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
import { stripIndent } from '../../utils/lazy';
interface FlagsDef {
organization?: string;
@ -90,61 +89,8 @@ export default class BlockCreateCmd extends Command {
BlockCreateCmd,
);
// Ascertain device type
const deviceType =
options.type ||
(await (await import('../../utils/patterns')).selectDeviceType());
// Ascertain organization
const organization =
options.organization?.toLowerCase() || (await this.getOrganization());
// Create application
try {
const application = await getBalenaSdk().models.application.create({
name: params.name,
deviceType,
organization,
applicationClass: 'block',
});
// Output
console.log(
`Block created: slug "${application.slug}", device type "${deviceType}"`,
);
} catch (err) {
if ((err.message || '').toLowerCase().includes('unique')) {
// BalenaRequestError: Request error: "organization" and "app_name" must be unique.
throw new ExpectedError(
`Error: An app or block or fleet with the name "${params.name}" already exists in organization "${organization}".`,
);
} else if ((err.message || '').toLowerCase().includes('unauthorized')) {
// BalenaRequestError: Request error: Unauthorized
throw new ExpectedError(
`Error: You are not authorized to create blocks in organization "${organization}".`,
);
}
throw err;
}
}
async getOrganization() {
const { getOwnOrganizations } = await import('../../utils/sdk');
const organizations = await getOwnOrganizations(getBalenaSdk(), {
$select: ['name', 'handle'],
});
if (organizations.length === 0) {
// User is not a member of any organizations (should not happen).
throw new Error('This account is not a member of any organizations');
} else if (organizations.length === 1) {
// User is a member of only one organization - use this.
return organizations[0].handle;
} else {
// User is a member of multiple organizations -
const { selectOrganization } = await import('../../utils/patterns');
return selectOrganization(organizations);
}
await (
await import('../../utils/application-create')
).applicationCreateBase('block', options, params);
}
}

View File

@ -346,9 +346,9 @@ ${dockerignoreHelp}
);
logger.logWarn(msg);
const [token, username, url, options] = await Promise.all([
const [token, { username }, url, options] = await Promise.all([
sdk.auth.getToken(),
sdk.auth.whoami(),
sdk.auth.getUserInfo(),
sdk.settings.get('balenaUrl'),
{
// opts.appName may be prefixed by 'owner/', unlike opts.app.app_name
@ -371,8 +371,8 @@ ${dockerignoreHelp}
$select: ['commit'],
});
} else {
const [userId, auth, apiEndpoint] = await Promise.all([
sdk.auth.getUserId(),
const [{ id: userId }, auth, apiEndpoint] = await Promise.all([
sdk.auth.getUserInfo(),
sdk.auth.getToken(),
sdk.settings.get('apiUrl'),
]);

View File

@ -20,7 +20,6 @@ import type { IArg } from '@oclif/parser/lib/args';
import type {
BalenaSDK,
Device,
DeviceType,
PineOptions,
PineTypedResult,
} from 'balena-sdk';
@ -138,7 +137,6 @@ export default class DeviceMoveCmd extends Command {
balena: BalenaSDK,
devices: Awaited<ReturnType<typeof this.getDevices>>,
) {
const { getExpandedProp } = await import('../../utils/pine');
// deduplicate the slugs
const deviceCpuArchs = Array.from(
new Set(
@ -148,48 +146,44 @@ export default class DeviceMoveCmd extends Command {
),
);
const deviceTypeOptions = {
$select: 'slug',
$expand: {
is_of__cpu_architecture: {
$select: 'slug',
},
const allCpuArches = await balena.pine.get({
resource: 'cpu_architecture',
options: {
$select: ['id', 'slug'],
},
} satisfies PineOptions<DeviceType>;
const deviceTypes = (await balena.models.deviceType.getAllSupported(
deviceTypeOptions,
)) as Array<PineTypedResult<DeviceType, typeof deviceTypeOptions>>;
});
const compatibleDeviceTypeSlugs = new Set(
deviceTypes
.filter((deviceType) => {
const deviceTypeArch = getExpandedProp(
deviceType.is_of__cpu_architecture,
'slug',
)!;
return deviceCpuArchs.every((deviceCpuArch) =>
balena.models.os.isArchitectureCompatibleWith(
deviceCpuArch,
deviceTypeArch,
),
);
})
.map((deviceType) => deviceType.slug),
);
const compatibleCpuArchIds = allCpuArches
.filter((cpuArch) => {
return deviceCpuArchs.every((deviceCpuArch) =>
balena.models.os.isArchitectureCompatibleWith(
deviceCpuArch,
cpuArch.slug,
),
);
})
.map((deviceType) => deviceType.id);
const patterns = await import('../../utils/patterns');
try {
const application = await patterns.selectApplication(
(app) =>
compatibleDeviceTypeSlugs.has(app.is_for__device_type[0].slug) &&
devices.some(
(device) => device.belongs_to__application.__id !== app.id,
),
{
is_for__device_type: {
$any: {
$alias: 'dt',
$expr: {
dt: {
is_of__cpu_architecture: { $in: compatibleCpuArchIds },
},
},
},
},
},
true,
);
return application;
} catch (err) {
if (!compatibleDeviceTypeSlugs.size) {
if (!compatibleCpuArchIds.length) {
throw new ExpectedError(
`${err.message}\nDo all devices have a compatible architecture?`,
);

View File

@ -268,6 +268,7 @@ async function getServiceIdForApp(
): Promise<number> {
let serviceId: number | undefined;
const services = await sdk.models.service.getAllByApplication(appSlug, {
$select: 'id',
$filter: { service_name: serviceName },
});
if (services.length > 0) {

View File

@ -93,7 +93,6 @@ export default class EnvsCmd extends Command {
'$ balena envs --fleet myorg/myfleet',
'$ balena envs --fleet MyFleet --json',
'$ balena envs --fleet MyFleet --service MyService',
'$ balena envs --fleet MyFleet --service MyService',
'$ balena envs --fleet MyFleet --config',
'$ balena envs --device 7cf02a6',
'$ balena envs --device 7cf02a6 --json',
@ -209,6 +208,7 @@ async function validateServiceName(
fleetSlug: string,
) {
const services = await sdk.models.service.getAllByApplication(fleetSlug, {
$select: 'id',
$filter: { service_name: serviceName },
});
if (services.length === 0) {

View File

@ -18,19 +18,9 @@
import { flags } from '@oclif/command';
import Command from '../../command';
import { ExpectedError } from '../../errors';
import * as cf from '../../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
interface FlagsDef {
organization?: string;
type?: string; // application device type
help: void;
}
interface ArgsDef {
name: string;
}
import { stripIndent } from '../../utils/lazy';
import { ArgsDef, FlagsDef } from '../../utils/application-create';
export default class FleetCreateCmd extends Command {
public static description = stripIndent`
@ -90,60 +80,8 @@ export default class FleetCreateCmd extends Command {
FleetCreateCmd,
);
// Ascertain device type
const deviceType =
options.type ||
(await (await import('../../utils/patterns')).selectDeviceType());
// Ascertain organization
const organization =
options.organization?.toLowerCase() || (await this.getOrganization());
// Create application
try {
const application = await getBalenaSdk().models.application.create({
name: params.name,
deviceType,
organization,
});
// Output
console.log(
`Fleet created: slug "${application.slug}", device type "${deviceType}"`,
);
} catch (err) {
if ((err.message || '').toLowerCase().includes('unique')) {
// BalenaRequestError: Request error: "organization" and "app_name" must be unique.
throw new ExpectedError(
`Error: An app or block or fleet with the name "${params.name}" already exists in organization "${organization}".`,
);
} else if ((err.message || '').toLowerCase().includes('unauthorized')) {
// BalenaRequestError: Request error: Unauthorized
throw new ExpectedError(
`Error: You are not authorized to create fleets in organization "${organization}".`,
);
}
throw err;
}
}
async getOrganization() {
const { getOwnOrganizations } = await import('../../utils/sdk');
const organizations = await getOwnOrganizations(getBalenaSdk(), {
$select: ['name', 'handle'],
});
if (organizations.length === 0) {
// User is not a member of any organizations (should not happen).
throw new Error('This account is not a member of any organizations');
} else if (organizations.length === 1) {
// User is a member of only one organization - use this.
return organizations[0].handle;
} else {
// User is a member of multiple organizations -
const { selectOrganization } = await import('../../utils/patterns');
return selectOrganization(organizations);
}
await (
await import('../../utils/application-create')
).applicationCreateBase('fleet', options, params);
}
}

View File

@ -15,6 +15,7 @@
* limitations under the License.
*/
import type * as BalenaSdk from 'balena-sdk';
import { flags } from '@oclif/command';
import Command from '../command';
@ -22,7 +23,7 @@ import * as cf from '../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../utils/lazy';
import type { DataSetOutputOptions } from '../framework';
interface ExtendedApplication extends ApplicationWithDeviceType {
interface ExtendedApplication extends ApplicationWithDeviceTypeSlug {
device_count: number;
online_devices: number;
device_type?: string;
@ -60,15 +61,20 @@ export default class FleetsCmd extends Command {
const balena = getBalenaSdk();
const pineOptions = {
$select: ['id', 'app_name', 'slug'],
$expand: {
is_for__device_type: { $select: 'slug' },
owns__device: { $select: 'is_online' },
},
} satisfies BalenaSdk.PineOptions<BalenaSdk.Application>;
// Get applications
const applications =
(await balena.models.application.getAllDirectlyAccessible({
$select: ['id', 'app_name', 'slug'],
$expand: {
is_for__device_type: { $select: 'slug' },
owns__device: { $select: 'is_online' },
},
})) as ExtendedApplication[];
(await balena.models.application.getAllDirectlyAccessible(
pineOptions,
)) as Array<
BalenaSdk.PineTypedResult<BalenaSdk.Application, typeof pineOptions>
> as ExtendedApplication[];
// Add extended properties
applications.forEach((application) => {

View File

@ -20,6 +20,7 @@ import Command from '../command';
import * as cf from '../utils/common-flags';
import { getBalenaSdk, stripIndent, getCliForm } from '../utils/lazy';
import { ExpectedError } from '../errors';
import type { WhoamiResult } from 'balena-sdk';
interface FlagsDef {
token: boolean;
@ -30,6 +31,7 @@ interface FlagsDef {
password?: string;
port?: number;
help: void;
hideExperimentalWarning: boolean;
}
interface ArgsDef {
@ -114,6 +116,11 @@ export default class LoginCmd extends Command {
'TCP port number of local HTTP login server (--web auth only)',
dependsOn: ['web'],
}),
hideExperimentalWarning: flags.boolean({
char: 'H',
default: false,
description: 'Hides warning for experimental features',
}),
help: cf.help,
};
@ -137,9 +144,24 @@ export default class LoginCmd extends Command {
console.log(`\nLogging in to ${balenaUrl}`);
await this.doLogin(options, balenaUrl, params.token);
const username = await balena.auth.whoami();
// 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;
console.info(`Successfully logged in as: ${username}`);
if (whoamiResult.actorType !== 'user' && !options.hideExperimentalWarning) {
console.info(stripIndent`
----------------------------------------------------------------------------------------
You are logging in with a ${whoamiResult.actorType} key.
This is an experimental feature and many features of the CLI might not work as expected.
We sure hope you know what you are doing.
----------------------------------------------------------------------------------------
`);
}
console.info(
`Successfully logged in as: ${this.getLoggedInMessage(whoamiResult)}`,
);
console.info(`\
Find out about the available commands by running:
@ -149,6 +171,16 @@ Find out about the available commands by running:
${messages.reachingOut}`);
}
private getLoggedInMessage(whoami: WhoamiResult): string {
if (whoami.actorType === 'user') {
return whoami.username;
}
const identifier =
whoami.actorType === 'device' ? whoami.uuid : whoami.slug;
return `${whoami.actorType} ${identifier}`;
}
async doLogin(
loginOptions: FlagsDef,
balenaUrl: string = 'balena-cloud.com',
@ -165,7 +197,14 @@ ${messages.reachingOut}`);
}
const balena = getBalenaSdk();
await balena.auth.loginWithToken(token!);
if (!(await balena.auth.whoami())) {
try {
if (!(await balena.auth.whoami())) {
throw new ExpectedError('Token authentication failed');
}
} catch (err) {
if (process.env.DEBUG) {
console.error(`Get user info failed with: ${err.message}`);
}
throw new ExpectedError('Token authentication failed');
}
return;

View File

@ -204,7 +204,7 @@ export default class OsConfigureCmd extends Command {
const helpers = await import('../../utils/helpers');
const { getApplication } = await import('../../utils/sdk');
let app: ApplicationWithDeviceType | undefined;
let app: ApplicationWithDeviceTypeSlug | undefined;
let device;
let deviceTypeSlug: string;
@ -223,7 +223,7 @@ export default class OsConfigureCmd extends Command {
$expand: {
is_for__device_type: { $select: 'slug' },
},
})) as ApplicationWithDeviceType;
})) as ApplicationWithDeviceTypeSlug;
await checkDeviceTypeCompatibility(options, app);
deviceTypeSlug =
options['device-type'] || app.is_for__device_type[0].slug;

View File

@ -46,7 +46,7 @@ interface FlagsDef extends DockerConnectionCliFlags {
commit?: string;
'splash-image'?: string;
'dont-check-arch': boolean;
'pin-device-to-release': boolean;
'pin-device-to-release'?: boolean;
'additional-space'?: number;
'add-certificate'?: string[];
help: void;
@ -122,7 +122,7 @@ https://github.com/balena-io-examples/staged-releases\
'disable architecture compatibility check between image and fleet',
}),
'pin-device-to-release': flags.boolean({
default: false,
allowNo: true,
description:
'pin the preloaded device to the preloaded release on provision',
char: 'p',
@ -230,7 +230,7 @@ Can be repeated to add multiple certificates.\
const splashImage = options['splash-image'];
const additionalSpace = options['additional-space'];
const dontCheckArch = options['dont-check-arch'] || false;
const pinDevice = options['pin-device-to-release'] || false;
const pinDevice = options['pin-device-to-release'];
if (dontCheckArch && !fleetSlug) {
throw new ExpectedError(
@ -257,7 +257,7 @@ Can be repeated to add multiple certificates.\
splashImage,
undefined, // TODO: Currently always undefined, investigate approach in ssh command.
dontCheckArch,
pinDevice,
pinDevice ?? false,
certificates,
additionalSpace,
);
@ -450,14 +450,14 @@ Can be repeated to add multiple certificates.\
async offerToDisableAutomaticUpdates(
application: Pick<Application, 'id' | 'should_track_latest_release'>,
commit: string,
pinDevice: boolean,
pinDevice: boolean | undefined,
) {
const balena = getBalenaSdk();
if (
this.isCurrentCommit(commit) ||
!application.should_track_latest_release ||
pinDevice
pinDevice != null
) {
return;
}
@ -476,8 +476,9 @@ through the web dashboard or programatically through the balena API / SDK.
Documentation about release policies and pinning can be found at:
https://www.balena.io/docs/learn/deploy/release-strategy/release-policy/
Alternatively, the --pin-device-to-release flag may be used to pin only the
preloaded device to the selected release.
Alternatively, the --pin-device-to-release or --no-pin-device-to-release flags may be used
to avoid this interactive confirmation and pin only the preloaded device to the selected release
or keep it unpinned respectively.
Would you like to disable automatic updates for this fleet now?\
`;
@ -511,7 +512,7 @@ Would you like to disable automatic updates for this fleet now?\
options: {
slug?: string;
commit?: string;
pinDevice: boolean;
pinDevice?: boolean;
},
) {
await preloader.prepare();

View File

@ -152,9 +152,9 @@ export default class SshCmd extends Command {
const { which } = await import('../utils/which');
const [whichProxytunnel, username, proxyUrl] = await Promise.all([
const [whichProxytunnel, { username }, proxyUrl] = await Promise.all([
useProxy ? which('proxytunnel', false) : undefined,
sdk.auth.whoami(),
sdk.auth.getUserInfo(),
// note that `proxyUrl` refers to the balenaCloud "resin-proxy"
// service, currently "balena-devices.com", rather than some
// local proxy server URL
@ -208,7 +208,7 @@ export default class SshCmd extends Command {
port: options.port || 'cloud',
proxyCommand,
service: params.service,
username: username!,
username,
});
}

View File

@ -36,18 +36,37 @@ export default class WhoamiCmd extends Command {
const balena = getBalenaSdk();
const [username, email, url] = await Promise.all([
const [whoamiResult, url] = await Promise.all([
balena.auth.whoami(),
balena.auth.getEmail(),
balena.settings.get('balenaUrl'),
]);
console.log(
getVisuals().table.vertical({ username, email, url }, [
'$account information$',
'username',
'email',
'url',
]),
);
if (whoamiResult?.actorType === 'user') {
const { username, email } = whoamiResult;
console.log(
getVisuals().table.vertical({ username, email, url }, [
'$account information$',
'username',
'email',
'url',
]),
);
} else if (whoamiResult?.actorType === 'device') {
console.log(
getVisuals().table.vertical({ device: whoamiResult.uuid, url }, [
'$account information$',
'device',
'url',
]),
);
} else if (whoamiResult?.actorType === 'application') {
console.log(
getVisuals().table.vertical({ application: whoamiResult.slug, url }, [
'$account information$',
'application',
'url',
]),
);
}
}
}

View File

@ -15,6 +15,8 @@
* limitations under the License.
*/
import type { BalenaSettingsStorage } from 'balena-settings-storage';
export interface ReleaseTimestampsByVersion {
[version: string]: string; // e.g. { '12.0.0': '2021-06-16T12:54:52.000Z' }
lastFetched: string; // ISO 8601 timestamp, e.g. '2021-06-27T16:46:10.000Z'
@ -46,7 +48,7 @@ export class DeprecationChecker {
readonly cacheFile = 'cachedReleaseTimestamps';
readonly now = new Date().getTime();
private initialized = false;
storage: ReturnType<typeof import('balena-settings-storage')>;
storage: BalenaSettingsStorage;
cachedTimestamps: ReleaseTimestampsByVersion;
nextMajorVersion: string; // semver without the 'v' prefix
@ -63,7 +65,7 @@ export class DeprecationChecker {
this.initialized = true;
const settings = await import('balena-settings-client');
const getStorage = await import('balena-settings-storage');
const { getStorage } = await import('balena-settings-storage');
const dataDirectory = settings.get<string>('dataDirectory');
this.storage = getStorage({ dataDirectory });
let stored: ReleaseTimestampsByVersion | undefined;

View File

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

View File

@ -17,6 +17,7 @@
import { Hook } from '@oclif/config';
import type { IConfig } from '@oclif/config';
import { getChalk } from '../../utils/lazy';
/*
A modified version of the command-not-found plugin logic,
@ -31,7 +32,7 @@ const hook: Hook<'command-not-found'> = async function (
) {
const Levenshtein = await import('fast-levenshtein');
const _ = await import('lodash');
const { color } = await import('@oclif/color');
const chalk = getChalk();
const commandId = opts.id || '';
const command = opts.id?.replace(':', ' ') || '';
@ -60,17 +61,19 @@ const hook: Hook<'command-not-found'> = async function (
// Output suggestions
console.error(
`${color.yellow(command)} is not a recognized balena command.\n`,
`${chalk.yellow(command)} is not a recognized balena command.\n`,
);
console.error(`Did you mean: ? `);
suggestions.forEach((s) => {
console.error(` ${color.cmd(s)}`);
console.error(` ${chalk.cyan.bold(s)}`);
});
console.error(
`\nRun ${color.cmd('balena help -v')} for a list of available commands,`,
`\nRun ${chalk.cyan.bold(
'balena help -v',
)} for a list of available commands,`,
);
console.error(
` or ${color.cmd(
` or ${chalk.cyan.bold(
'balena help <command>',
)} for detailed help on a specific command.`,
);

View File

@ -133,7 +133,6 @@ Please use "balena ${alternative}" instead.`);
'local stop': [removed, stopAlternative, 'v11.0.0'],
app: [replaced, 'fleet', 'v13.0.0'],
apps: [replaced, 'fleets', 'v13.0.0'],
'app create': [replaced, 'fleet create', 'v13.0.0'],
'app purge': [replaced, 'fleet purge', 'v13.0.0'],
'app rename': [replaced, 'fleet rename', 'v13.0.0'],
'app restart': [replaced, 'fleet restart', 'v13.0.0'],

View File

@ -0,0 +1,58 @@
import { ExpectedError } from '../errors';
import { getBalenaSdk } from './lazy';
export interface FlagsDef {
organization?: string;
type?: string; // application device type
help: void;
}
export interface ArgsDef {
name: string;
}
export async function applicationCreateBase(
resource: 'fleet' | 'app' | 'block',
options: FlagsDef,
params: ArgsDef,
) {
// Ascertain device type
const deviceType =
options.type || (await (await import('./patterns')).selectDeviceType());
// Ascertain organization
const organization =
options.organization?.toLowerCase() ||
(await (await import('./patterns')).getAndSelectOrganization());
// Create application
try {
const application = await getBalenaSdk().models.application.create({
name: params.name,
deviceType,
organization,
});
// Output
const { capitalize } = await import('lodash');
console.log(
`${capitalize(resource)} created: slug "${
application.slug
}", device type "${deviceType}"`,
);
} catch (err) {
if ((err.message || '').toLowerCase().includes('unique')) {
// BalenaRequestError: Request error: "organization" and "app_name" must be unique.
throw new ExpectedError(
`Error: An app or block or fleet with the name "${params.name}" already exists in organization "${organization}".`,
);
} else if ((err.message || '').toLowerCase().includes('unauthorized')) {
// BalenaRequestError: Request error: Unauthorized
throw new ExpectedError(
`Error: You are not authorized to create ${resource}s in organization "${organization}".`,
);
}
throw err;
}
}

View File

@ -138,7 +138,7 @@ export async function getCachedUsername(): Promise<CachedUsername | undefined> {
if (cachedUsername) {
return cachedUsername;
}
const [{ getBalenaSdk }, getStorage, settings] = await Promise.all([
const [{ getBalenaSdk }, { getStorage }, settings] = await Promise.all([
import('./lazy'),
import('balena-settings-storage'),
import('balena-settings-client'),
@ -167,7 +167,7 @@ export async function getCachedUsername(): Promise<CachedUsername | undefined> {
// ignore
}
try {
const username = await getBalenaSdk().auth.whoami();
const { username } = await getBalenaSdk().auth.getUserInfo();
if (username) {
cachedUsername = { token, username };
await storage.set('cachedUsername', cachedUsername);

View File

@ -588,7 +588,7 @@ async function assignDockerBuildOpts(
pull: opts.pull,
};
if (task.external) {
task.dockerOpts.authconfig = await getAuthConfigObj(
task.dockerOpts.authconfig = getAuthConfigObj(
task.imageName!,
opts.registrySecrets,
);

View File

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

View File

@ -19,10 +19,10 @@ import type {
BalenaSDK,
Device,
Organization,
PineFilter,
PineOptions,
PineTypedResult,
} from 'balena-sdk';
import _ = require('lodash');
import { instanceOf, NotLoggedInError, ExpectedError } from '../errors';
import { getBalenaSdk, getVisuals, stripIndent, getCliForm } from './lazy';
@ -115,22 +115,22 @@ export function askLoginType() {
});
}
export function selectDeviceType() {
return getBalenaSdk()
.models.config.getDeviceTypes()
.then((deviceTypes) => {
deviceTypes = _.sortBy(deviceTypes, 'name').filter(
(dt) => dt.state !== 'DISCONTINUED',
);
return getCliForm().ask({
message: 'Device Type',
type: 'list',
choices: _.map(deviceTypes, ({ slug: value, name }) => ({
name,
value,
})),
});
});
export async function selectDeviceType() {
const sdk = getBalenaSdk();
let deviceTypes = await sdk.models.deviceType.getAllSupported();
if (deviceTypes.length === 0) {
// Without this open-balena users would get an empty list
// until we add a hostApps import in open-balena.
deviceTypes = await sdk.models.deviceType.getAll();
}
return getCliForm().ask({
message: 'Device Type',
type: 'list',
choices: deviceTypes.map(({ slug: value, name }) => ({
name,
value,
})),
});
}
/**
@ -179,27 +179,32 @@ type SelectApplicationResult = PineTypedResult<
>;
export async function selectApplication(
filter?: (app: SelectApplicationResult) => boolean,
filter?:
| PineFilter<Application>
| ((app: SelectApplicationResult) => boolean),
errorOnEmptySelection = false,
) {
const balena = getBalenaSdk();
const apps = (await balena.models.application.getAllDirectlyAccessible(
selectApplicationPineOptions,
)) as SelectApplicationResult[];
let apps = (await balena.models.application.getAllDirectlyAccessible({
...selectApplicationPineOptions,
...(filter != null && typeof filter === 'object' && { $filter: filter }),
})) as SelectApplicationResult[];
if (!apps.length) {
throw new ExpectedError('No fleets found');
}
const applications = filter ? apps.filter(filter) : apps;
if (filter != null && typeof filter === 'function') {
apps = apps.filter(filter);
}
if (errorOnEmptySelection && applications.length === 0) {
if (errorOnEmptySelection && apps.length === 0) {
throw new ExpectedError('No suitable fleets found for selection');
}
return getCliForm().ask({
message: 'Select an application',
type: 'list',
choices: _.map(applications, (application) => ({
choices: apps.map((application) => ({
name: `${application.app_name} (${application.slug}) [${application.is_for__device_type[0].slug}]`,
value: application,
})),
@ -223,6 +228,24 @@ export async function selectOrganization(
});
}
export async function getAndSelectOrganization() {
const { getOwnOrganizations } = await import('./sdk');
const organizations = await getOwnOrganizations(getBalenaSdk(), {
$select: ['name', 'handle'],
});
if (organizations.length === 0) {
// User is not a member of any organizations (should not happen).
throw new Error('This account is not a member of any organizations');
} else if (organizations.length === 1) {
// User is a member of only one organization - use this.
return organizations[0].handle;
} else {
// User is a member of multiple organizations -
return selectOrganization(organizations);
}
}
export async function awaitDeviceOsUpdate(
uuid: string,
targetOsVersion: string,
@ -338,7 +361,7 @@ export async function getOnlineTargetDeviceUuid(
const devices = application.owns__device;
// Throw if no devices online
if (_.isEmpty(devices)) {
if (!devices.length) {
throw new ExpectedError(
`Fleet ${application.slug} found, but has no devices online.`,
);
@ -349,7 +372,7 @@ export async function getOnlineTargetDeviceUuid(
message: `Select a device on fleet ${application.slug}`,
type: 'list',
default: devices[0].uuid,
choices: _.map(devices, (device) => ({
choices: devices.map((device) => ({
name: `${device.device_name || 'Untitled'} (${device.uuid.slice(0, 7)})`,
value: device.uuid,
})),
@ -363,7 +386,7 @@ export function selectFromList<T>(
return getCliForm().ask<T>({
message,
type: 'list',
choices: _.map(choices, (s) => ({
choices: choices.map((s) => ({
name: s.name,
value: s,
})),

View File

@ -228,8 +228,8 @@ async function selectLocalDevice(): Promise<string> {
}
async function selectAppFromList(
applications: ApplicationWithDeviceType[],
): Promise<ApplicationWithDeviceType> {
applications: ApplicationWithDeviceTypeSlug[],
): Promise<ApplicationWithDeviceTypeSlug> {
const _ = await import('lodash');
const { selectFromList } = await import('../utils/patterns');
@ -247,7 +247,7 @@ async function getOrSelectApplication(
sdk: BalenaSdk.BalenaSDK,
deviceTypeSlug: string,
appName?: string,
): Promise<ApplicationWithDeviceType> {
): Promise<ApplicationWithDeviceTypeSlug> {
const pineOptions = {
$select: 'slug',
$expand: {
@ -256,51 +256,72 @@ async function getOrSelectApplication(
},
},
} satisfies BalenaSdk.PineOptions<BalenaSdk.DeviceType>;
const [deviceType, allDeviceTypes] = await Promise.all([
sdk.models.deviceType.get(deviceTypeSlug, pineOptions) as Promise<
BalenaSdk.PineTypedResult<BalenaSdk.DeviceType, typeof pineOptions>
>,
sdk.models.deviceType.getAllSupported(pineOptions) as Promise<
Array<BalenaSdk.PineTypedResult<BalenaSdk.DeviceType, typeof pineOptions>>
>,
]);
const deviceType = (await sdk.models.deviceType.get(
deviceTypeSlug,
pineOptions,
)) as BalenaSdk.PineTypedResult<BalenaSdk.DeviceType, typeof pineOptions>;
const allCpuArches = await sdk.pine.get({
resource: 'cpu_architecture',
options: {
$select: ['id', 'slug'],
},
});
const compatibleDeviceTypes = allDeviceTypes
.filter((dt) =>
const compatibleCpuArchIds = allCpuArches
.filter((cpuArch) =>
sdk.models.os.isArchitectureCompatibleWith(
deviceType.is_of__cpu_architecture[0].slug,
dt.is_of__cpu_architecture[0].slug,
cpuArch.slug,
),
)
.map((type) => type.slug);
.map((cpu) => cpu.id);
if (!appName) {
return createOrSelectApp(sdk, compatibleDeviceTypes, deviceTypeSlug);
return createOrSelectApp(
sdk,
{
is_for__device_type: {
$any: {
$alias: 'dt',
$expr: {
dt: {
is_of__cpu_architecture: { $in: compatibleCpuArchIds },
},
},
},
},
},
deviceTypeSlug,
);
}
const options: BalenaSdk.PineOptions<BalenaSdk.Application> = {
const options = {
$expand: {
is_for__device_type: { $select: 'slug' },
is_for__device_type: { $select: ['slug', 'is_of__cpu_architecture'] },
},
};
} satisfies BalenaSdk.PineOptions<BalenaSdk.Application>;
// Check for a fleet slug of the form `user/fleet` and update the API query.
let name: string;
const match = appName.split('/');
if (match.length > 1) {
// These will match at most one fleet
options.$filter = { slug: appName.toLowerCase() };
(options as BalenaSdk.PineOptions<BalenaSdk.Application>).$filter = {
slug: appName.toLowerCase(),
};
name = match[1];
} else {
// We're given an application; resolve it if it's ambiguous and also validate
// it's of appropriate device type.
options.$filter = { app_name: appName };
(options as BalenaSdk.PineOptions<BalenaSdk.Application>).$filter = {
app_name: appName,
};
name = appName;
}
const applications = (await sdk.models.application.getAllDirectlyAccessible(
options,
)) as ApplicationWithDeviceType[];
)) as Array<BalenaSdk.PineTypedResult<BalenaSdk.Application, typeof options>>;
if (applications.length === 0) {
await confirm(
@ -315,8 +336,11 @@ async function getOrSelectApplication(
// We've found at least one fleet with the given name.
// Filter out fleets for non-matching device types and see what we're left with.
const compatibleCpuArchIdsSet = new Set(compatibleCpuArchIds);
const validApplications = applications.filter((app) =>
compatibleDeviceTypes.includes(app.is_for__device_type[0].slug),
compatibleCpuArchIdsSet.has(
app.is_for__device_type[0].is_of__cpu_architecture.__id,
),
);
if (validApplications.length === 0) {
@ -332,21 +356,14 @@ async function getOrSelectApplication(
async function createOrSelectApp(
sdk: BalenaSdk.BalenaSDK,
compatibleDeviceTypes: string[],
compatibleDeviceTypesFilter: BalenaSdk.PineFilter<BalenaSdk.Application>,
deviceType: string,
): Promise<ApplicationWithDeviceType> {
): Promise<ApplicationWithDeviceTypeSlug> {
// No fleet specified, show a list to select one.
const applications = (await sdk.models.application.getAllDirectlyAccessible({
$expand: { is_for__device_type: { $select: 'slug' } },
$filter: {
is_for__device_type: {
$any: {
$alias: 'dt',
$expr: { dt: { slug: { $in: compatibleDeviceTypes } } },
},
},
},
})) as ApplicationWithDeviceType[];
$filter: compatibleDeviceTypesFilter,
})) as ApplicationWithDeviceTypeSlug[];
if (applications.length === 0) {
await confirm(
@ -366,11 +383,14 @@ async function createApplication(
sdk: BalenaSdk.BalenaSDK,
deviceType: string,
name?: string,
): Promise<ApplicationWithDeviceType> {
): Promise<ApplicationWithDeviceTypeSlug> {
const validation = await import('./validation');
const username = await sdk.auth.whoami();
if (!username) {
let username: string;
try {
const userInfo = await sdk.auth.getUserInfo();
username = userInfo.username;
} catch (err) {
throw new sdk.errors.BalenaNotLoggedIn();
}
@ -387,7 +407,7 @@ async function createApplication(
try {
await sdk.models.application.getDirectlyAccessible(appName, {
$filter: {
slug: { $startswith: `${username!.toLowerCase()}/` },
slug: { $startswith: `${username.toLowerCase()}/` },
},
});
// TODO: This is the only example in the codebase where `printErrorMessage()`
@ -414,12 +434,12 @@ async function createApplication(
$expand: {
is_for__device_type: { $select: 'slug' },
},
})) as ApplicationWithDeviceType;
})) as ApplicationWithDeviceTypeSlug;
}
async function generateApplicationConfig(
sdk: BalenaSdk.BalenaSDK,
app: ApplicationWithDeviceType,
app: ApplicationWithDeviceTypeSlug,
options: {
version: string;
appUpdatePollInterval?: number;

View File

@ -167,7 +167,7 @@ async function handleHeadlessBuildStream(
// been started
let message: HeadlessBuilderMessage;
try {
const response = await streamToPromise(stream as NodeJS.ReadWriteStream);
const response = await streamToPromise(stream as NodeJS.ReadStream);
message = JSON.parse(response.toString());
} catch (e) {
if (e.code === 'SIGINT') {
@ -419,7 +419,7 @@ async function getRemoteBuildStream(
if (build.opts.headless) {
stream = buildRequest;
} else {
stream = buildRequest.pipe(JSONStream.parse('*'));
stream = buildRequest.pipe(JSONStream.parse('*')) as NodeJS.ReadStream;
}
stream = stream
.once('error', () => uploadSpinner.stop())

View File

@ -43,6 +43,29 @@ export async function getApplication(
options?: PineOptions<Application>,
): Promise<Application> {
const { looksLikeFleetSlug } = await import('./validation');
const whoamiResult = await sdk.auth.whoami();
const isDeviceActor = whoamiResult?.actorType === 'device';
if (isDeviceActor) {
const $filterByActor = {
$filter: {
owns__device: {
$any: {
$alias: 'd',
$expr: {
d: {
actor: whoamiResult.id,
},
},
},
},
},
};
options = options
? sdk.utils.mergePineOptions(options, $filterByActor)
: $filterByActor;
}
if (
typeof nameOrSlugOrId === 'string' &&
!looksLikeFleetSlug(nameOrSlugOrId)
@ -52,13 +75,15 @@ export async function getApplication(
return await sdk.models.application.getAppByName(
nameOrSlugOrId,
options,
'directly_accessible',
isDeviceActor ? undefined : 'directly_accessible',
);
}
return await sdk.models.application.getDirectlyAccessible(
nameOrSlugOrId,
options,
);
const getFunction = isDeviceActor
? sdk.models.application.get
: sdk.models.application.getDirectlyAccessible;
return getFunction(nameOrSlugOrId, options);
}
/**
@ -105,7 +130,7 @@ export async function getOwnOrganizations(
$alias: 'orm',
$expr: {
orm: {
user: await sdk.auth.getUserId(),
user: (await sdk.auth.getUserInfo()).id,
},
},
},

View File

@ -51,7 +51,7 @@ export const tunnelConnectionToDevice = (
sdk.auth.getToken(),
]).then(([tunnelUrl, whoami, token]) => {
const auth = {
user: whoami || 'root',
user: whoami?.actorType === 'user' ? whoami.username : 'root',
password: token,
};

19542
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "balena-cli",
"version": "16.6.3",
"version": "17.1.1",
"description": "The official balena Command Line Interface",
"main": "./build/app.js",
"homepage": "https://github.com/balena-io/balena-cli",
@ -36,7 +36,6 @@
"node_modules/balena-sdk/node_modules/balena-pine/**/*",
"node_modules/balena-pine/**/*",
"node_modules/pinejs-client-core/**/*",
"node_modules/opn/xdg-open",
"node_modules/open/xdg-open",
"node_modules/windosu/*.bat",
"node_modules/windosu/*.cmd",
@ -89,7 +88,7 @@
"author": "Balena Inc. (https://balena.io/)",
"license": "Apache-2.0",
"engines": {
"node": ">=16 <18"
"node": ">=18 <20"
},
"husky": {
"hooks": {
@ -107,10 +106,7 @@
"macos": {
"identifier": "io.balena.cli",
"sign": "Developer ID Installer: Balena Ltd (66H43P8FRG)"
},
"plugins": [
"@oclif/plugin-help"
]
}
},
"devDependencies": {
"@balena/lint": "^6.2.2",
@ -147,7 +143,7 @@
"@types/ndjson": "^2.0.1",
"@types/net-keepalive": "^0.4.1",
"@types/nock": "^11.1.0",
"@types/node": "^16.18.25",
"@types/node": "^18.17.6",
"@types/node-cleanup": "^2.1.2",
"@types/parse-link-header": "^1.0.1",
"@types/prettyjson": "^0.0.30",
@ -190,13 +186,15 @@
"simple-git": "^3.14.1",
"sinon": "^11.1.2",
"ts-node": "^10.4.0",
"oclif": "^3.9.1",
"typescript": "^5.1.3"
},
"dependencies": {
"@balena/compose": "^2.2.1",
"@balena/compose": "^3.0.2",
"@balena/dockerignore": "^1.0.2",
"@balena/es-version": "^1.0.1",
"@oclif/command": "^1.8.16",
"@oclif/core": "^2.15.0",
"@resin.io/valid-email": "^0.1.0",
"@sentry/node": "^6.16.1",
"@types/fast-levenshtein": "0.0.1",
@ -205,18 +203,17 @@
"balena-device-init": "^6.0.0",
"balena-errors": "^4.7.3",
"balena-image-fs": "^7.0.6",
"balena-image-manager": "^9.0.0",
"balena-preload": "^14.0.0",
"balena-sdk": "^17.0.0",
"balena-image-manager": "^9.0.2",
"balena-preload": "^14.0.2",
"balena-sdk": "^18.0.0",
"balena-semver": "^2.3.0",
"balena-settings-client": "^5.0.2",
"balena-settings-storage": "^7.0.0",
"balena-settings-storage": "^8.1.0",
"bluebird": "^3.7.2",
"body-parser": "^1.19.1",
"chalk": "^3.0.0",
"chokidar": "^3.5.2",
"cli-truncate": "^2.1.0",
"cli-ux": "^5.5.1",
"color-hash": "^1.1.1",
"columnify": "^1.5.2",
"common-tags": "^1.7.2",
@ -225,7 +222,7 @@
"docker-progress": "^5.1.3",
"dockerode": "3.3.3",
"ejs": "^3.1.6",
"etcher-sdk": "^8.5.3",
"etcher-sdk": "^8.7.0",
"event-stream": "3.3.4",
"express": "^4.17.2",
"fast-boot2": "^1.1.0",
@ -252,7 +249,6 @@
"net-keepalive": "^3.0.0",
"node-cleanup": "^2.1.2",
"node-unzip-2": "^0.2.8",
"oclif": "^1.18.4",
"open": "^7.1.0",
"patch-package": "^6.4.7",
"prettyjson": "^1.2.5",
@ -284,6 +280,6 @@
"windosu": "^0.3.0"
},
"versionist": {
"publishedAt": "2023-06-30T17:07:33.152Z"
"publishedAt": "2023-09-05T15:06:11.726Z"
}
}

View File

@ -0,0 +1,49 @@
diff --git a/node_modules/@oclif/core/lib/help/command.js b/node_modules/@oclif/core/lib/help/command.js
index 6de139b..3a13197 100644
--- a/node_modules/@oclif/core/lib/help/command.js
+++ b/node_modules/@oclif/core/lib/help/command.js
@@ -206,7 +206,7 @@ class CommandHelp extends formatter_1.HelpFormatter {
if (args.filter(a => a.description).length === 0)
return;
return args.map(a => {
- const name = a.name.toUpperCase();
+ const name = a.required ? `<${a.name}>` : `[${a.name}]`;
let description = a.description || '';
if (a.default)
description = `[default: ${a.default}] ${description}`;
@@ -238,14 +238,12 @@ class CommandHelp extends formatter_1.HelpFormatter {
label = labels.join(', ');
}
if (flag.type === 'option') {
- let value = flag.helpValue || (this.opts.showFlagNameInTitle ? flag.name : '<value>');
+ let value = flag.helpValue || (this.opts.showFlagNameInTitle ? flag.name : `<${flag.name}>`);
if (!flag.helpValue && flag.options) {
value = showOptions || this.opts.showFlagOptionsInTitle ? `${flag.options.join('|')}` : '<option>';
}
if (flag.multiple)
- value += '...';
- if (!value.includes('|'))
- value = underline(value);
+ value += ' ...';
label += `=${value}`;
}
return label;
diff --git a/node_modules/@oclif/core/lib/help/index.js b/node_modules/@oclif/core/lib/help/index.js
index f9ef7cc..a14c67c 100644
--- a/node_modules/@oclif/core/lib/help/index.js
+++ b/node_modules/@oclif/core/lib/help/index.js
@@ -136,11 +136,12 @@ class Help extends HelpBase {
}
this.log(this.formatCommand(command));
this.log('');
- if (subTopics.length > 0) {
+ const SUPPRESS_SUBTOPICS = true;
+ if (subTopics.length > 0 && !SUPPRESS_SUBTOPICS) {
this.log(this.formatTopics(subTopics));
this.log('');
}
- if (subCommands.length > 0) {
+ if (subCommands.length > 0 && !SUPPRESS_SUBTOPICS) {
const aliases = [];
const uniqueSubCommands = subCommands.filter(p => {
aliases.push(...p.aliases);

View File

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

View File

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

View File

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

View File

@ -1,15 +0,0 @@
diff --git a/node_modules/opn/index.js b/node_modules/opn/index.js
index 13dcb66..0f0c1df 100644
--- a/node_modules/opn/index.js
+++ b/node_modules/opn/index.js
@@ -51,7 +51,9 @@ module.exports = function (target, opts) {
if (opts.app) {
cmd = opts.app;
} else {
- cmd = path.join(__dirname, 'xdg-open');
+ cmd = process.pkg
+ ? path.join(path.dirname(process.execPath), 'xdg-open-402')
+ : path.join(__dirname, 'xdg-open');
}
if (appArgs.length > 0) {

View File

@ -95,7 +95,7 @@ describe('balena envs', function () {
it('should successfully list service variables for a test fleet (-s flag)', async () => {
const serviceName = 'service2';
api.expectGetService({ serviceName });
api.expectGetServiceFromApp({ serviceName });
api.expectGetApplication();
api.expectGetAppEnvVars();
api.expectGetAppServiceVars();
@ -117,7 +117,7 @@ describe('balena envs', function () {
it('should successfully list env and service vars for a test fleet (-s flags)', async () => {
const serviceName = 'service1';
api.expectGetService({ serviceName });
api.expectGetServiceFromApp({ serviceName });
api.expectGetApplication();
api.expectGetAppEnvVars();
api.expectGetAppServiceVars();
@ -216,7 +216,7 @@ describe('balena envs', function () {
it('should successfully list service variables for a test device (-s flag)', async () => {
const serviceName = 'service2';
api.expectGetService({ serviceName });
api.expectGetServiceFromApp({ serviceName });
api.expectGetApplication();
api.expectGetDevice({ shortUUID, fullUUID });
api.expectGetDevice({ fullUUID });
@ -269,7 +269,7 @@ describe('balena envs', function () {
it('should successfully list env and service vars for a test device (-s flags)', async () => {
const serviceName = 'service1';
api.expectGetService({ serviceName });
api.expectGetServiceFromApp({ serviceName });
api.expectGetApplication();
api.expectGetAppEnvVars();
api.expectGetAppServiceVars();
@ -299,7 +299,7 @@ describe('balena envs', function () {
it('should successfully list env and service vars for a test device (-js flags)', async () => {
const serviceName = 'service1';
api.expectGetService({ serviceName });
api.expectGetServiceFromApp({ serviceName });
api.expectGetApplication();
api.expectGetAppEnvVars();
api.expectGetAppServiceVars();

View File

@ -0,0 +1,75 @@
/**
* @license
* Copyright 2019-2023 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { expect } from 'chai';
import { BalenaAPIMock } from '../nock/balena-api-mock';
import { cleanOutput, runCommand } from '../helpers';
describe('balena whoami', function () {
let api: BalenaAPIMock;
this.beforeEach(() => {
api = new BalenaAPIMock();
api.expectGetMixpanel({ optional: true });
});
this.afterEach(async () => {
// Check all expected api calls have been made and clean up.
api.done();
});
it(`should output login required message if haven't logged in`, async () => {
api.expectWhoAmIFail();
const { err, out } = await runCommand('whoami');
expect(out).to.be.empty;
expect(err[0]).to.include('Login required');
});
it('should display device with device response', async () => {
api.expectDeviceWhoAmI();
const { err, out } = await runCommand('whoami');
const lines = cleanOutput(out);
expect(lines[0]).to.contain('== ACCOUNT INFORMATION');
expect(lines[1]).to.contain('DEVICE: a11dc1acd31b623a0e4e084a6cf13aaa');
expect(lines[2]).to.contain('URL: balena-cloud.com');
expect(err).to.be.empty;
});
it('should display application with application response', async () => {
api.expectApplicationWhoAmI();
const { err, out } = await runCommand('whoami');
const lines = cleanOutput(out);
expect(lines[0]).to.contain('== ACCOUNT INFORMATION');
expect(lines[1]).to.contain('APPLICATION: mytestorf/mytestfleet');
expect(lines[2]).to.contain('URL: balena-cloud.com');
expect(err).to.be.empty;
});
it('should display user with user response', async () => {
api.expectGetWhoAmI();
const { err, out } = await runCommand('whoami');
const lines = cleanOutput(out);
expect(lines[0]).to.contain('== ACCOUNT INFORMATION');
expect(lines[1]).to.contain('USERNAME: gh_user');
expect(lines[2]).to.contain('EMAIL: testuser@test.com');
expect(lines[3]).to.contain('URL: balena-cloud.com');
expect(err).to.be.empty;
});
});

View File

@ -16,7 +16,7 @@
*/
import * as settings from 'balena-settings-client';
import * as getStorage from 'balena-settings-storage';
import { getStorage } from 'balena-settings-storage';
import { expect } from 'chai';
import mock = require('mock-require');
import * as semver from 'semver';
@ -78,7 +78,7 @@ describe('DeprecationChecker', function () {
.stub(mockStorage, 'set')
.withArgs(checker.cacheFile, sinon.match.any);
mock(storageModPath, () => mockStorage);
mock(storageModPath, { getStorage: () => mockStorage });
});
this.afterEach(() => {

View File

@ -386,6 +386,21 @@ export class BalenaAPIMock extends NockMock {
});
}
public expectGetServiceFromApp(opts: {
optional?: boolean;
persist?: boolean;
serviceId?: number;
serviceName: string;
}) {
const serviceId = opts.serviceId || 243768;
this.optGet(/^\/v6\/application($|\?).*\$expand=service.*/, opts).reply(
200,
{
d: [{ service: [{ id: serviceId, service_name: opts.serviceName }] }],
},
);
}
public expectPostService409(opts: ScopeOpts = {}) {
this.optPost(/^\/v\d+\/service$/, opts).reply(
409,
@ -415,13 +430,37 @@ export class BalenaAPIMock extends NockMock {
// User details are cached in the SDK
// so often we don't know if we can expect the whoami request
public expectGetWhoAmI(opts: ScopeOpts = { optional: true }) {
this.optGet('/user/v1/whoami', opts).reply(200, {
id: 99999,
this.optGet('/actor/v1/whoami', opts).reply(200, {
id: 1234,
actorType: 'user',
actorTypeId: 99999,
username: 'gh_user',
email: 'testuser@test.com',
});
}
public expectDeviceWhoAmI(opts: ScopeOpts = { optional: true }) {
this.optGet('/actor/v1/whoami', opts).reply(200, {
id: 1235,
actorType: 'device',
actorTypeId: 88888,
uuid: 'a11dc1acd31b623a0e4e084a6cf13aaa',
});
}
public expectApplicationWhoAmI(opts: ScopeOpts = { optional: true }) {
this.optGet('/actor/v1/whoami', opts).reply(200, {
id: 1236,
actorType: 'application',
actorTypeId: 77777,
slug: 'mytestorf/mytestfleet',
});
}
public expectWhoAmIFail(opts: ScopeOpts = { optional: true }) {
this.optGet('/actor/v1/whoami', opts).reply(401);
}
public expectGetMixpanel(opts: ScopeOpts = {}) {
this.optGet(/^\/mixpanel\/track/, opts).reply(200, {});
}

View File

@ -76,16 +76,9 @@
The file must be distributed with executable as %2.
%1: node_modules/drivelist/scripts/win32.bat
%2: path-to-executable/drivelist/win32.bat
> Warning Cannot include file %1 into executable.
The file must be distributed with executable as %2.
%1: node_modules/opn/xdg-open
%2: path-to-executable/xdg-open
> Warning Cannot include file %1 into executable.
The file must be distributed with executable as %2.
%1: node_modules/opn/xdg-open
%2: path-to-executable/xdg-open
prebuild-install warn install No prebuilt binaries found (target=v16.16.0 runtime=node arch=x64 libc= platform=darwin)
prebuild-install warn install No prebuilt binaries found (target=v16.16.0 runtime=node arch=x64 libc= platform=darwin)
prebuild-install warn install No prebuilt binaries found (target=v16.16.0 runtime=node arch=x64 libc= platform=darwin)
prebuild-install warn install No prebuilt binaries found (target=v16.16.0 runtime=node arch=x64 libc= platform=darwin)
prebuild-install warn install No prebuilt binaries found (target=v16.16.0 runtime=node arch=x64 libc= platform=darwin)
prebuild-install warn install No prebuilt binaries found (target=v18.5.0 runtime=node arch=x64 libc= platform=darwin)
prebuild-install warn install No prebuilt binaries found (target=v18.5.0 runtime=node arch=x64 libc= platform=darwin)
prebuild-install warn install No prebuilt binaries found (target=v18.5.0 runtime=node arch=x64 libc= platform=darwin)
prebuild-install warn install No prebuilt binaries found (target=v18.5.0 runtime=node arch=x64 libc= platform=darwin)
prebuild-install warn install No prebuilt binaries found (target=v18.5.0 runtime=node arch=x64 libc= platform=darwin)

View File

@ -76,15 +76,8 @@
The file must be distributed with executable as %2.
%1: node_modules/drivelist/scripts/win32.bat
%2: path-to-executable/drivelist/win32.bat
> Warning Cannot include file %1 into executable.
The file must be distributed with executable as %2.
%1: node_modules/opn/xdg-open
%2: path-to-executable/xdg-open
> Warning Cannot include file %1 into executable.
The file must be distributed with executable as %2.
%1: node_modules/opn/xdg-open
%2: path-to-executable/xdg-open
prebuild-install warn install No prebuilt binaries found (target=v16.16.0 runtime=node arch=arm64 libc= platform=linux)
prebuild-install warn install No prebuilt binaries found (target=v16.16.0 runtime=node arch=arm64 libc= platform=linux)
prebuild-install warn install No prebuilt binaries found (target=v16.16.0 runtime=node arch=arm64 libc= platform=linux)
prebuild-install warn install No prebuilt binaries found (target=v16.16.0 runtime=node arch=arm64 libc= platform=linux)
prebuild-install warn install No prebuilt binaries found (target=v18.5.0 runtime=node arch=arm64 libc= platform=linux)
prebuild-install warn install No prebuilt binaries found (target=v18.5.0 runtime=node arch=arm64 libc= platform=linux)
prebuild-install warn install No prebuilt binaries found (target=v18.5.0 runtime=node arch=arm64 libc= platform=linux)
prebuild-install warn install No prebuilt binaries found (target=v18.5.0 runtime=node arch=arm64 libc= platform=linux)

View File

@ -76,15 +76,8 @@
The file must be distributed with executable as %2.
%1: node_modules/drivelist/scripts/win32.bat
%2: path-to-executable/drivelist/win32.bat
> Warning Cannot include file %1 into executable.
The file must be distributed with executable as %2.
%1: node_modules/opn/xdg-open
%2: path-to-executable/xdg-open
> Warning Cannot include file %1 into executable.
The file must be distributed with executable as %2.
%1: node_modules/opn/xdg-open
%2: path-to-executable/xdg-open
prebuild-install warn install No prebuilt binaries found (target=v16.16.0 runtime=node arch=x64 libc= platform=linux)
prebuild-install warn install No prebuilt binaries found (target=v16.16.0 runtime=node arch=x64 libc= platform=linux)
prebuild-install warn install No prebuilt binaries found (target=v16.16.0 runtime=node arch=x64 libc= platform=linux)
prebuild-install warn install No prebuilt binaries found (target=v16.16.0 runtime=node arch=x64 libc= platform=linux)
prebuild-install warn install No prebuilt binaries found (target=v18.5.0 runtime=node arch=x64 libc= platform=linux)
prebuild-install warn install No prebuilt binaries found (target=v18.5.0 runtime=node arch=x64 libc= platform=linux)
prebuild-install warn install No prebuilt binaries found (target=v18.5.0 runtime=node arch=x64 libc= platform=linux)
prebuild-install warn install No prebuilt binaries found (target=v18.5.0 runtime=node arch=x64 libc= platform=linux)

View File

@ -76,11 +76,3 @@
The file must be distributed with executable as %2.
%1: node_modules\drivelist\scripts\win32.bat
%2: path-to-executable/drivelist/win32.bat
> Warning Cannot include file %1 into executable.
The file must be distributed with executable as %2.
%1: node_modules\opn\xdg-open
%2: path-to-executable/xdg-open
> Warning Cannot include file %1 into executable.
The file must be distributed with executable as %2.
%1: node_modules\opn\xdg-open
%2: path-to-executable/xdg-open

7
typings/global.d.ts vendored
View File

@ -1,8 +1,11 @@
import { Application, DeviceType, Device } from 'balena-sdk';
declare global {
type ApplicationWithDeviceType = Application & {
is_for__device_type: [DeviceType];
type ApplicationWithDeviceTypeSlug = Omit<
Application,
'is_for__device_type'
> & {
is_for__device_type: [Pick<DeviceType, 'slug'>];
};
type DeviceWithDeviceType = Device & {
is_of__device_type: [DeviceType];