diff --git a/.gitignore b/.gitignore
index 5ec0852e..a16b4ff3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -40,6 +40,7 @@ balenarc.yml
build/
build-bin/
build-zip/
+dist/
# Ignore fast-boot cache file
**/.fast-boot.json
diff --git a/.travis.yml b/.travis.yml
index cc18cee2..dc306149 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -3,15 +3,21 @@ os:
- linux
- osx
node_js:
- - "6"
-before_install:
-- npm -g install npm@4
-script: npm run ci
+ - "10"
+script:
+ - node --version
+ - npm --version
+ - npm run ci
notifications:
email: false
deploy:
- provider: script
- script: npm run release
+ script:
+ - node --version
+ - npm --version
+ - npm run build:standalone
+ - npm run build:installer
+ - npm run release
skip_cleanup: true
on:
tags: true
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 00000000..955726fa
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,65 @@
+# Contributing
+
+The balena CLI is an open source project and your contribution is welcome!
+
+After cloning this repository and running `npm install`, the CLI can be built with `npm run build`
+and executed with `./bin/run`. In order to ease development:
+
+* `npm run build:fast` skips some of the build steps for interactive testing, or
+* `./bin/balena-dev` uses `ts-node/register` and `coffeescript/register` to transpile on the fly.
+
+Before opening a PR, please be sure to test your changes with `npm test`.
+
+## Semantic versioning and commit messages
+
+The CLI version numbering adheres to [Semantic Versioning](http://semver.org/). The following
+header/row is required in the body of a commit message, and will cause the CI build to fail if absent:
+
+```
+Change-type: patch|minor|major
+```
+
+Version numbers and commit messages are automatically added to the `CHANGELOG.md` file by the CI
+build flow, after a pull request is merged. It should not be manually edited.
+
+## Editing documentation files (CHANGELOG, README, website...)
+
+The `doc/cli.markdown` file is automatically generated by running `npm run build:doc` (which also
+runs as part of `npm run build`). That file is then pulled by scripts in the
+[balena-io/docs](https://github.com/balena-io/docs/) GitHub repo for publishing at the [CLI
+Documentation page](https://www.balena.io/docs/reference/cli/).
+
+The content sources for the auto generation of `doc/cli.markdown` are:
+
+* Selected sections of the README file.
+* The CLI's command documentation in source code (both Capitano and oclif commands), for example:
+ * `lib/actions/build.coffee`
+ * `lib/actions-oclif/env/add.ts`
+
+The README file is manually edited, but subsections are automatically extracted for inclusion in
+`doc/cli.markdown` by the `getCapitanoDoc()` function in
+[`automation/capitanodoc/capitanodoc.ts`](https://github.com/balena-io/balena-cli/blob/master/automation/capitanodoc/capitanodoc.ts).
+
+The `INSTALL.md` and `TROUBLESHOOTING.md` files are also manually edited.
+
+## Windows
+
+Please note that `npm run build:installer` (which generates the `.exe` executable installer on
+Windows) requires [MSYS2](https://www.msys2.org/) to be installed. Other than that, the standard
+Command Prompt or PowerShell can be used.
+
+## TypeScript vs CoffeeScript, and Capitano vs oclif
+
+The CLI was originally written in [CoffeeScript](https://coffeescript.org), but we decided to
+migrate to [TypeScript](https://www.typescriptlang.org/) in order to take advantage of static
+typing and formal programming interfaces. The migration is taking place gradually, as part of
+maintenance work or the implementation of new features.
+
+Similarly, [Capitano](https://github.com/balena-io/capitano) was originally adopted as the CLI's
+framework, but we recently decided to take advantage of [oclif](https://oclif.io/)'s features such
+as native installers for Windows, macOS and Linux, and support for custom flag parsing (for
+example, we're still battling with Capitano's behavior of dropping leading zeros of arguments that
+look like integers such as some abbreviated UUIDs, and migrating to oclif is a solution). Again the
+migration is taking place gradually, with some CLI commands parsed by oclif and others by Capitano
+(a simple command line pre-parsing takes place in `app.ts` to decide whether to route full parsing
+to Capitano or oclif).
diff --git a/INSTALL.md b/INSTALL.md
index 8ca83a7f..93ff854d 100644
--- a/INSTALL.md
+++ b/INSTALL.md
@@ -1,30 +1,70 @@
# balena CLI Installation Instructions
-The easiest and recommended way of installing the CLI on all platforms (Windows, Linux, macOS) is
-to use the [Standalone Installation](#standalone-installation) described below. Some specific CLI
-commands have a few extra installation steps: see section [Additional Dependencies](#additional-dependencies).
+There are 3 options to choose from to install balena's CLI:
+
+* [Executable Installer](#executable-installer): the easiest method, using the traditional
+ graphical desktop application installers for Windows and macOS (coming soon for Linux users too).
+* [Standalone Zip Package](#standalone-zip-package): these are plain zip files with the balena CLI
+ executable in them. Recommended for scripted installation in CI (continuous integration)
+ environments.
+* [NPM Installation](#npm-installation): recommended for developers who may be interested in
+ integrating the balena CLI in their existing Node.js projects or workflow.
+
+Some specific CLI commands have a few extra installation steps: see section [Additional
+Dependencies](#additional-dependencies).
> **Windows users:** We now have a [YouTube video tutorial](https://www.youtube.com/watch?v=2LApclXFqsg)
for installing and getting started with the balena CLI on Windows!
-## Standalone Installation
+## Executable Installer
-1. Download the latest zip file for your OS from https://github.com/balena-io/balena-cli/releases.
- (Note that "[Darwin](https://en.wikipedia.org/wiki/Darwin_(operating_system))" is the
- appropriate zip file for macOS.)
-2. Extract the zip file contents to any folder you choose. The extracted contents will include a
- `balena-cli` folder.
-3. Add the `balena-cli` folder to the system's `PATH` environment variable. See instructions for:
- [Windows](https://www.computerhope.com/issues/ch000549.htm) |
- [Linux](https://stackoverflow.com/questions/14637979/how-to-permanently-set-path-on-linux-unix) |
- [macOS](https://www.architectryan.com/2012/10/02/add-to-the-path-on-mac-os-x-mountain-lion/#.Uydjga1dXDg)
+_Please note: the executable installers are in **beta** status (recently introduced)._
-Check that the installation was successful by opening or re-opening a command terminal window
-(so that the PATH environment variable changes take effect), and running these commands:
+1. Download the latest installer from the [releases page](https://github.com/balena-io/balena-cli/releases).
+ Look for a file name that ends with "installer-BETA", for example:
+ `balena-cli-v10.13.6-windows-x64-installer-BETA.exe`
+ `balena-cli-v10.13.6-macOS-x64-installer-BETA.pkg`
+2. Double click to run. Your system may raise a pop-up warning that the installer is from an
+ "unknown publisher" or "unidentified developer". Check the following instructions for how
+ to get through the warnings:
+ [Windows](https://github.com/balena-io/balena-cli/issues/1250) or
+ [macOS](https://github.com/balena-io/balena-cli/issues/1251).
+ (We are looking at how to get the installers digitally signed to avoid the warnings.)
+
+After the installation completes, close and re-open any open command terminal windows so that the
+changes made by the installer to the PATH environment variable can take effect. Check that the
+installation was successful by running these commands:
* `balena` - should print the balena CLI help
* `balena version` - should print the installed CLI version
+> Note: If you had previously installed the CLI using a standalone zip package, it may be a good
+> idea to check your system's `PATH` environment variable for duplicate entries, as the terminal
+> will use the entry that comes first. Check the [Standalone Zip Package](#standalone-zip-package)
+> instructions for how to modify the PATH variable.
+
+By default, the CLI is installed to the following folders:
+
+OS | Folders
+--- | ---
+Windows: | `C:\Program Files\balena-cli\`
+macOS: | `/usr/local/lib/balena-cli/` `/usr/local/bin/balena`
+
+## Standalone Zip Package
+
+1. Download the latest zip file from the [releases page](https://github.com/balena-io/balena-cli/releases).
+ Look for a file name that ends with the word "standalone", for example:
+ `balena-cli-v10.13.6-linux-x64-standalone.zip`
+ `balena-cli-v10.13.6-macOS-x64-standalone.zip`
+ `balena-cli-v10.13.6-windows-x64-standalone.zip`
+2. Extract the zip file contents to any folder you choose. The extracted contents will include a
+ `balena-cli` folder.
+3. Add the `balena-cli` folder to the system's `PATH` environment variable.
+ See instructions for:
+ [Linux](https://stackoverflow.com/questions/14637979/how-to-permanently-set-path-on-linux-unix) |
+ [macOS](https://www.architectryan.com/2012/10/02/add-to-the-path-on-mac-os-x-mountain-lion/#.Uydjga1dXDg) |
+ [Windows](https://www.computerhope.com/issues/ch000549.htm)
+
To update the CLI to a new version, download a new release zip file and replace the previous
installation folder. To uninstall, simply delete the folder and edit the PATH environment variable
as described above.
diff --git a/README.md b/README.md
index 60ac4ee8..0bf1e2c5 100644
--- a/README.md
+++ b/README.md
@@ -29,9 +29,10 @@ are supported. We are aware of users also having a good experience with alternat
including:
* Microsoft's [Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/about)
- (a.k.a. Microsoft's "bash for Windows 10").
-* [Git for Windows](https://git-for-windows.github.io/).
-* [MinGW](http://www.mingw.org): install the `msys-rsync` and `msys-openssh` packages too.
+ (a.k.a. Microsoft's "bash for Windows 10")
+* [Git for Windows](https://git-for-windows.github.io/)
+* [MSYS](http://www.mingw.org/wiki/MSYS) and [MSYS2](https://www.msys2.org/) (install the
+ `msys-rsync` and `msys-openssh` packages too)
On **macOS** and **Linux,** the standard terminal window is supported. _Optionally,_ `bash` command
auto completion may be enabled by copying the
@@ -52,14 +53,14 @@ $ balena login
HTTP(S) proxies can be configured through any of the following methods, in order of preference:
-* Set the \`BALENARC_PROXY\` environment variable in URL format (with protocol, host, port, and
+* Set the `BALENARC_PROXY` environment variable in URL format (with protocol, host, port, and
optionally basic auth).
* Alternatively, use the [balena config file](https://www.npmjs.com/package/balena-settings-client#documentation)
- (project-specific or user-level) and set the \`proxy\` setting. It can be:
+ (project-specific or user-level) and set the `proxy` setting. It can be:
* A string in URL format, or
* An object in the [global-tunnel-ng options format](https://www.npmjs.com/package/global-tunnel-ng#options) (which allows more control).
-* Alternatively, set the conventional \`https_proxy\` / \`HTTPS_PROXY\` / \`http_proxy\` / \`HTTP_PROXY\`
-environment variable (in the same standard URL format).
+* Alternatively, set the conventional `https_proxy` / `HTTPS_PROXY` / `http_proxy` / `HTTP_PROXY`
+ environment variable (in the same standard URL format).
To get a proxy to work with the `balena ssh` command, check the
[installation instructions](https://github.com/balena-io/balena-cli/blob/master/INSTALL.md).
@@ -78,30 +79,11 @@ If you come across any problems or would like to get in touch:
* For bug reports or feature requests,
[have a look at the GitHub issues or create a new one](https://github.com/balena-io/balena-cli/issues/).
-## Contributing
+## Contributing (including editing documentation files)
-The balena CLI is an open source project and your contribution is welcome!
-
-The CLI was originally written in [CoffeeScript](https://coffeescript.org), but we have decided to
-migrate to [TypeScript](https://www.typescriptlang.org/) in order to take advantage of static
-typing and formal programming interfaces. The migration is taking place gradually, as part of
-maintenance work or the implementation of new features.
-
-After cloning this repository and running `npm install` you can build the CLI using `npm run build`.
-You can then run the generated build using `./bin/balena`.
-In order to ease development:
-
-* you can build the CLI using the `npm run build:fast` variant which skips some of the build steps or
-* you can use `./bin/balena-dev` which live transpiles the sources of the CLI.
-
-In either case, before opening a PR be sure to also test your changes with `npm test`.
-
-## Note on editing this README document
-
-This file is edited/created by hand, but it is then automatically parsed to extract selected
-subsections for the [CLI's web documentation page](https://www.balena.io/docs/reference/cli/).
-The code that parses this file is in [`automation/capitanodoc/capitanodoc.ts`
-](https://github.com/balena-io/balena-cli/blob/master/automation/capitanodoc/capitanodoc.ts).
+Please have a look at the [CONTRIBUTING.md](./CONTRIBUTING.md) file for some guidance before
+submitting a pull request or updating documentation (because some files are automatically
+generated). Thank you for your help and interest!
## License
diff --git a/appveyor.yml b/appveyor.yml
index ab9ebe80..8eb60841 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -1,6 +1,8 @@
# appveyor file
# http://www.appveyor.com/docs/appveyor-yml
+image: Visual Studio 2017
+
init:
- git config --global core.autocrlf input
@@ -14,12 +16,12 @@ matrix:
# what combinations to test
environment:
matrix:
- - nodejs_version: 6
+ - nodejs_version: 10
install:
- ps: Install-Product node $env:nodejs_version x64
- - npm install -g npm@4
- set PATH=%APPDATA%\npm;%PATH%
+ - npm config set python 'C:\Python27\python.exe'
- npm install
build: off
@@ -27,8 +29,12 @@ build: off
test_script:
- node --version
- npm --version
- - cmd: npm test
+ - npm test
deploy_script:
+ - node --version
+ - npm --version
+ - npm run build:standalone
+ - npm run build:installer
- IF "%APPVEYOR_REPO_TAG%" == "true" (npm run release)
- IF NOT "%APPVEYOR_REPO_TAG%" == "true" (echo 'Not tagged, skipping deploy')
diff --git a/automation/build-bin.ts b/automation/build-bin.ts
old mode 100755
new mode 100644
index 506dd536..43348c69
--- a/automation/build-bin.ts
+++ b/automation/build-bin.ts
@@ -14,50 +14,106 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
+import { run as oclifRun } from '@oclif/dev-cli';
import * as Bluebird from 'bluebird';
import * as filehound from 'filehound';
import * as fs from 'fs-extra';
import * as path from 'path';
import { exec as execPkg } from 'pkg';
+import * as rimraf from 'rimraf';
-const ROOT = path.join(__dirname, '..');
+export const ROOT = path.join(__dirname, '..');
-console.log('Building package...\n');
+/**
+ * Use the 'pkg' module to create a single large executable file with
+ * the contents of 'node_modules' and the CLI's javascript code.
+ * Also copy a number of native modules (binary '.node' files) that are
+ * compiled during 'npm install' to the 'build-bin' folder, alongside
+ * the single large executable file created by pkg. (This is necessary
+ * because of a pkg limitation that does not allow binary executables
+ * to be directly executed from inside another binary executable.)
+ */
+export async function buildPkg() {
+ console.log('Building package...\n');
-execPkg(['--target', 'host', '--output', 'build-bin/balena', 'package.json'])
- .then(() => {
- const xpaths: Array<[string, string[]]> = [
- // [platform, [path, to, file]]
- ['*', ['opn', 'xdg-open']],
- ['darwin', ['denymount', 'bin', 'denymount']],
- ];
- return Bluebird.map(xpaths, ([platform, xpath]) => {
- if (platform === '*' || platform === process.platform) {
- // eg copy from node_modules/opn/xdg-open to build-bin/xdg-open
- return fs.copy(
- path.join(ROOT, 'node_modules', ...xpath),
- path.join(ROOT, 'build-bin', xpath.pop()!),
- );
- }
- }).return();
- })
- .then(() => {
- return filehound
- .create()
- .paths(path.join(ROOT, 'node_modules'))
- .ext(['node', 'dll'])
- .find();
- })
- .then(nativeExtensions => {
- console.log(`\nCopying to build-bin:\n${nativeExtensions.join('\n')}`);
-
- return nativeExtensions.map(extPath => {
+ await execPkg([
+ '--target',
+ 'host',
+ '--output',
+ 'build-bin/balena',
+ 'package.json',
+ ]);
+ const xpaths: Array<[string, string[]]> = [
+ // [platform, [path, to, file]]
+ ['*', ['opn', 'xdg-open']],
+ ['darwin', ['denymount', 'bin', 'denymount']],
+ ];
+ await Bluebird.map(xpaths, ([platform, xpath]) => {
+ if (platform === '*' || platform === process.platform) {
+ // eg copy from node_modules/opn/xdg-open to build-bin/xdg-open
return fs.copy(
- extPath,
- extPath.replace(
- path.join(ROOT, 'node_modules'),
- path.join(ROOT, 'build-bin'),
- ),
+ path.join(ROOT, 'node_modules', ...xpath),
+ path.join(ROOT, 'build-bin', xpath.pop()!),
);
- });
+ }
});
+ const nativeExtensionPaths: string[] = await filehound
+ .create()
+ .paths(path.join(ROOT, 'node_modules'))
+ .ext(['node', 'dll'])
+ .find();
+
+ console.log(`\nCopying to build-bin:\n${nativeExtensionPaths.join('\n')}`);
+
+ await Bluebird.map(nativeExtensionPaths, extPath =>
+ fs.copy(
+ extPath,
+ extPath.replace(
+ path.join(ROOT, 'node_modules'),
+ path.join(ROOT, 'build-bin'),
+ ),
+ ),
+ );
+}
+
+/**
+ * Run the `oclif-dev pack:win` or `pack:macos` command (depending on the value
+ * of process.platform) to generate the native installers (which end up under
+ * the 'dist' folder). There are some harcoded options such as selecting only
+ * 64-bit binaries under Windows.
+ */
+export async function buildOclifInstaller() {
+ console.log(`buildOclifInstaller cwd="${process.cwd()}" ROOT="${ROOT}"`);
+ let packOS = '';
+ let packOpts = ['-r', ROOT];
+ if (process.platform === 'darwin') {
+ packOS = 'macos';
+ } else if (process.platform === 'win32') {
+ packOS = 'win';
+ packOpts = packOpts.concat('-t', 'win32-x64');
+ }
+ if (packOS) {
+ const packCmd = `pack:${packOS}`;
+ const dirs = [path.join(ROOT, 'dist', packOS)];
+ if (packOS === 'win') {
+ dirs.push(path.join(ROOT, 'tmp', 'win*'));
+ }
+ for (const dir of dirs) {
+ console.log(`rimraf(${dir})`);
+ await Bluebird.fromCallback(cb => rimraf(dir, cb));
+ }
+ console.log('=======================================================');
+ console.log(`oclif-dev "${packCmd}" [${packOpts}]`);
+ console.log('=======================================================');
+ oclifRun([packCmd].concat(...packOpts));
+ }
+}
+
+/**
+ * Convert e.g. 'C:\myfolder' -> '/C/myfolder' so that the path can be given
+ * as argument to "unix tools" like 'tar' under MSYS or MSYS2 on Windows.
+ */
+export function fixPathForMsys(p: string): string {
+ return p.replace(/\\/g, '/').replace(/^([a-zA-Z]):/, '/$1');
+}
diff --git a/automation/capitanodoc/capitanodoc.ts b/automation/capitanodoc/capitanodoc.ts
index 423d5dbb..c98b66d7 100644
--- a/automation/capitanodoc/capitanodoc.ts
+++ b/automation/capitanodoc/capitanodoc.ts
@@ -48,7 +48,10 @@ const capitanoDoc = {
},
{
title: 'Environment Variables',
- files: ['build/actions/environment-variables.js'],
+ files: [
+ 'build/actions/environment-variables.js',
+ 'build/actions-oclif/env/add.js',
+ ],
},
{
title: 'Tags',
@@ -70,10 +73,6 @@ const capitanoDoc = {
title: 'Logs',
files: ['build/actions/logs.js'],
},
- {
- title: 'Sync',
- files: ['build/actions/sync.js'],
- },
{
title: 'SSH',
files: ['build/actions/ssh.js', 'build/actions/tunnel.js'],
diff --git a/automation/capitanodoc/doc-types.d.ts b/automation/capitanodoc/doc-types.d.ts
index 536cf6c8..195fa47b 100644
--- a/automation/capitanodoc/doc-types.d.ts
+++ b/automation/capitanodoc/doc-types.d.ts
@@ -1,4 +1,23 @@
-import { CommandDefinition } from 'capitano';
+/**
+ * @license
+ * Copyright 2019 Balena Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Command as OclifCommandClass } from '@oclif/command';
+import { CommandDefinition as CapitanoCommand } from 'capitano';
+
+type OclifCommand = typeof OclifCommandClass;
export interface Document {
title: string;
@@ -8,7 +27,7 @@ export interface Document {
export interface Category {
title: string;
- commands: CommandDefinition[];
+ commands: Array;
}
-export { CommandDefinition as Command };
+export { CapitanoCommand, OclifCommand };
diff --git a/automation/capitanodoc/index.ts b/automation/capitanodoc/index.ts
index d0d08864..150f27d2 100644
--- a/automation/capitanodoc/index.ts
+++ b/automation/capitanodoc/index.ts
@@ -18,7 +18,7 @@ import * as _ from 'lodash';
import * as path from 'path';
import { getCapitanoDoc } from './capitanodoc';
-import { Category, Document } from './doc-types';
+import { CapitanoCommand, Category, Document, OclifCommand } from './doc-types';
import * as markdown from './markdown';
/**
@@ -39,25 +39,40 @@ export async function renderMarkdown(): Promise {
commands: [],
};
- for (const file of commandCategory.files) {
- const actions: any = require(path.join(process.cwd(), file));
-
- if (actions.signature) {
- category.commands.push(_.omit(actions, 'action'));
- } else {
- for (const actionName of Object.keys(actions)) {
- const actionCommand = actions[actionName];
- category.commands.push(_.omit(actionCommand, 'action'));
- }
- }
+ for (const jsFilename of commandCategory.files) {
+ category.commands.push(
+ ...(jsFilename.includes('actions-oclif')
+ ? importOclifCommands(jsFilename)
+ : importCapitanoCommands(jsFilename)),
+ );
}
-
result.categories.push(category);
}
return markdown.render(result);
}
+function importCapitanoCommands(jsFilename: string): CapitanoCommand[] {
+ const actions = require(path.join(process.cwd(), jsFilename));
+ const commands: CapitanoCommand[] = [];
+
+ if (actions.signature) {
+ commands.push(_.omit(actions, 'action'));
+ } else {
+ for (const actionName of Object.keys(actions)) {
+ const actionCommand = actions[actionName];
+ commands.push(_.omit(actionCommand, 'action'));
+ }
+ }
+ return commands;
+}
+
+function importOclifCommands(jsFilename: string): OclifCommand[] {
+ const command: OclifCommand = require(path.join(process.cwd(), jsFilename))
+ .default as OclifCommand;
+ return [command];
+}
+
/**
* Print the CLI docs markdown to stdout.
* See package.json for how the output is redirected to a file.
diff --git a/automation/capitanodoc/markdown.ts b/automation/capitanodoc/markdown.ts
index 20b4b6c9..974d7b09 100644
--- a/automation/capitanodoc/markdown.ts
+++ b/automation/capitanodoc/markdown.ts
@@ -14,81 +14,139 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import { flagUsages } from '@oclif/parser';
import * as ent from 'ent';
import * as _ from 'lodash';
-import { Category, Command, Document } from './doc-types';
+import { getManualSortCompareFunction } from '../../lib/utils/helpers';
+import { CapitanoCommand, Category, Document, OclifCommand } from './doc-types';
import * as utils from './utils';
-export function renderCommand(command: Command) {
- let result = `## ${ent.encode(command.signature)}\n\n${command.help}\n`;
+function renderCapitanoCommand(command: CapitanoCommand): string[] {
+ const result = [`## ${ent.encode(command.signature)}`, command.help];
if (!_.isEmpty(command.options)) {
- result += '\n### Options';
+ result.push('### Options');
for (const option of command.options!) {
- result += `\n\n#### ${utils.parseSignature(option)}\n\n${
- option.description
- }`;
+ if (option == null) {
+ throw new Error(`Undefined option in markdown generation!`);
+ }
+ result.push(
+ `#### ${utils.parseCapitanoOption(option)}`,
+ option.description,
+ );
}
-
- result += '\n';
}
-
return result;
}
-export function renderCategory(category: Category) {
- let result = `# ${category.title}\n`;
+function renderOclifCommand(command: OclifCommand): string[] {
+ const result = [`## ${ent.encode(command.usage)}`];
+ const description = (command.description || '')
+ .split('\n')
+ .slice(1) // remove the first line, which oclif uses as help header
+ .join('\n')
+ .trim();
+ result.push(description);
+ if (!_.isEmpty(command.examples)) {
+ result.push('Examples:', command.examples!.map(v => `\t${v}`).join('\n'));
+ }
+
+ if (!_.isEmpty(command.args)) {
+ result.push('### Arguments');
+ for (const arg of command.args!) {
+ result.push(`#### ${arg.name.toUpperCase()}`, arg.description || '');
+ }
+ }
+
+ if (!_.isEmpty(command.flags)) {
+ result.push('### Options');
+ for (const [name, flag] of Object.entries(command.flags!)) {
+ if (name === 'help') {
+ continue;
+ }
+ flag.name = name;
+ const flagUsage = flagUsages([flag])
+ .map(([usage, _description]) => usage)
+ .join()
+ .trim();
+ result.push(`#### ${flagUsage}`);
+ result.push(flag.description || '');
+ }
+ }
+ return result;
+}
+
+function renderCategory(category: Category): string[] {
+ const result = [`# ${category.title}`];
for (const command of category.commands) {
- result += `\n${renderCommand(command)}`;
+ result.push(
+ ...(typeof command === 'object'
+ ? renderCapitanoCommand(command)
+ : renderOclifCommand(command)),
+ );
}
-
return result;
}
-function getAnchor(command: Command) {
- return (
- '#' +
- command.signature
- .replace(/\s/g, '-')
- .replace(//g, '-')
- .replace(/\[/g, '-')
- .replace(/\]/g, '-')
- .replace(/-+/g, '-')
- .replace(/-$/, '')
- .replace(/\.\.\./g, '')
- .replace(/\|/g, '')
- .toLowerCase()
- );
+function getAnchor(cmdSignature: string): string {
+ return `#${_.trim(cmdSignature.replace(/\W+/g, '-'), '-').toLowerCase()}`;
}
-export function renderToc(categories: Category[]) {
- let result = `# CLI Command Reference\n`;
+function renderToc(categories: Category[]): string[] {
+ const result = [`# CLI Command Reference`];
for (const category of categories) {
- result += `\n- ${category.title}\n\n`;
+ result.push(`- ${category.title}`);
+ result.push(
+ category.commands
+ .map(command => {
+ const signature =
+ typeof command === 'object'
+ ? command.signature // Capitano
+ : utils.capitanoizeOclifUsage(command.usage); // oclif
+ return `\t- [${ent.encode(signature)}](${getAnchor(signature)})`;
+ })
+ .join('\n'),
+ );
+ }
+ return result;
+}
- for (const command of category.commands) {
- result += `\t- [${ent.encode(command.signature)}](${getAnchor(
- command,
- )})\n`;
+const manualCategorySorting: { [category: string]: string[] } = {
+ 'Environment Variables': ['envs', 'env rm', 'env add', 'env rename'],
+};
+
+function sortCommands(doc: Document): void {
+ for (const category of doc.categories) {
+ if (category.title in manualCategorySorting) {
+ category.commands = category.commands.sort(
+ getManualSortCompareFunction(
+ manualCategorySorting[category.title],
+ (cmd: CapitanoCommand | OclifCommand, x: string) =>
+ typeof cmd === 'object' // Capitano vs oclif command
+ ? cmd.signature.replace(/\W+/g, ' ').includes(x)
+ : (cmd.usage || '')
+ .toString()
+ .replace(/\W+/g, ' ')
+ .includes(x),
+ ),
+ );
}
}
-
- return result;
}
export function render(doc: Document) {
- let result = `# ${doc.title}\n\n${doc.introduction}\n\n${renderToc(
- doc.categories,
- )}`;
-
+ sortCommands(doc);
+ const result = [
+ `# ${doc.title}`,
+ doc.introduction,
+ ...renderToc(doc.categories),
+ ];
for (const category of doc.categories) {
- result += `\n${renderCategory(category)}`;
+ result.push(...renderCategory(category));
}
-
- return result;
+ return result.join('\n\n');
}
diff --git a/automation/capitanodoc/utils.ts b/automation/capitanodoc/utils.ts
index 060cf6f8..2eca5c7b 100644
--- a/automation/capitanodoc/utils.ts
+++ b/automation/capitanodoc/utils.ts
@@ -14,6 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
import { OptionDefinition } from 'capitano';
import * as ent from 'ent';
import * as fs from 'fs';
@@ -32,7 +33,7 @@ export function getOptionSignature(signature: string) {
return `${getOptionPrefix(signature)}${signature}`;
}
-export function parseSignature(option: OptionDefinition) {
+export function parseCapitanoOption(option: OptionDefinition): string {
let result = getOptionSignature(option.signature);
if (_.isArray(option.alias)) {
@@ -50,6 +51,16 @@ export function parseSignature(option: OptionDefinition) {
return ent.encode(result);
}
+/** Convert e.g. 'env add NAME [VALUE]' to 'env add [value]' */
+export function capitanoizeOclifUsage(
+ oclifUsage: string | string[] | undefined,
+): string {
+ return (oclifUsage || '')
+ .toString()
+ .replace(/(?<=\s)[A-Z]+(?=(\s|$))/g, match => `<${match}>`)
+ .toLowerCase();
+}
+
export class MarkdownFileParser {
constructor(public mdFilePath: string) {}
diff --git a/automation/custom-types.d.ts b/automation/custom-types.d.ts
deleted file mode 100644
index 21ec5fad..00000000
--- a/automation/custom-types.d.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-declare module 'pkg' {
- export function exec(args: string[]): Promise;
-}
-
-declare module 'filehound' {
- export function create(): FileHound;
-
- export interface FileHound {
- paths(paths: string[]): FileHound;
- paths(...paths: string[]): FileHound;
- ext(extensions: string[]): FileHound;
- ext(...extensions: string[]): FileHound;
- find(): Promise;
- }
-}
-
-declare module 'publish-release' {
- interface PublishOptions {
- token: string;
- owner: string;
- repo: string;
- tag: string;
- name: string;
- reuseRelease?: boolean;
- assets: string[];
- }
-
- interface Release {
- html_url: string;
- }
-
- let publishRelease: (
- args: PublishOptions,
- callback: (e: Error, release: Release) => void,
- ) => void;
-
- export = publishRelease;
-}
diff --git a/automation/deploy-bin.ts b/automation/deploy-bin.ts
index 8201f8ae..15852ed7 100644
--- a/automation/deploy-bin.ts
+++ b/automation/deploy-bin.ts
@@ -15,70 +15,133 @@
* limitations under the License.
*/
import * as archiver from 'archiver';
-import * as Promise from 'bluebird';
+import * as Bluebird from 'bluebird';
import * as fs from 'fs-extra';
-import * as mkdirp from 'mkdirp';
-import * as os from 'os';
+import * as _ from 'lodash';
import * as path from 'path';
import * as publishRelease from 'publish-release';
-import * as packageJSON from '../package.json';
-
-const publishReleaseAsync = Promise.promisify(publishRelease);
-const mkdirpAsync = Promise.promisify(mkdirp);
-
const { GITHUB_TOKEN } = process.env;
const ROOT = path.join(__dirname, '..');
-
+// Note: the following 'tslint disable' line was only required to
+// satisfy ts-node under Appveyor's MSYS2 on Windows -- oddly specific.
+// Maybe something to do with '/' vs '\' in paths in some tslint file.
+// tslint:disable-next-line:no-var-requires
+const packageJSON = require(path.join(ROOT, 'package.json'));
const version = 'v' + packageJSON.version;
-const outputFile = path.join(
- ROOT,
- 'build-zip',
- `balena-cli-${version}-${os.platform()}-${os.arch()}.zip`,
-);
+const arch = process.arch;
-mkdirpAsync(path.dirname(outputFile))
- .then(
- () =>
- new Promise((resolve, reject) => {
- console.log('Zipping build...');
+function dPath(...paths: string[]) {
+ return path.join(ROOT, 'dist', ...paths);
+}
- const archive = archiver('zip', {
- zlib: { level: 7 },
- });
- archive.directory(path.join(ROOT, 'build-bin'), 'balena-cli');
+interface PathByPlatform {
+ [platform: string]: string;
+}
- const outputStream = fs.createWriteStream(outputFile);
+const standaloneZips: PathByPlatform = {
+ linux: dPath(`balena-cli-${version}-linux-${arch}-standalone.zip`),
+ darwin: dPath(`balena-cli-${version}-macOS-${arch}-standalone.zip`),
+ win32: dPath(`balena-cli-${version}-windows-${arch}-standalone.zip`),
+};
- outputStream.on('close', resolve);
- outputStream.on('error', reject);
+const oclifInstallers: PathByPlatform = {
+ darwin: dPath('macos', `balena-${version}.pkg`),
+ win32: dPath('win', `balena-${version}-${arch}.exe`),
+};
- archive.on('error', reject);
- archive.on('warning', console.warn);
+const renamedOclifInstallers: PathByPlatform = {
+ darwin: dPath(`balena-cli-${version}-macOS-${arch}-installer-BETA.pkg`),
+ win32: dPath(`balena-cli-${version}-windows-${arch}-installer-BETA.exe`),
+};
- archive.pipe(outputStream);
- archive.finalize();
- }),
- )
- .then(() => {
- console.log('Build zipped');
- console.log('Publishing build...');
+const finalReleaseAssets: { [platform: string]: string[] } = {
+ win32: [standaloneZips['win32'], renamedOclifInstallers['win32']],
+ darwin: [standaloneZips['darwin'], renamedOclifInstallers['darwin']],
+ linux: [standaloneZips['linux']],
+};
- return publishReleaseAsync({
+/**
+ * Create the zip file for the standalone 'pkg' bundle previously created
+ * by the buildPkg() function in 'build-bin.ts'.
+ */
+export async function zipStandaloneInstaller() {
+ const outputFile = standaloneZips[process.platform];
+ if (!outputFile) {
+ throw new Error(
+ `Standalone installer unavailable for platform "${process.platform}"`,
+ );
+ }
+ await fs.mkdirp(path.dirname(outputFile));
+ await new Bluebird((resolve, reject) => {
+ console.log(`Zipping build to "${outputFile}"...`);
+
+ const archive = archiver('zip', {
+ zlib: { level: 7 },
+ });
+ archive.directory(path.join(ROOT, 'build-bin'), 'balena-cli');
+
+ const outputStream = fs.createWriteStream(outputFile);
+
+ outputStream.on('close', resolve);
+ outputStream.on('error', reject);
+
+ archive.on('error', reject);
+ archive.on('warning', console.warn);
+
+ archive.pipe(outputStream);
+ archive.finalize();
+ });
+ console.log('Build zipped');
+}
+
+/**
+ * Create or update a release in GitHub's releases page, uploading the
+ * installer files (standalone zip + native oclif installers).
+ */
+export async function createGitHubRelease() {
+ console.log(`Publishing release ${version} to GitHub`);
+ const ghRelease = await Bluebird.fromCallback(
+ publishRelease.bind(null, {
token: GITHUB_TOKEN || '',
owner: 'balena-io',
repo: 'balena-cli',
tag: version,
name: `balena-CLI ${version}`,
reuseRelease: true,
- assets: [outputFile],
- });
- })
- .then(release => {
- console.log(`Release ${version} successful: ${release.html_url}`);
- })
- .catch(err => {
+ assets: finalReleaseAssets[process.platform],
+ }),
+ );
+ console.log(`Release ${version} successful: ${ghRelease.html_url}`);
+}
+
+/**
+ * Top-level function to create a CLI release in GitHub's releases page:
+ * call zipStandaloneInstaller(), rename the files as we'd like them to
+ * display on the releases page, and call createGitHubRelease() to upload
+ * the files.
+ */
+export async function release() {
+ console.log(`Creating release assets for CLI ${version}`);
+ try {
+ await zipStandaloneInstaller();
+ } catch (error) {
+ console.log(`Error creating standalone installer zip file: ${error}`);
+ process.exit(1);
+ }
+ if (process.platform === 'win32' || process.platform === 'darwin') {
+ if (await fs.pathExists(oclifInstallers[process.platform])) {
+ await fs.rename(
+ oclifInstallers[process.platform],
+ renamedOclifInstallers[process.platform],
+ );
+ }
+ }
+ try {
+ await createGitHubRelease();
+ } catch (err) {
console.error('Release failed');
console.error(err);
process.exit(1);
- });
+ }
+}
diff --git a/automation/run.ts b/automation/run.ts
new file mode 100644
index 00000000..08beb0e2
--- /dev/null
+++ b/automation/run.ts
@@ -0,0 +1,112 @@
+/**
+ * @license
+ * Copyright 2019 Balena Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { spawn } from 'child_process';
+import * as _ from 'lodash';
+import * as shellEscape from 'shell-escape';
+
+import {
+ buildOclifInstaller,
+ buildPkg,
+ fixPathForMsys,
+ ROOT,
+} from './build-bin';
+import { release } from './deploy-bin';
+
+/**
+ * Run the MSYS2 bash.exe shell in a child process (child_process.spawn()).
+ * The given argv arguments are escaped using the 'shell-escape' package,
+ * so that backslashes in Windows paths, and other bash-special characters,
+ * are preserved. If argv is not provided, defaults to process.argv, to the
+ * effect that this current (parent) process is re-executed under MSYS2 bash.
+ * This is useful to change the default shell from cmd.exe to MSYS2 bash on
+ * Windows.
+ * @param argv Arguments to be shell-escaped and given to MSYS2 bash.exe.
+ */
+export async function runUnderMsys(argv?: string[]) {
+ const newArgv = argv || process.argv;
+ await new Promise((resolve, reject) => {
+ const cmd = 'C:\\msys64\\usr\\bin\\bash.exe';
+ const args = ['-lc', shellEscape(newArgv)];
+ const child = spawn(cmd, args, { stdio: 'inherit' });
+ child.on('close', code => {
+ if (code) {
+ console.log(`runUnderMsys: child process exited with code ${code}`);
+ reject(code);
+ } else {
+ resolve();
+ }
+ });
+ });
+}
+
+/**
+ * Trivial command-line parser. Check whether the command-line argument is one
+ * of the following strings, then call the appropriate functions:
+ * 'build:installer' (to build a native oclif installer)
+ * 'build:standalone' (to build a standalone pkg package)
+ * 'release' (to create/update a GitHub release)
+ *
+ * In the case of 'build:installer', also call runUnderMsys() to switch the
+ * shell from cmd.exe to MSYS2 bash.exe.
+ *
+ * @param args Arguments to parse (default is process.argv.slice(2))
+ */
+export async function run(args?: string[]) {
+ args = args || process.argv.slice(2);
+ console.log(`automation/run.ts process.argv=[${process.argv}]\n`);
+ console.log(`automation/run.ts args=[${args}]`);
+ if (_.isEmpty(args)) {
+ console.error('Error: missing args');
+ process.exit(1);
+ }
+ const commands: { [cmd: string]: () => void } = {
+ 'build:installer': buildOclifInstaller,
+ 'build:standalone': buildPkg,
+ release,
+ };
+ for (const arg of args) {
+ if (!commands.hasOwnProperty(arg)) {
+ throw new Error(`Error: unknown build target: ${arg}`);
+ }
+ }
+
+ // If runUnderMsys() is called to re-execute this script under MSYS2,
+ // the current working dir becomes the MSYS2 homedir, so we change back.
+ process.chdir(ROOT);
+
+ for (const arg of args) {
+ if (arg === 'build:installer' && process.platform === 'win32') {
+ // ensure running under MSYS2
+ if (!process.env.MSYSTEM) {
+ process.env.MSYS2_PATH_TYPE = 'inherit';
+ await runUnderMsys([
+ fixPathForMsys(process.argv[0]),
+ fixPathForMsys(process.argv[1]),
+ arg,
+ ]);
+ continue;
+ }
+ if (process.env.MSYS2_PATH_TYPE !== 'inherit') {
+ throw new Error('the MSYS2_PATH_TYPE env var must be set to "inherit"');
+ }
+ }
+ await commands[arg]();
+ }
+}
+
+run();
diff --git a/automation/tsconfig.json b/automation/tsconfig.json
index 938fd3d7..83d916dc 100644
--- a/automation/tsconfig.json
+++ b/automation/tsconfig.json
@@ -1,16 +1,19 @@
{
"compilerOptions": {
"module": "commonjs",
- "target": "es2015",
+ "target": "es2017",
"strict": true,
+ "strictPropertyInitialization": false,
"noUnusedLocals": true,
"noUnusedParameters": true,
"preserveConstEnums": true,
"removeComments": true,
- "sourceMap": true
- },
- "include": [
- "./**/*.ts",
- "../typings/*.d.ts"
- ]
+ "resolveJsonModule": true,
+ "sourceMap": true,
+ "skipLibCheck": true,
+ "typeRoots" : [
+ "../node_modules/@types",
+ "../typings"
+ ]
+ }
}
diff --git a/balena-completion.bash b/balena-completion.bash
index 498bc65b..a1500b6c 100644
--- a/balena-completion.bash
+++ b/balena-completion.bash
@@ -7,7 +7,7 @@ _balena_complete()
# Valid top-level completions
commands="app apps build config deploy device devices env envs help key \
keys local login logout logs note os preload quickstart settings \
- signup ssh sync util version whoami"
+ scan ssh util version whoami"
# Sub-completions
app_cmds="create restart rm"
config_cmds="generate inject read reconfigure write"
@@ -16,7 +16,7 @@ _balena_complete()
device_public_url_cmds="disable enable status"
env_cmds="add rename rm"
key_cmds="add rm"
- local_cmds="configure flash logs push scan ssh stop"
+ local_cmds="configure flash"
os_cmds="build-config configure download initialize versions"
util_cmds="available-drives"
diff --git a/bin/balena b/bin/balena
index cc67e77c..69f81d46 100755
--- a/bin/balena
+++ b/bin/balena
@@ -8,4 +8,5 @@ process.env.UV_THREADPOOL_SIZE = '64';
require('fast-boot2').start({
cacheFile: __dirname + '/.fast-boot.json'
})
-require('../build/app');
+// Run the CLI
+require('../build/app').run();
diff --git a/bin/balena-dev b/bin/balena-dev
index c00fc85d..2b0dfdb4 100755
--- a/bin/balena-dev
+++ b/bin/balena-dev
@@ -12,8 +12,12 @@ process.env.UV_THREADPOOL_SIZE = '64';
// Use fast-boot to cache require lookups, speeding up startup
require('fast-boot2').start({
cacheFile: '.fast-boot.json'
-})
-process.env['TS_NODE_PROJECT'] = require('path').dirname(__dirname);
+});
require('coffeescript/register');
-require('ts-node/register');
-require('../lib/app');
+// Note: before ts-node v6.0.0, 'transpile-only' (no type checking) was the
+// default option. We upgraded ts-node and found that adding 'transpile-only'
+// was necessary to avoid a mysterious 'null' error message. On the plus side,
+// it is supposed to run faster. We still benefit from type checking when
+// running 'npm run build'.
+require('ts-node/register/transpile-only');
+require('../lib/app').run();
diff --git a/doc/cli.markdown b/doc/cli.markdown
index a2b4196e..6503fc02 100644
--- a/doc/cli.markdown
+++ b/doc/cli.markdown
@@ -21,9 +21,10 @@ are supported. We are aware of users also having a good experience with alternat
including:
* Microsoft's [Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/about)
- (a.k.a. Microsoft's "bash for Windows 10").
-* [Git for Windows](https://git-for-windows.github.io/).
-* [MinGW](http://www.mingw.org): install the `msys-rsync` and `msys-openssh` packages too.
+ (a.k.a. Microsoft's "bash for Windows 10")
+* [Git for Windows](https://git-for-windows.github.io/)
+* [MSYS](http://www.mingw.org/wiki/MSYS) and [MSYS2](https://www.msys2.org/) (install the
+ `msys-rsync` and `msys-openssh` packages too)
On **macOS** and **Linux,** the standard terminal window is supported. _Optionally,_ `bash` command
auto completion may be enabled by copying the
@@ -44,14 +45,14 @@ $ balena login
HTTP(S) proxies can be configured through any of the following methods, in order of preference:
-* Set the \`BALENARC_PROXY\` environment variable in URL format (with protocol, host, port, and
+* Set the `BALENARC_PROXY` environment variable in URL format (with protocol, host, port, and
optionally basic auth).
* Alternatively, use the [balena config file](https://www.npmjs.com/package/balena-settings-client#documentation)
- (project-specific or user-level) and set the \`proxy\` setting. It can be:
+ (project-specific or user-level) and set the `proxy` setting. It can be:
* A string in URL format, or
* An object in the [global-tunnel-ng options format](https://www.npmjs.com/package/global-tunnel-ng#options) (which allows more control).
-* Alternatively, set the conventional \`https_proxy\` / \`HTTPS_PROXY\` / \`http_proxy\` / \`HTTP_PROXY\`
-environment variable (in the same standard URL format).
+* Alternatively, set the conventional `https_proxy` / `HTTPS_PROXY` / `http_proxy` / `HTTP_PROXY`
+ environment variable (in the same standard URL format).
To get a proxy to work with the `balena ssh` command, check the
[installation instructions](https://github.com/balena-io/balena-cli/blob/master/INSTALL.md).
@@ -84,7 +85,6 @@ If you come across any problems or would like to get in touch:
- [login](#login)
- [logout](#logout)
- - [signup](#signup)
- [whoami](#whoami)
- Device
@@ -109,7 +109,7 @@ If you come across any problems or would like to get in touch:
- [envs](#envs)
- [env rm <id>](#env-rm-id)
- - [env add <key> [value]](#env-add-key-value)
+ - [env add <name> [value]](#env-add-name-value)
- [env rename <id> <value>](#env-rename-id-value)
- Tags
@@ -137,13 +137,9 @@ If you come across any problems or would like to get in touch:
- [logs <uuidOrDevice>](#logs-uuidordevice)
-- Sync
-
- - [sync [uuid]](#sync-uuid)
-
- SSH
- - [ssh [uuid]](#ssh-uuid)
+ - [ssh <applicationOrDevice> [serviceName]](#ssh-applicationordevice-servicename)
- [tunnel <uuid>](#tunnel-uuid)
- Notes
@@ -182,11 +178,6 @@ If you come across any problems or would like to get in touch:
- [local configure <target>](#local-configure-target)
- [local flash <image>](#local-flash-image)
- - [local logs [deviceIp]](#local-logs-deviceip)
- - [local scan](#local-scan)
- - [local ssh [deviceIp]](#local-ssh-deviceip)
- - [local push [deviceIp]](#local-push-deviceip)
- - [local stop [deviceIp]](#local-stop-deviceip)
- Deploy
@@ -338,21 +329,6 @@ Examples:
$ balena logout
-## signup
-
-Use this command to signup for a balena account.
-
-If signup is successful, you'll be logged in to your new user automatically.
-
-Examples:
-
- $ balena signup
- Email: johndoe@acme.com
- Password: ***********
-
- $ balena whoami
- johndoe
-
## whoami
Use this command to find out the current logged in username and email address.
@@ -633,43 +609,52 @@ confirm non interactively
device
-## env add <key> [value]
+## env add NAME [VALUE]
-Use this command to add an enviroment or config variable to an application
-or device.
+Add an environment or config variable to an application or device, as selected
+by the respective command-line options.
-If value is omitted, the tool will attempt to use the variable's value
-as defined in your host machine.
+If VALUE is omitted, the CLI will attempt to use the value of the environment
+variable of same name in the CLI process' environment. In this case, a warning
+message will be printed. Use `--quiet` to suppress it.
-Use the `--device` option if you want to assign the environment variable
-to a specific device.
-
-If the value is grabbed from the environment, a warning message will be printed.
-Use `--quiet` to remove it.
-
-Service-specific variables are not currently supported. The following
-examples set variables that apply to all services in an app or device.
+Service-specific variables are not currently supported. The given command line
+examples variables that apply to all services in an app or device.
Examples:
- $ balena env add EDITOR vim --application MyApp
$ balena env add TERM --application MyApp
+ $ balena env add EDITOR vim --application MyApp
$ balena env add EDITOR vim --device 7cf02a6
+### Arguments
+
+#### NAME
+
+environment or config variable name
+
+#### VALUE
+
+variable value; if omitted, use value from CLI's environment
+
### Options
-#### --application, -a, --app <application>
+#### -a, --application APPLICATION
application name
-#### --device, -d <device>
+#### -d, --device DEVICE
-device uuid
+device UUID
+
+#### -q, --quiet
+
+suppress warning messages
## env rename <id> <value>
Use this command to change the value of an application or device
-enviroment variable.
+environment variable.
The --device option selects a device instead of an application.
@@ -865,6 +850,7 @@ Examples:
$ balena logs 192.168.0.31
$ balena logs 192.168.0.31 --service my-service
+ $ balena logs 192.168.0.31 --service my-service-1 --service my-service-2
$ balena logs 23c73a1.local --system
$ balena logs 23c73a1.local --system --service my-service
@@ -877,140 +863,56 @@ continuously stream output
#### --service, -s <service>
-Only show logs for a single service. This can be used in combination with --system
+Reject logs not originating from this service.
+This can be used in combination with --system or other --service flags.
#### --system, -S
Only show system logs. This can be used in combination with --service.
-# Sync
-
-## sync [uuid]
-
-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-Deprecation notice: please note that `balena sync` is deprecated and will
-be removed in a future release of the CLI. We are working on an exciting
-replacement that will be released soon!
-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-Warning: 'balena sync' requires an openssh-compatible client and 'rsync' to
-be correctly installed in your shell environment. For more information (including
-Windows support) please check the README here: https://github.com/balena-io/balena-cli
-
-Use this command to sync your local changes to a certain device on the fly.
-
-After every 'balena sync' the updated settings will be saved in
-'