Merge pull request #1285 from balena-io/release-v11

Release balena-cli v11.0.0
This commit is contained in:
Paulo Castro 2019-06-04 19:16:16 +01:00 committed by GitHub
commit abc2cfd14c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
87 changed files with 2823 additions and 1301 deletions

1
.gitignore vendored
View File

@ -40,6 +40,7 @@ balenarc.yml
build/
build-bin/
build-zip/
dist/
# Ignore fast-boot cache file
**/.fast-boot.json

View File

@ -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

65
CONTRIBUTING.md Normal file
View File

@ -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).

View File

@ -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/` <br> `/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.

View File

@ -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

View File

@ -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')

128
automation/build-bin.ts Executable file → Normal file
View File

@ -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');
}

View File

@ -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'],

View File

@ -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<CapitanoCommand | OclifCommand>;
}
export { CommandDefinition as Command };
export { CapitanoCommand, OclifCommand };

View File

@ -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<string> {
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.

View File

@ -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(/-+/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<CapitanoCommand | OclifCommand, string>(
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');
}

View File

@ -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 <name> [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) {}

View File

@ -1,38 +0,0 @@
declare module 'pkg' {
export function exec(args: string[]): Promise<void>;
}
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<string[]>;
}
}
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;
}

View File

@ -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<string | null, string>(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);
});
}
}

112
automation/run.ts Normal file
View File

@ -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();

View File

@ -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"
]
}
}

View File

@ -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"

View File

@ -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();

View File

@ -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();

View File

@ -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 &#60;id&#62;](#env-rm-id)
- [env add &#60;key&#62; [value]](#env-add-key-value)
- [env add &#60;name&#62; [value]](#env-add-name-value)
- [env rename &#60;id&#62; &#60;value&#62;](#env-rename-id-value)
- Tags
@ -137,13 +137,9 @@ If you come across any problems or would like to get in touch:
- [logs &#60;uuidOrDevice&#62;](#logs-uuidordevice)
- Sync
- [sync [uuid]](#sync-uuid)
- SSH
- [ssh [uuid]](#ssh-uuid)
- [ssh &#60;applicationOrDevice&#62; [serviceName]](#ssh-applicationordevice-servicename)
- [tunnel &#60;uuid&#62;](#tunnel-uuid)
- Notes
@ -182,11 +178,6 @@ If you come across any problems or would like to get in touch:
- [local configure &#60;target&#62;](#local-configure-target)
- [local flash &#60;image&#62;](#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 &#60;key&#62; [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 &#60;application&#62;
#### -a, --application APPLICATION
application name
#### --device, -d &#60;device&#62;
#### -d, --device DEVICE
device uuid
device UUID
#### -q, --quiet
suppress warning messages
## env rename &#60;id&#62; &#60;value&#62;
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 &#60;service&#62;
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
'<source>/.balena-sync.yml' and will be used in later invocations. You can
also change any option by editing '.balena-sync.yml' directly.
Here is an example '.balena-sync.yml' :
$ cat $PWD/.balena-sync.yml
uuid: 7cf02a6
destination: '/usr/src/app'
before: 'echo Hello'
after: 'echo Done'
ignore:
- .git
- node_modules/
Command line options have precedence over the ones saved in '.balena-sync.yml'.
If '.gitignore' is found in the source directory then all explicitly listed files will be
excluded from the syncing process. You can choose to change this default behavior with the
'--skip-gitignore' option.
Examples:
$ balena sync 7cf02a6 --source . --destination /usr/src/app
$ balena sync 7cf02a6 -s /home/user/myBalenaProject -d /usr/src/app --before 'echo Hello' --after 'echo Done'
$ balena sync --ignore lib/
$ balena sync --verbose false
$ balena sync
### Options
#### --source, -s &#60;path&#62;
local directory path to synchronize to device
#### --destination, -d &#60;path&#62;
destination path on device
#### --ignore, -i &#60;paths&#62;
comma delimited paths to ignore when syncing
#### --skip-gitignore
do not parse excluded/included files from .gitignore
#### --skip-restart
do not restart container after syncing
#### --before, -b &#60;command&#62;
execute a command before syncing
#### --after, -a &#60;command&#62;
execute a command after syncing
#### --port, -t &#60;port&#62;
ssh port
#### --progress, -p
show progress
#### --verbose, -v
increase verbosity
# SSH
## ssh [uuid]
## ssh &#60;applicationOrDevice&#62; [serviceName]
This command can be used to start a shell on a local or remote device.
If a service name is not provided, a shell will be opened on the host OS.
If an application name is provided, all online devices in the application
will be presented, and the chosen device will then have a shell opened on
in it's service container or host OS.
For local devices, the ip address and .local domain name are supported.
Examples:
balena ssh MyApp
balena ssh f49cefd
balena ssh f49cefd my-service
balena ssh f49cefd --port <port>
balena ssh 192.168.0.1 --verbose
balena ssh f49cefd.local my-service
Warning: 'balena ssh' requires an openssh-compatible client 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 get a shell into the running application container of
your device.
Examples:
$ balena ssh MyApp
$ balena ssh 7cf02a6
$ balena ssh 7cf02a6 --port 8080
$ balena ssh 7cf02a6 -v
$ balena ssh 7cf02a6 -s
$ balena ssh 7cf02a6 --noninteractive
support) please check:
https://github.com/balena-io/balena-cli/blob/master/INSTALL.md#additional-dependencies
### Options
#### --port, -p &#60;port&#62;
ssh gateway port
SSH gateway port
#### --verbose, -v
increase verbosity
#### --host, -s
access host OS (for devices with balenaOS >= 2.0.0+rev1)
Increase verbosity
#### --noproxy
don't use the proxy configuration for this connection. Only makes sense if you've configured proxy globally.
#### --noninteractive
run command non-interactively, do not automatically suggest devices to connect to if UUID not found
Don't use the proxy configuration for this connection. This flag
only make sense if you've configured a proxy globally.
## tunnel &#60;uuid&#62;
@ -1404,7 +1306,7 @@ Disables check for matching architecture in image and application
#### --pin-device-to-release, -p
Pin the preloaded device (not application) to the preloaded release on provision
Pin the preloaded device to the preloaded release on provision
#### --add-certificate &#60;certificate.crt&#62;
@ -1421,7 +1323,7 @@ Path to a local docker socket (e.g. /var/run/docker.sock)
Docker daemon hostname or IP address (dev machine or balena device)
#### --dockerPort, -p &#60;dockerPort&#62;
#### --dockerPort &#60;dockerPort&#62;
Docker daemon TCP port number (hint: 2375 for balena devices)
@ -1458,9 +1360,12 @@ The logs from only a single service can be shown with the --service flag, and
showing only the system logs can be achieved with --system. Note that these
flags can be used together.
It is also possible to run a push to a local mode device in live mode.
This will watch for changes in the source directory and perform an
in-place build in the running containers [BETA].
When pushing to a local device a live session will be started.
The project source folder is watched for filesystem events, and changes
to files and folders are automatically synchronized to the running
containers. The synchronisation is only in one direction, from this machine to
the device, and changes made on the device itself may be overwritten.
This feature requires a device running supervisor version v9.7.0 or greater.
The --registry-secrets option specifies a JSON or YAML file containing private
Docker registry usernames and passwords to be used when pulling base images.
@ -1486,6 +1391,7 @@ Examples:
$ balena push 10.0.0.1 --source <source directory>
$ balena push 10.0.0.1 --service my-service
$ balena push 10.0.0.1 --env MY_ENV_VAR=value --env my-service:SERVICE_VAR=value
$ balena push 10.0.0.1 --nolive
$ balena push 23c73a1.local --system
$ balena push 23c73a1.local --system --service my-service
@ -1512,16 +1418,11 @@ Don't use cache when building this project
Path to a local YAML or JSON file containing Docker registry passwords used to pull base images
#### --live, -l
#### --nolive
Note this feature is in beta.
Start a live session with the containers pushed to a local mode device.
The project source folder is watched for filesystem events, and changes
to files and folders are automatically synchronized to the running
containers. The synchronisation is only in one direction, from this machine to
the device, and changes made on the device itself may be overwritten.
This feature requires a device running supervisor version v9.7.0 or greater.
Don't run a live session on this push. The filesystem will not be monitored, and changes
will not be synchronised to any running containers. Note that both this flag and --detached
and required to cause the process to end once the initial build has completed.
#### --detached, -d
@ -1529,7 +1430,8 @@ Don't tail application logs when pushing to a local mode device
#### --service &#60;service&#62;
Only show logs from 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 and other --service flags.
Only valid when pushing to a local mode device.
#### --system
@ -1588,212 +1490,6 @@ confirm non-interactively
drive
## local logs [deviceIp]
Examples:
$ balena local logs
$ balena local logs -f
$ balena local logs 192.168.1.10
$ balena local logs 192.168.1.10 -f
$ balena local logs 192.168.1.10 -f --app-name myapp
### Options
#### --follow, -f
follow log
#### --app-name, -a &#60;name&#62;
name of container to get logs from
## local scan
Examples:
$ balena local scan
$ balena local scan --timeout 120
$ balena local scan --verbose
### Options
#### --verbose, -v
Display full info
#### --timeout, -t &#60;timeout&#62;
Scan timeout in seconds
## local ssh [deviceIp]
Warning: 'balena local ssh' requires an openssh-compatible client 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 get a shell into the running application container of
your device.
The '--host' option will get you a shell into the Host OS of the balenaOS device.
No option will return a list of containers to enter or you can explicitly select
one by passing its name to the --container option
Examples:
$ balena local ssh
$ balena local ssh --host
$ balena local ssh --container chaotic_water
$ balena local ssh --container chaotic_water --port 22222
$ balena local ssh --verbose
### Options
#### --verbose, -v
increase verbosity
#### --host, -s
get a shell into the host OS
#### --container, -c &#60;container&#62;
name of container to access
#### --port, -p &#60;port&#62;
ssh port number (default: 22222)
## local push [deviceIp]
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Deprecation notice: `balena local push` is deprecated and will be removed in a
future release of the CLI. Please use `balena push <ipAddress>` instead.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Use this command to push your local changes to a container on a LAN-accessible
balenaOS device on the fly.
This command requires an openssh-compatible 'ssh' client and 'rsync' to be
available in the executable PATH of the shell environment. For more information
(including Windows support) please check the README at:
https://github.com/balena-io/balena-cli
If `Dockerfile` or any file in the 'build-triggers' list is changed,
a new container will be built and run on your device.
If not, changes will simply be synced with `rsync` into the application container.
After every 'balena local push' the updated settings will be saved in
'<source>/.balena-sync.yml' and will be used in later invocations. You can
also change any option by editing '.balena-sync.yml' directly.
Here is an example '.balena-sync.yml' :
$ cat $PWD/.balena-sync.yml
local_balenaos:
app-name: local-app
build-triggers:
- Dockerfile: file-hash-abcdefabcdefabcdefabcdefabcdefabcdef
- package.json: file-hash-abcdefabcdefabcdefabcdefabcdefabcdef
environment:
- MY_VARIABLE=123
Command line options have precedence over the ones saved in '.balena-sync.yml'.
If '.gitignore' is found in the source directory then all explicitly listed files will be
excluded when using rsync to update the container. You can choose to change this default behavior with the
'--skip-gitignore' option.
Examples:
$ balena local push
$ balena local push --app-name test-server --build-triggers package.json,requirements.txt
$ balena local push --force-build
$ balena local push --force-build --skip-logs
$ balena local push --ignore lib/
$ balena local push --verbose false
$ balena local push 192.168.2.10 --source . --destination /usr/src/app
$ balena local push 192.168.2.10 -s /home/user/balenaProject -d /usr/src/app --before 'echo Hello' --after 'echo Done'
### Options
#### --source, -s &#60;path&#62;
root of project directory to push
#### --destination, -d &#60;path&#62;
destination path on device container
#### --ignore, -i &#60;paths&#62;
comma delimited paths to ignore when syncing with 'rsync'
#### --skip-gitignore
do not parse excluded/included files from .gitignore
#### --before, -b &#60;command&#62;
execute a command before pushing
#### --after, -a &#60;command&#62;
execute a command after pushing
#### --progress, -p
show progress
#### --skip-logs
do not stream logs after push
#### --verbose, -v
increase verbosity
#### --app-name, -n &#60;name&#62;
application name - may contain lowercase characters, digits and one or more dashes. It may not start or end with a dash.
#### --build-triggers, -r &#60;files&#62;
comma delimited file list that will trigger a container rebuild if changed
#### --force-build, -f
force a container build and run
#### --env, -e &#60;env&#62;
environment variable (e.g. --env 'ENV=value'). Multiple --env parameters are supported.
## local stop [deviceIp]
Examples:
$ balena local stop
$ balena local stop --app-name myapp
$ balena local stop --all
$ balena local stop 192.168.1.10
$ balena local stop 192.168.1.10 --app-name myapp
### Options
#### --all
stop all containers
#### --app-name, -a &#60;name&#62;
name of container to stop
# Deploy
## build [source]
@ -2082,4 +1778,3 @@ Examples:
Use this command to list your machine's drives usable for writing the OS image to.
Skips the system drives.

150
lib/actions-oclif/env/add.ts vendored Normal file
View File

@ -0,0 +1,150 @@
/**
* @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, flags } from '@oclif/command';
import { stripIndent } from 'common-tags';
import { CommandHelp } from '../../utils/oclif-utils';
interface FlagsDef {
application?: string;
device?: string;
help: void;
quiet: boolean;
}
interface ArgsDef {
name: string;
value?: string;
}
export default class EnvAddCmd extends Command {
public static description = stripIndent`
Add an environment 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 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.
Service-specific variables are not currently supported. The given command line
examples variables that apply to all services in an app or device.
`;
public static examples = [
'$ balena env add TERM --application MyApp',
'$ balena env add EDITOR vim --application MyApp',
'$ balena env add EDITOR vim --device 7cf02a6',
];
public static args = [
{
name: 'name',
required: true,
description: 'environment or config variable name',
},
{
name: 'value',
required: false,
description:
"variable value; if omitted, use value from CLI's environment",
},
];
// hardcoded 'env add' to avoid oclif's 'env:add' topic syntax
public static usage =
'env add ' + new CommandHelp({ args: EnvAddCmd.args }).defaultUsage();
public static flags = {
application: flags.string({
char: 'a',
description: 'application name',
exclusive: ['device'],
}),
device: flags.string({
char: 'd',
description: 'device UUID',
exclusive: ['application'],
}),
help: flags.help({ char: 'h' }),
quiet: flags.boolean({
char: 'q',
description: 'suppress warning messages',
default: false,
}),
};
public async run() {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
EnvAddCmd,
);
const Bluebird = await import('bluebird');
const _ = await import('lodash');
const balena = (await import('balena-sdk')).fromSharedOptions();
const { exitWithExpectedError } = await import('../../utils/patterns');
const cmd = this;
await Bluebird.try(async function() {
if (params.value == null) {
params.value = process.env[params.name];
if (params.value == null) {
throw new Error(
`Environment value not found for variable: ${params.name}`,
);
} else if (!options.quiet) {
cmd.warn(
`Using ${params.name}=${params.value} from CLI process environment`,
);
}
}
const reservedPrefixes = await getReservedPrefixes();
const isConfigVar = _.some(reservedPrefixes, prefix =>
_.startsWith(params.name, prefix),
);
if (options.application) {
return balena.models.application[
isConfigVar ? 'configVar' : 'envVar'
].set(options.application, params.name, params.value);
} else if (options.device) {
return balena.models.device[isConfigVar ? 'configVar' : 'envVar'].set(
options.device,
params.name,
params.value,
);
} else {
exitWithExpectedError('You must specify an application or device');
}
});
}
}
async function getReservedPrefixes(): Promise<string[]> {
const balena = (await import('balena-sdk')).fromSharedOptions();
const settings = await balena.settings.getAll();
const response = await balena.request.send({
baseUrl: settings.apiUrl,
url: '/config/vars',
});
return response.body.reservedNamespaces;
}

View File

@ -102,8 +102,9 @@ exports.login =
return patterns.askLoginType().then (loginType) ->
if loginType is 'register'
{ runCommand } = require('../utils/helpers')
return runCommand('signup')
signupUrl = 'https://dashboard.balena-cloud.com/signup'
require('opn')(signupUrl, { wait: false })
patterns.exitWithExpectedError("Please sign up at #{signupUrl}")
options[loginType] = true
return login(options)
@ -139,47 +140,6 @@ exports.logout =
balena = require('balena-sdk').fromSharedOptions()
balena.auth.logout().nodeify(done)
exports.signup =
signature: 'signup'
description: 'signup to balena'
help: '''
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
'''
action: (params, options, done) ->
balena = require('balena-sdk').fromSharedOptions()
form = require('resin-cli-form')
validation = require('../utils/validation')
balena.settings.get('balenaUrl').then (balenaUrl) ->
console.log("\nRegistering to #{balenaUrl}")
form.run [
message: 'Email:'
name: 'email'
type: 'input'
validate: validation.validateEmail
,
message: 'Password:'
name: 'password'
type: 'password',
validate: validation.validatePassword
]
.then(balena.auth.register)
.then(balena.auth.loginWithToken)
.nodeify(done)
exports.whoami =
signature: 'whoami'
description: 'get current username and email address'

View File

@ -13,26 +13,14 @@ 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 { ApplicationVariable, DeviceVariable } from 'balena-sdk';
import * as Bluebird from 'bluebird';
import { CommandDefinition } from 'capitano';
import { stripIndent } from 'common-tags';
import { normalizeUuidProp } from '../utils/normalization';
import * as commandOptions from './command-options';
const getReservedPrefixes = async (): Promise<string[]> => {
const balena = (await import('balena-sdk')).fromSharedOptions();
const settings = await balena.settings.getAll();
const response = await balena.request.send({
baseUrl: settings.apiUrl,
url: '/config/vars',
});
return response.body.reservedNamespaces;
};
export const list: CommandDefinition<
{},
{
@ -73,14 +61,13 @@ export const list: CommandDefinition<
permission: 'user',
async action(_params, options, done) {
normalizeUuidProp(options, 'device');
const Bluebird = await import('bluebird');
const _ = await import('lodash');
const balena = (await import('balena-sdk')).fromSharedOptions();
const visuals = await import('resin-cli-visuals');
const { exitWithExpectedError } = await import('../utils/patterns');
return Bluebird.try(function(): Promise<
return Bluebird.try(function(): Bluebird<
DeviceVariable[] | ApplicationVariable[]
> {
if (options.application) {
@ -151,7 +138,7 @@ export const remove: CommandDefinition<
return patterns
.confirm(
options.yes,
options.yes || false,
'Are you sure you want to delete the environment variable?',
)
.then(function() {
@ -171,87 +158,6 @@ export const remove: CommandDefinition<
},
};
export const add: CommandDefinition<
{
key: string;
value?: string;
},
{
application?: string;
device?: string;
}
> = {
signature: 'env add <key> [value]',
description: 'add an environment or config variable',
help: stripIndent`
Use this command to add an enviroment or config variable to an application
or device.
If value is omitted, the tool will attempt to use the variable's value
as defined in your host machine.
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.
Examples:
$ balena env add EDITOR vim --application MyApp
$ balena env add TERM --application MyApp
$ balena env add EDITOR vim --device 7cf02a6
`,
options: [commandOptions.optionalApplication, commandOptions.optionalDevice],
permission: 'user',
async action(params, options, done) {
normalizeUuidProp(options, 'device');
const Bluebird = await import('bluebird');
const _ = await import('lodash');
const balena = (await import('balena-sdk')).fromSharedOptions();
const { exitWithExpectedError } = await import('../utils/patterns');
return Bluebird.try(async function() {
if (params.value == null) {
params.value = process.env[params.key];
if (params.value == null) {
throw new Error(`Environment value not found for key: ${params.key}`);
} else {
console.info(
`Warning: using ${params.key}=${
params.value
} from host environment`,
);
}
}
const reservedPrefixes = await getReservedPrefixes();
const isConfigVar = _.some(reservedPrefixes, prefix =>
_.startsWith(params.key, prefix),
);
if (options.application) {
return balena.models.application[
isConfigVar ? 'configVar' : 'envVar'
].set(options.application, params.key, params.value);
} else if (options.device) {
return balena.models.device[isConfigVar ? 'configVar' : 'envVar'].set(
options.device,
params.key,
params.value,
);
} else {
exitWithExpectedError('You must specify an application or device');
}
}).nodeify(done);
},
};
export const rename: CommandDefinition<
{
id: number;
@ -265,7 +171,7 @@ export const rename: CommandDefinition<
description: 'rename an environment variable',
help: stripIndent`
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.
@ -280,7 +186,6 @@ export const rename: CommandDefinition<
permission: 'user',
options: [commandOptions.booleanDevice],
async action(params, options, done) {
const Bluebird = await import('bluebird');
const balena = (await import('balena-sdk')).fromSharedOptions();
return Bluebird.try(function() {

View File

@ -17,11 +17,14 @@ limitations under the License.
_ = require('lodash')
capitano = require('capitano')
columnify = require('columnify')
messages = require('../utils/messages')
{ getManualSortCompareFunction } = require('../utils/helpers')
{ exitWithExpectedError } = require('../utils/patterns')
{ getOclifHelpLinePairs } = require('./help_ts')
parse = (object) ->
return _.fromPairs _.map(object, (item) ->
return _.map object, (item) ->
# Hacky way to determine if an object is
# a function or a command
@ -33,18 +36,37 @@ parse = (object) ->
return [
signature
item.description
]).sort()
]
indent = (text) ->
text = _.map text.split('\n'), (line) ->
return ' ' + line
return text.join('\n')
print = (data) ->
console.log indent columnify data,
print = (usageDescriptionPairs) ->
console.log indent columnify _.fromPairs(usageDescriptionPairs),
showHeaders: false
minWidth: 35
manuallySortedPrimaryCommands = [
'help',
'login',
'push',
'logs',
'ssh',
'apps',
'app',
'devices',
'device',
'tunnel',
'preload',
'build',
'deploy',
'join',
'leave',
'local scan',
]
general = (params, options, done) ->
console.log('Usage: balena [COMMAND] [OPTIONS]\n')
console.log(messages.reachingOut)
@ -60,17 +82,21 @@ general = (params, options, done) ->
return 'primary'
return 'secondary'
print(parse(groupedCommands.primary))
print parse(groupedCommands.primary).sort(getManualSortCompareFunction(
manuallySortedPrimaryCommands,
([signature, description], manualItem) ->
signature == manualItem or signature.startsWith("#{manualItem} ")
))
if options.verbose
console.log('\nAdditional commands:\n')
print(parse(groupedCommands.secondary))
print parse(groupedCommands.secondary).concat(getOclifHelpLinePairs()).sort()
else
console.log('\nRun `balena help --verbose` to list additional commands')
if not _.isEmpty(capitano.state.globalOptions)
console.log('\nGlobal Options:\n')
print(parse(capitano.state.globalOptions))
print parse(capitano.state.globalOptions).sort()
return done()
@ -90,7 +116,7 @@ command = (params, options, done) ->
if not _.isEmpty(command.options)
console.log('\nOptions:\n')
print(parse(command.options))
print parse(command.options).sort()
return done()

35
lib/actions/help_ts.ts Normal file
View File

@ -0,0 +1,35 @@
/**
* @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 } from '@oclif/command';
import * as _ from 'lodash';
import EnvAddCmd from '../actions-oclif/env/add';
export function getOclifHelpLinePairs(): [[string, string]] {
return [getCmdUsageDescriptionLinePair(EnvAddCmd)];
}
function getCmdUsageDescriptionLinePair(cmd: typeof Command): [string, string] {
const usage = (cmd.usage || '').toString().toLowerCase();
let description = '';
const matches = /\s*(.+?)\n.*/s.exec(cmd.description || '');
if (matches && matches.length > 1) {
description = _.lowerFirst(_.trimEnd(matches[1], '.'));
}
return [usage, description];
}

View File

@ -25,12 +25,12 @@ module.exports =
keys: require('./keys')
logs: require('./logs')
local: require('./local')
scan: require('./scan')
notes: require('./notes')
help: require('./help')
os: require('./os')
settings: require('./settings')
config: require('./config')
sync: require('./sync')
ssh: require('./ssh')
internal: require('./internal')
build: require('./build')

View File

@ -205,6 +205,7 @@ module.exports =
root: true
action: (params, options, done) ->
Promise = require('bluebird')
path = require('path')
umount = require('umount')
umountAsync = Promise.promisify(umount.umount)
isMountedAsync = Promise.promisify(umount.isMounted)
@ -217,7 +218,12 @@ module.exports =
return if not isMounted
umountAsync(params.target)
.then (configurationSchema) ->
denymount params.target, (cb) ->
dmOpts = {}
if process.pkg
# when running in a standalone pkg install, the 'denymount'
# executable is placed on the same folder as process.execPath
dmOpts.executablePath = path.join(path.dirname(process.execPath), 'denymount')
dmHandler = (cb) ->
reconfix.readConfiguration(configurationSchema, params.target)
.then(getConfiguration)
.then (answers) ->
@ -225,6 +231,7 @@ module.exports =
removeHostname(configurationSchema)
reconfix.writeConfiguration(configurationSchema, answers, params.target)
.asCallback(cb)
denymount params.target, dmHandler, dmOpts
.then ->
console.log('Done!')
.asCallback(done)

View File

@ -16,8 +16,3 @@ limitations under the License.
exports.configure = require('./configure')
exports.flash = require('./flash').flash
exports.logs = require('./logs')
exports.scan = require('./scan')
exports.ssh = require('./ssh')
exports.push = require('./push')
exports.stop = require('./stop')

View File

@ -38,9 +38,9 @@ export const logs: CommandDefinition<
uuidOrDevice: string;
},
{
tail: boolean;
service: string;
system: boolean;
tail?: boolean;
service?: [string] | string;
system?: boolean;
}
> = {
signature: 'logs <uuidOrDevice>',
@ -66,6 +66,7 @@ export const logs: CommandDefinition<
$ 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`,
@ -78,8 +79,9 @@ export const logs: CommandDefinition<
},
{
signature: 'service',
description:
'Only show logs for a single service. This can be used in combination with --system',
description: stripIndent`
Reject logs not originating from this service.
This can be used in combination with --system or other --service flags.`,
parameter: 'service',
alias: 's',
},
@ -91,21 +93,30 @@ export const logs: CommandDefinition<
'Only show system logs. This can be used in combination with --service.',
},
],
permission: 'user',
primary: true,
async action(params, options, done) {
normalizeUuidProp(params);
const balena = (await import('balena-sdk')).fromSharedOptions();
const isArray = await import('lodash/isArray');
const { serviceIdToName } = await import('../utils/cloud');
const { displayDeviceLogs, displayLogObject } = await import(
'../utils/device/logs'
);
const { validateIPAddress } = await import('../utils/validation');
const { exitWithExpectedError } = await import('../utils/patterns');
const { exitIfNotLoggedIn, exitWithExpectedError } = await import(
'../utils/patterns'
);
const Logger = await import('../utils/logger');
const logger = new Logger();
const servicesToDisplay =
options.service != null
? isArray(options.service)
? options.service
: [options.service]
: undefined;
const displayCloudLog = async (line: CloudLog) => {
if (!line.isSystem) {
let serviceName = await serviceIdToName(balena, line.serviceId);
@ -116,14 +127,14 @@ export const logs: CommandDefinition<
{ serviceName, ...line },
logger,
options.system || false,
options.service,
servicesToDisplay,
);
} else {
displayLogObject(
line,
logger,
options.system || false,
options.service,
servicesToDisplay,
);
}
};
@ -150,9 +161,10 @@ export const logs: CommandDefinition<
logStream,
logger,
options.system || false,
options.service,
servicesToDisplay,
);
} else {
exitIfNotLoggedIn();
if (options.tail) {
return balena.logs
.subscribe(params.uuidOrDevice, { count: 100 })

View File

@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
###
_ = require('lodash')
dockerUtils = require('../utils/docker')
allDeviceTypes = undefined
@ -131,6 +133,54 @@ offerToDisableAutomaticUpdates = (application, commit, pinDevice) ->
body:
should_track_latest_release: false
preloadOptions = dockerUtils.appendConnectionOptions [
{
signature: 'app'
parameter: 'appId'
description: 'id of the application to preload'
alias: 'a'
}
{
signature: 'commit'
parameter: 'hash'
description: '''
The commit hash for a specific application release to preload, use "current" to specify the current
release (ignored if no appId is given). The current release is usually also the latest, but can be
manually pinned using https://github.com/balena-io-projects/staged-releases .
'''
alias: 'c'
}
{
signature: 'splash-image'
parameter: 'splashImage.png'
description: 'path to a png image to replace the splash screen'
alias: 's'
}
{
signature: 'dont-check-arch'
boolean: true
description: 'Disables check for matching architecture in image and application'
}
{
signature: 'pin-device-to-release'
boolean: true
description: 'Pin the preloaded device to the preloaded release on provision'
alias: 'p'
}
{
signature: 'add-certificate'
parameter: 'certificate.crt'
description: '''
Add the given certificate (in PEM format) to /etc/ssl/certs in the preloading container.
The file name must end with '.crt' and must not be already contained in the preloader's
/etc/ssl/certs folder.
Can be repeated to add multiple certificates.
'''
}
]
# Remove dockerPort `-p` alias as it conflicts with pin-device-to-release
delete _.find(preloadOptions, signature: 'dockerPort').alias
module.exports =
signature: 'preload <image>'
description: 'preload an app on a disk image (or Edison zip archive)'
@ -149,51 +199,7 @@ module.exports =
'''
permission: 'user'
primary: true
options: dockerUtils.appendConnectionOptions [
{
signature: 'app'
parameter: 'appId'
description: 'id of the application to preload'
alias: 'a'
}
{
signature: 'commit'
parameter: 'hash'
description: '''
The commit hash for a specific application release to preload, use "current" to specify the current
release (ignored if no appId is given). The current release is usually also the latest, but can be
manually pinned using https://github.com/balena-io-projects/staged-releases .
'''
alias: 'c'
}
{
signature: 'splash-image'
parameter: 'splashImage.png'
description: 'path to a png image to replace the splash screen'
alias: 's'
}
{
signature: 'dont-check-arch'
boolean: true
description: 'Disables check for matching architecture in image and application'
}
{
signature: 'pin-device-to-release'
boolean: true
description: 'Pin the preloaded device (not application) to the preloaded release on provision'
alias: 'p'
}
{
signature: 'add-certificate'
parameter: 'certificate.crt'
description: '''
Add the given certificate (in PEM format) to /etc/ssl/certs in the preloading container.
The file name must end with '.crt' and must not be already contained in the preloader's
/etc/ssl/certs folder.
Can be repeated to add multiple certificates.
'''
}
]
options: preloadOptions
action: (params, options, done) ->
_ = require('lodash')
Promise = require('bluebird')

View File

@ -105,16 +105,16 @@ export const push: CommandDefinition<
applicationOrDevice_raw: string;
},
{
source: string;
emulated: boolean;
dockerfile: string; // DeviceDeployOptions.dockerfilePath (alternative Dockerfile)
nocache: boolean;
'registry-secrets': string;
live: boolean;
detached: boolean;
service: string;
system: boolean;
env: string | string[];
source?: string;
emulated?: boolean;
dockerfile?: string; // DeviceDeployOptions.dockerfilePath (alternative Dockerfile)
nocache?: boolean;
'registry-secrets'?: string;
nolive?: boolean;
detached?: boolean;
service?: string | string[];
system?: boolean;
env?: string | string[];
}
> = {
signature: 'push <applicationOrDevice>',
@ -139,9 +139,12 @@ export const push: CommandDefinition<
showing only the system logs can be achieved with --system. Note that these
flags can be used together.
It is also possible to run a push to a local mode device in live mode.
This will watch for changes in the source directory and perform an
in-place build in the running containers [BETA].
When pushing to a local device a live session will be started.
The project source folder is watched for filesystem events, and changes
to files and folders are automatically synchronized to the running
containers. The synchronisation is only in one direction, from this machine to
the device, and changes made on the device itself may be overwritten.
This feature requires a device running supervisor version v9.7.0 or greater.
${registrySecretsHelp.split('\n').join('\n\t\t')}
@ -155,6 +158,7 @@ export const push: CommandDefinition<
$ balena push 10.0.0.1 --source <source directory>
$ balena push 10.0.0.1 --service my-service
$ balena push 10.0.0.1 --env MY_ENV_VAR=value --env my-service:SERVICE_VAR=value
$ balena push 10.0.0.1 --nolive
$ balena push 23c73a1.local --system
$ balena push 23c73a1.local --system --service my-service
@ -193,18 +197,12 @@ export const push: CommandDefinition<
Path to a local YAML or JSON file containing Docker registry passwords used to pull base images`,
},
{
signature: 'live',
alias: 'l',
signature: 'nolive',
boolean: true,
description: stripIndent`
Note this feature is in beta.
Start a live session with the containers pushed to a local mode device.
The project source folder is watched for filesystem events, and changes
to files and folders are automatically synchronized to the running
containers. The synchronisation is only in one direction, from this machine to
the device, and changes made on the device itself may be overwritten.
This feature requires a device running supervisor version v9.7.0 or greater.`,
Don't run a live session on this push. The filesystem will not be monitored, and changes
will not be synchronised to any running containers. Note that both this flag and --detached
and required to cause the process to end once the initial build has completed.`,
},
{
signature: 'detached',
@ -215,7 +213,8 @@ export const push: CommandDefinition<
{
signature: 'service',
description: stripIndent`
Only show logs from 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 and other --service flags.
Only valid when pushing to a local mode device.`,
parameter: 'service',
},
@ -243,6 +242,7 @@ export const push: CommandDefinition<
async action(params, options, done) {
const sdk = (await import('balena-sdk')).fromSharedOptions();
const Bluebird = await import('bluebird');
const isArray = await import('lodash/isArray');
const remote = await import('../utils/remote-build');
const deviceDeploy = await import('../utils/device/deploy');
const { exitIfNotLoggedIn, exitWithExpectedError } = await import(
@ -277,9 +277,9 @@ export const push: CommandDefinition<
switch (buildTarget) {
case BuildTarget.Cloud:
// Ensure that the live argument has not been passed to a cloud build
if (options.live) {
if (options.nolive != null) {
exitWithExpectedError(
'The --live flag is only valid when pushing to a local mode device',
'The --nolive flag is only valid when pushing to a local mode device',
);
}
if (options.detached) {
@ -312,8 +312,8 @@ export const push: CommandDefinition<
async (token, baseUrl, owner) => {
const opts = {
dockerfilePath,
emulated: options.emulated,
nocache: options.nocache,
emulated: options.emulated || false,
nocache: options.nocache || false,
registrySecrets,
};
const args = {
@ -332,6 +332,12 @@ export const push: CommandDefinition<
break;
case BuildTarget.Device:
const device = appOrDevice;
const servicesToDisplay =
options.service != null
? isArray(options.service)
? options.service
: [options.service]
: undefined;
// TODO: Support passing a different port
await Bluebird.resolve(
deviceDeploy.deployToDevice({
@ -340,9 +346,9 @@ export const push: CommandDefinition<
dockerfilePath,
registrySecrets,
nocache: options.nocache || false,
live: options.live || false,
nolive: options.nolive || false,
detached: options.detached || false,
service: options.service,
services: servicesToDisplay,
system: options.system || false,
env:
typeof options.env === 'string'

View File

@ -33,15 +33,15 @@ dockerVersionProperties = [
]
module.exports =
signature: 'local scan'
signature: 'scan'
description: 'Scan for balenaOS devices in your local network'
help: '''
Examples:
$ balena local scan
$ balena local scan --timeout 120
$ balena local scan --verbose
$ balena scan
$ balena scan --timeout 120
$ balena scan --verbose
'''
options: [
signature: 'verbose'
@ -62,9 +62,9 @@ module.exports =
prettyjson = require('prettyjson')
{ discover } = require('balena-sync')
{ SpinnerPromise } = require('resin-cli-visuals')
{ dockerPort, dockerTimeout } = require('./common')
dockerUtils = require('../../utils/docker')
{ exitWithExpectedError } = require('../../utils/patterns')
{ dockerPort, dockerTimeout } = require('./local/common')
dockerUtils = require('../utils/docker')
{ exitWithExpectedError } = require('../utils/patterns')
if options.timeout?
options.timeout *= 1000

View File

@ -1,149 +0,0 @@
###
Copyright 2016-2017 Balena
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.
###
commandOptions = require('./command-options')
{ normalizeUuidProp } = require('../utils/normalization')
module.exports =
signature: 'ssh [uuid]'
description: 'get a shell into the running app container of a device'
help: '''
Warning: 'balena ssh' requires an openssh-compatible client 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 get a shell into the running application container of
your device.
Examples:
$ balena ssh MyApp
$ balena ssh 7cf02a6
$ balena ssh 7cf02a6 --port 8080
$ balena ssh 7cf02a6 -v
$ balena ssh 7cf02a6 -s
$ balena ssh 7cf02a6 --noninteractive
'''
permission: 'user'
primary: true
options: [
signature: 'port'
parameter: 'port'
description: 'ssh gateway port'
alias: 'p'
,
signature: 'verbose'
boolean: true
description: 'increase verbosity'
alias: 'v'
commandOptions.hostOSAccess,
signature: 'noproxy'
boolean: true
description: "don't use the proxy configuration for this connection.
Only makes sense if you've configured proxy globally."
,
signature: 'noninteractive'
boolean: true
description: 'run command non-interactively, do not automatically suggest devices to connect to if UUID not found'
]
action: (params, options, done) ->
normalizeUuidProp(params)
child_process = require('child_process')
Promise = require('bluebird')
balena = require('balena-sdk').fromSharedOptions()
_ = require('lodash')
bash = require('bash')
hasbin = require('hasbin')
{ getSubShellCommand } = require('../utils/helpers')
patterns = require('../utils/patterns')
options.port ?= 22
verbose = if options.verbose then '-vvv' else ''
proxyConfig = global.PROXY_CONFIG
useProxy = !!proxyConfig and not options.noproxy
getSshProxyCommand = (hasTunnelBin) ->
return '' if not useProxy
if not hasTunnelBin
console.warn('''
Proxy is enabled but the `proxytunnel` binary cannot be found.
Please install it if you want to route the `balena ssh` requests through the proxy.
Alternatively you can pass `--noproxy` param to the `balena ssh` command to ignore the proxy config
for the `ssh` requests.
Attemmpting the unproxied request for now.
''')
return ''
tunnelOptions =
proxy: "#{proxyConfig.host}:#{proxyConfig.port}"
dest: '%h:%p'
{ proxyAuth } = proxyConfig
if proxyAuth
i = proxyAuth.indexOf(':')
_.assign tunnelOptions,
user: proxyAuth.substring(0, i)
pass: proxyAuth.substring(i + 1)
proxyCommand = "proxytunnel #{bash.args(tunnelOptions, '--', '=')}"
return "-o #{bash.args({ ProxyCommand: proxyCommand }, '', '=')}"
Promise.try ->
return false if not params.uuid
return balena.models.device.has(params.uuid)
.then (uuidExists) ->
return params.uuid if uuidExists
if options.noninteractive
console.error("Could not find device: #{params.uuid}")
process.exit(1)
return patterns.inferOrSelectDevice()
.then (uuid) ->
console.info("Connecting to: #{uuid}")
balena.models.device.get(uuid)
.then (device) ->
patterns.exitWithExpectedError('Device is not online') if not device.is_online
Promise.props
username: balena.auth.whoami()
uuid: device.uuid
# get full uuid
containerId: if options.host then '' else balena.models.device.getApplicationInfo(device.uuid).get('containerId')
proxyUrl: balena.settings.get('proxyUrl')
hasTunnelBin: if useProxy then hasbin('proxytunnel') else null
.then ({ username, uuid, containerId, proxyUrl, hasTunnelBin }) ->
throw new Error('Did not find running application container') if not containerId?
Promise.try ->
sshProxyCommand = getSshProxyCommand(hasTunnelBin)
if options.host
accessCommand = "host #{uuid}"
else
accessCommand = "enter #{uuid} #{containerId}"
command = "ssh #{verbose} -t \
-o LogLevel=ERROR \
-o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
#{sshProxyCommand} \
-p #{options.port} #{username}@ssh.#{proxyUrl} #{accessCommand}"
subShellCommand = getSubShellCommand(command)
child_process.spawn subShellCommand.program, subShellCommand.args,
stdio: 'inherit'
.nodeify(done)

504
lib/actions/ssh.ts Normal file
View File

@ -0,0 +1,504 @@
/*
Copyright 2016-2019 Balena
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as BalenaSdk from 'balena-sdk';
import { CommandDefinition } from 'capitano';
import { stripIndent } from 'common-tags';
import { BalenaApplicationNotFound, BalenaDeviceNotFound } from 'balena-errors';
import {
validateApplicationName,
validateDotLocalUrl,
validateIPAddress,
validateShortUuid,
validateUuid,
} from '../utils/validation';
enum SSHTarget {
APPLICATION,
DEVICE,
LOCAL_DEVICE,
}
async function getSSHTarget(
sdk: BalenaSdk.BalenaSDK,
applicationOrDevice: string,
): Promise<{
target: SSHTarget;
deviceChecked?: boolean;
applicationChecked?: boolean;
device?: BalenaSdk.Device;
} | null> {
if (
validateDotLocalUrl(applicationOrDevice) ||
validateIPAddress(applicationOrDevice)
) {
return { target: SSHTarget.LOCAL_DEVICE };
}
const appTest = validateApplicationName(applicationOrDevice);
const uuidTest = validateUuid(applicationOrDevice);
if (appTest || uuidTest) {
// Do some further processing to work out which it is
if (appTest && !uuidTest) {
return {
target: SSHTarget.APPLICATION,
applicationChecked: false,
};
}
if (uuidTest && !appTest) {
return {
target: SSHTarget.DEVICE,
deviceChecked: false,
};
}
// This is the harder part, we have a string that
// fulfills both the uuid and application name
// requirements. We should go away and test for both a
// device with that uuid, and an application with that
// name, and choose the appropriate one
try {
await sdk.models.application.get(applicationOrDevice);
return { target: SSHTarget.APPLICATION, applicationChecked: true };
} catch (e) {
if (e instanceof BalenaApplicationNotFound) {
// Here we want to check for a device with that UUID
try {
const device = await sdk.models.device.get(applicationOrDevice, {
$select: ['id', 'uuid', 'supervisor_version', 'is_online'],
});
return { target: SSHTarget.DEVICE, deviceChecked: true, device };
} catch (err) {
if (err instanceof BalenaDeviceNotFound) {
throw new Error(
`Device or application not found: ${applicationOrDevice}`,
);
}
throw err;
}
}
throw e;
}
}
return null;
}
async function getContainerId(
sdk: BalenaSdk.BalenaSDK,
uuid: string,
serviceName: string,
sshOpts: {
port?: number;
proxyCommand?: string;
proxyUrl: string;
username: string;
},
version?: string,
id?: number,
): Promise<string> {
const semver = await import('resin-semver');
if (version == null || id == null) {
const device = await sdk.models.device.get(uuid, {
$select: ['id', 'supervisor_version'],
});
version = device.supervisor_version;
id = device.id;
}
let containerId: string | undefined;
if (semver.gte(version, '8.6.0')) {
const apiUrl = await sdk.settings.get('apiUrl');
// TODO: Move this into the SDKs device model
const request = await sdk.request.send({
method: 'POST',
url: '/supervisor/v2/containerId',
baseUrl: apiUrl,
body: {
method: 'GET',
deviceId: id,
},
});
if (request.status !== 200) {
throw new Error(
`There was an error connecting to device ${uuid}, HTTP response code: ${
request.status
}.`,
);
}
const body = request.body;
if (body.status !== 'success') {
throw new Error(
`There was an error communicating with device ${uuid}.\n\tError: ${
body.message
}`,
);
}
containerId = body.services[serviceName];
} else {
console.log(stripIndent`
Using legacy method to detect container ID. This will be slow.
To speed up this process, please update your device to an OS
which has a supervisor version of at least v8.6.0.
`);
// We need to execute a balena ps command on the device,
// and parse the output, looking for a specific
// container
const { child_process } = await import('mz');
const escapeRegex = await import('lodash/escapeRegExp');
const { getSubShellCommand } = await import('../utils/helpers');
const { deviceContainerEngineBinary } = await import('../utils/device/ssh');
const command = generateVpnSshCommand({
uuid,
verbose: false,
port: sshOpts.port,
command: `host ${uuid} '"${deviceContainerEngineBinary}" ps --format "{{.ID}} {{.Names}}"'`,
proxyCommand: sshOpts.proxyCommand,
proxyUrl: sshOpts.proxyUrl,
username: sshOpts.username,
});
const subShellCommand = getSubShellCommand(command);
const subprocess = child_process.spawn(
subShellCommand.program,
subShellCommand.args,
{
stdio: [null, 'pipe', null],
},
);
const containers = await new Promise<string>((resolve, reject) => {
let output = '';
subprocess.stdout.on('data', chunk => (output += chunk.toString()));
subprocess.on('close', (code: number) => {
if (code !== 0) {
reject(
new Error(
`Non-zero error code when looking for service container: ${code}`,
),
);
} else {
resolve(output);
}
});
});
const lines = containers.split('\n');
const regex = new RegExp(`\\/?${escapeRegex(serviceName)}_\\d+_\\d+`);
for (const container of lines) {
const [cId, name] = container.split(' ');
if (regex.test(name)) {
containerId = cId;
break;
}
}
}
if (containerId == null) {
throw new Error(
`Could not find a service ${serviceName} on device ${uuid}.`,
);
}
return containerId;
}
function generateVpnSshCommand(opts: {
uuid: string;
command: string;
verbose: boolean;
port?: number;
username: string;
proxyUrl: string;
proxyCommand?: string;
}) {
return (
`ssh ${
opts.verbose ? '-vvv' : ''
} -t -o LogLevel=ERROR -o StrictHostKeyChecking=no ` +
`-o UserKnownHostsFile=/dev/null ` +
`${opts.proxyCommand != null ? opts.proxyCommand : ''} ` +
`${opts.port != null ? `-p ${opts.port}` : ''} ` +
`${opts.username}@ssh.${opts.proxyUrl} ${opts.command}`
);
}
export const ssh: CommandDefinition<
{
applicationOrDevice: string;
serviceName?: string;
},
{
port: string;
service: string;
verbose: true | undefined;
noProxy: boolean;
}
> = {
signature: 'ssh <applicationOrDevice> [serviceName]',
description: 'SSH into the host or application container of a device',
primary: true,
help: stripIndent`
This command can be used to start a shell on a local or remote device.
If a service name is not provided, a shell will be opened on the host OS.
If an application name is provided, all online devices in the application
will be presented, and the chosen device will then have a shell opened on
in it's service container or host OS.
For local devices, the ip address and .local domain name are supported.
Examples:
balena ssh MyApp
balena ssh f49cefd
balena ssh f49cefd my-service
balena ssh f49cefd --port <port>
balena ssh 192.168.0.1 --verbose
balena ssh f49cefd.local my-service
Warning: 'balena ssh' requires an openssh-compatible client to be correctly
installed in your shell environment. For more information (including Windows
support) please check:
https://github.com/balena-io/balena-cli/blob/master/INSTALL.md#additional-dependencies`,
options: [
{
signature: 'port',
parameter: 'port',
description: 'SSH gateway port',
alias: 'p',
},
{
signature: 'verbose',
boolean: true,
description: 'Increase verbosity',
alias: 'v',
},
{
signature: 'noproxy',
boolean: true,
description: stripIndent`
Don't use the proxy configuration for this connection. This flag
only make sense if you've configured a proxy globally.`,
},
],
action: async (params, options) => {
const map = await import('lodash/map');
const bash = await import('bash');
// TODO: Make this typed
const hasbin = require('hasbin');
const { getSubShellCommand } = await import('../utils/helpers');
const { child_process } = await import('mz');
const { exitIfNotLoggedIn } = await import('../utils/patterns');
const { exitWithExpectedError, selectFromList } = await import(
'../utils/patterns'
);
const sdk = BalenaSdk.fromSharedOptions();
const verbose = options.verbose === true;
// ugh TODO: Fix this
const proxyConfig = (global as any).PROXY_CONFIG;
const useProxy = !!proxyConfig && !options.noProxy;
const port = options.port != null ? parseInt(options.port, 10) : undefined;
const getSshProxyCommand = (hasTunnelBin: boolean) => {
if (!useProxy) {
return '';
}
if (!hasTunnelBin) {
console.warn(stripIndent`
Proxy is enabled but the \`proxytunnel\` binary cannot be found.
Please install it if you want to route the \`balena ssh\` requests through the proxy.
Alternatively you can pass \`--noproxy\` param to the \`balena ssh\` command to ignore the proxy config
for the \`ssh\` requests.
Attempting the unproxied request for now.`);
return '';
}
let tunnelOptions: Dictionary<string> = {
proxy: `${proxyConfig.host}:${proxyConfig.port}`,
dest: '%h:%p',
};
const { proxyAuth } = proxyConfig;
if (proxyAuth) {
const i = proxyAuth.indexOf(':');
tunnelOptions = {
user: proxyAuth.substring(0, i),
pass: proxyAuth.substring(i + 1),
...tunnelOptions,
};
}
const proxyCommand = `proxytunnel ${bash.args(tunnelOptions, '--', '=')}`;
return `-o ${bash.args({ ProxyCommand: proxyCommand }, '', '=')}`;
};
// Detect what type of SSH we're doing
const maybeParamChecks = await getSSHTarget(
sdk,
params.applicationOrDevice,
);
if (maybeParamChecks == null) {
exitWithExpectedError(
new Error(stripIndent`
Could not parse SSH target.
You can provide an application name, IP address or .local address`),
);
}
const paramChecks = maybeParamChecks!;
switch (paramChecks.target) {
case SSHTarget.APPLICATION:
exitIfNotLoggedIn();
// Here what we want to do is fetch all device which
// are part of this application, and online
try {
const devices = await sdk.models.device.getAllByApplication(
params.applicationOrDevice,
{ $filter: { is_online: true }, $select: ['device_name', 'uuid'] },
);
const choice = await selectFromList(
'Please choose an online device to SSH into:',
map(devices, ({ device_name, uuid: uuidToChoose }) => ({
name: `${device_name} [${uuidToChoose.substr(0, 7)}]`,
uuid: uuidToChoose,
})),
);
// A little bit hacky, but it means we can fall
// through to the next handling mechanism
params.applicationOrDevice = choice.uuid;
} catch (e) {
if (e instanceof BalenaApplicationNotFound) {
exitWithExpectedError(
`Could not find an application named ${
params.applicationOrDevice
}`,
);
}
throw e;
}
case SSHTarget.DEVICE:
exitIfNotLoggedIn();
// We want to do two things here; firstly, check
// that the device exists and is accessible, and
// also convert a short uuid to a long one if
// necessary
let uuid = params.applicationOrDevice;
let version: string | undefined;
let id: number | undefined;
let isOnline: boolean | undefined;
// We also want to avoid checking for a device if we
// know it exists
if (!paramChecks.deviceChecked || validateShortUuid(uuid)) {
try {
const device = await sdk.models.device.get(uuid, {
$select: ['id', 'uuid', 'supervisor_version', 'is_online'],
});
uuid = device.uuid;
version = device.supervisor_version;
id = device.id;
isOnline = device.is_online;
} catch (e) {
if (e instanceof BalenaDeviceNotFound) {
exitWithExpectedError(`Could not find device: ${uuid}`);
}
}
} else {
version = paramChecks.device!.supervisor_version;
uuid = paramChecks.device!.uuid;
id = paramChecks.device!.id;
isOnline = paramChecks.device!.is_online;
}
if (!isOnline) {
throw new Error(`Device ${uuid} is not online.`);
}
const [hasTunnelBin, username, proxyUrl] = await Promise.all([
useProxy ? await hasbin('proxytunnel') : undefined,
sdk.auth.whoami(),
sdk.settings.get('proxyUrl'),
]);
const proxyCommand = getSshProxyCommand(hasTunnelBin);
if (username == null) {
exitWithExpectedError(
`Opening an SSH connection to a remote device requires you to be logged in.`,
);
}
// At this point, we have a long uuid with a device
// that we know exists and is accessible
let containerId: string | undefined;
if (params.serviceName != null) {
containerId = await getContainerId(
sdk,
uuid,
params.serviceName,
{
port,
proxyCommand,
proxyUrl,
username: username!,
},
version,
id,
);
}
let accessCommand: string;
if (containerId != null) {
accessCommand = `enter ${uuid} ${containerId}`;
} else {
accessCommand = `host ${uuid}`;
}
const command = generateVpnSshCommand({
uuid,
command: accessCommand,
verbose,
port,
proxyCommand,
proxyUrl,
username: username!,
});
const subShellCommand = getSubShellCommand(command);
await child_process.spawn(
subShellCommand.program,
subShellCommand.args,
{
stdio: 'inherit',
},
);
break;
case SSHTarget.LOCAL_DEVICE:
const { performLocalDeviceSSH } = await import('../utils/device/ssh');
await performLocalDeviceSSH({
address: params.applicationOrDevice,
port,
verbose,
service: params.serviceName,
});
break;
}
},
};

View File

@ -1,44 +0,0 @@
/*
Copyright 2016-2019 Balena
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as BalenaSync from 'balena-sync';
import { CommandDefinition } from 'capitano';
import { stripIndent } from 'common-tags';
export = deprecateSyncCmd(BalenaSync.capitano('balena-cli'));
const deprecationMsg = stripIndent`\
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
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!
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
`;
function deprecateSyncCmd(syncCmd: CommandDefinition): CommandDefinition {
syncCmd.primary = false;
syncCmd.description = syncCmd.description.replace(
'(beta)',
'[deprecated: see "help sync"]',
);
syncCmd.help = deprecationMsg + '\n\n' + syncCmd.help;
const originalAction = syncCmd.action;
syncCmd.action = (params, options, done): void => {
console.log(deprecationMsg);
originalAction(params, options, done);
};
return syncCmd;
}

View File

@ -180,8 +180,8 @@ export const tunnel: CommandDefinition<Args, Options> = {
return handler(client)
.then(() => {
logConnection(
client.remoteAddress,
client.remotePort,
client.remoteAddress || '',
client.remotePort || 0,
client.localAddress,
client.localPort,
device.vpn_address || '',
@ -190,8 +190,8 @@ export const tunnel: CommandDefinition<Args, Options> = {
})
.catch(err =>
logConnection(
client.remoteAddress,
client.remotePort,
client.remoteAddress || '',
client.remotePort || 0,
client.localAddress,
client.localPort,
device.vpn_address || '',

View File

@ -1,5 +1,5 @@
###
Copyright 2016-2017 Balena
Copyright 2016-2019 Balena
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -14,77 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
###
Raven = require('raven')
Raven.disableConsoleAlerts()
Raven.config require('./config').sentryDsn,
captureUnhandledRejections: true,
autoBreadcrumbs: true,
release: require('../package.json').version
.install (logged, error) ->
console.error(error)
process.exit(1)
Raven.setContext
extra:
args: process.argv
node_version: process.version
validNodeVersions = require('../package.json').engines.node
if not require('semver').satisfies(process.version, validNodeVersions)
console.warn """
Warning: this version of Node does not match the requirements of this package.
This package expects #{validNodeVersions}, but you're using #{process.version}.
This may cause unexpected behaviour.
To upgrade your Node, visit https://nodejs.org/en/download/
"""
# Doing this before requiring any other modules,
# including the 'balena-sdk', to prevent any module from reading the http proxy config
# before us
globalTunnel = require('global-tunnel-ng')
settings = require('balena-settings-client')
try
proxy = settings.get('proxy') or null
catch
proxy = null
# Init the tunnel even if the proxy is not configured
# because it can also get the proxy from the http(s)_proxy env var
# If that is not set as well the initialize will do nothing
globalTunnel.initialize(proxy)
# TODO: make this a feature of capitano https://github.com/balena-io/capitano/issues/48
global.PROXY_CONFIG = globalTunnel.proxyConfig
Promise = require('bluebird')
capitano = require('capitano')
capitanoExecuteAsync = Promise.promisify(capitano.execute)
# We don't yet use balena-sdk directly everywhere, but we set up shared
# options correctly so we can do safely in submodules
BalenaSdk = require('balena-sdk')
BalenaSdk.setSharedOptions(
apiUrl: settings.get('apiUrl')
imageMakerUrl: settings.get('imageMakerUrl')
dataDirectory: settings.get('dataDirectory')
retries: 2
)
actions = require('./actions')
errors = require('./errors')
events = require('./events')
update = require('./utils/update')
{ exitIfNotLoggedIn } = require('./utils/patterns')
# Assign bluebird as the global promise library
# stream-to-promise will produce native promises if not
# for this module, which could wreak havoc in this
# bluebird-only codebase.
require('any-promise/register/bluebird')
capitano.permission 'user', (done) ->
exitIfNotLoggedIn()
require('./utils/patterns').exitIfNotLoggedIn()
.then(done, done)
capitano.command
@ -116,7 +52,6 @@ capitano.command(actions.app.info)
# ---------- Auth Module ----------
capitano.command(actions.auth.login)
capitano.command(actions.auth.logout)
capitano.command(actions.auth.signup)
capitano.command(actions.auth.whoami)
# ---------- Device Module ----------
@ -147,7 +82,6 @@ capitano.command(actions.keys.remove)
# ---------- Env Module ----------
capitano.command(actions.env.list)
capitano.command(actions.env.add)
capitano.command(actions.env.rename)
capitano.command(actions.env.remove)
@ -176,9 +110,6 @@ capitano.command(actions.settings.list)
# ---------- Logs Module ----------
capitano.command(actions.logs.logs)
# ---------- Sync Module ----------
capitano.command(actions.sync)
# ---------- Tunnel Module ----------
capitano.command(actions.tunnel.tunnel)
@ -186,16 +117,12 @@ capitano.command(actions.tunnel.tunnel)
capitano.command(actions.preload)
# ---------- SSH Module ----------
capitano.command(actions.ssh)
capitano.command(actions.ssh.ssh)
# ---------- Local balenaOS Module ----------
capitano.command(actions.local.configure)
capitano.command(actions.local.flash)
capitano.command(actions.local.logs)
capitano.command(actions.local.push)
capitano.command(actions.local.ssh)
capitano.command(actions.local.scan)
capitano.command(actions.local.stop)
capitano.command(actions.scan)
# ---------- Public utils ----------
capitano.command(actions.util.availableDrives)
@ -216,14 +143,13 @@ capitano.command(actions.push.push)
capitano.command(actions.join.join)
capitano.command(actions.leave.leave)
update.notify()
cli = capitano.parse(process.argv)
runCommand = ->
capitanoExecuteAsync = Promise.promisify(capitano.execute)
if cli.global?.help
capitanoExecuteAsync(command: "help #{cli.command ? ''}")
else
capitanoExecuteAsync(cli)
Promise.all([events.trackCommand(cli), runCommand()])
.catch(errors.handle)
.catch(require('./errors').handleError)

107
lib/app-common.ts Normal file
View File

@ -0,0 +1,107 @@
/**
* @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.
*/
/**
* Sentry.io setup
* @see https://docs.sentry.io/clients/node/
*/
function setupRaven() {
const Raven = require('raven');
Raven.disableConsoleAlerts();
Raven.config(require('./config').sentryDsn, {
captureUnhandledRejections: true,
autoBreadcrumbs: true,
release: require('../package.json').version,
}).install(function(_logged: any, error: Error) {
console.error(error);
return process.exit(1);
});
Raven.setContext({
extra: {
args: process.argv,
node_version: process.version,
},
});
}
function checkNodeVersion() {
const validNodeVersions = require('../package.json').engines.node;
if (!require('semver').satisfies(process.version, validNodeVersions)) {
const { stripIndent } = require('common-tags');
console.warn(stripIndent`
------------------------------------------------------------------------------
Warning: Node version "${
process.version
}" does not match required versions "${validNodeVersions}".
This may cause unexpected behaviour. To upgrade Node, visit:
https://nodejs.org/en/download/
------------------------------------------------------------------------------
`);
}
}
function setupGlobalHttpProxy() {
// Doing this before requiring any other modules,
// including the 'balena-sdk', to prevent any module from reading the http proxy config
// before us
const globalTunnel = require('global-tunnel-ng');
const settings = require('balena-settings-client');
let proxy;
try {
proxy = settings.get('proxy') || null;
} catch (error1) {
proxy = null;
}
// Init the tunnel even if the proxy is not configured
// because it can also get the proxy from the http(s)_proxy env var
// If that is not set as well the initialize will do nothing
globalTunnel.initialize(proxy);
// TODO: make this a feature of capitano https://github.com/balena-io/capitano/issues/48
(global as any).PROXY_CONFIG = globalTunnel.proxyConfig;
}
function setupBalenaSdkSharedOptions() {
// We don't yet use balena-sdk directly everywhere, but we set up shared
// options correctly so we can do safely in submodules
const BalenaSdk = require('balena-sdk');
const settings = require('balena-settings-client');
BalenaSdk.setSharedOptions({
apiUrl: settings.get('apiUrl'),
imageMakerUrl: settings.get('imageMakerUrl'),
dataDirectory: settings.get('dataDirectory'),
retries: 2,
});
}
export function globalInit() {
setupRaven();
checkNodeVersion();
setupGlobalHttpProxy();
setupBalenaSdkSharedOptions();
// Assign bluebird as the global promise library.
// stream-to-promise will produce native promises if not for this module,
// which is likely to lead to errors as much of the CLI coffeescript code
// expects bluebird promises.
require('any-promise/register/bluebird');
// check for CLI updates once a day
require('./utils/update').notify();
}

37
lib/app-oclif.ts Normal file
View File

@ -0,0 +1,37 @@
/**
* @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 { ExitError } from '@oclif/errors';
import { handleError } from './errors';
/**
* oclif CLI entrypoint
*/
export function run(argv: string[]) {
process.argv = argv;
require('@oclif/command')
.run()
.then(require('@oclif/command/flush'))
.catch((error: Error) => {
// oclif sometimes exits with ExitError code 0 (not an error)
if (error instanceof ExitError && error.oclif.exit === 0) {
return;
}
handleError(error);
});
}

140
lib/app.ts Normal file
View File

@ -0,0 +1,140 @@
/**
* @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 { stripIndent } from 'common-tags';
import { exitWithExpectedError } from './utils/patterns';
/**
* Simple command-line pre-parsing to choose between oclif or Capitano.
* @param argv process.argv
*/
function routeCliFramework(argv: string[]): void {
if (process.env.DEBUG) {
console.log(
`Debug: original argv0="${process.argv0}" argv=[${argv}] length=${
argv.length
}`,
);
}
const cmdSlice = argv.slice(2);
let isOclif = false;
// Look for commands that have been deleted, to print a notice
checkDeletedCommand(cmdSlice);
if (cmdSlice.length > 1) {
// convert e.g. 'balena help env add' to 'balena env add --help'
if (cmdSlice[0] === 'help') {
cmdSlice.shift();
cmdSlice.push('--help');
}
// Look for commands that have been transitioned to oclif
isOclif = isOclifCommand(cmdSlice);
if (isOclif) {
// convert space-separated commands to oclif's topic:command syntax
argv = [
argv[0],
argv[1],
cmdSlice[0] + ':' + cmdSlice[1],
...cmdSlice.slice(2),
];
}
}
if (isOclif) {
if (process.env.DEBUG) {
console.log(`Debug: oclif new argv=[${argv}] length=${argv.length}`);
}
require('./app-oclif').run(argv);
} else {
require('./app-capitano');
}
}
/**
*
* @param argvSlice process.argv.slice(2)
*/
function checkDeletedCommand(argvSlice: string[]): void {
if (argvSlice[0] === 'help') {
argvSlice = argvSlice.slice(1);
}
function replaced(
oldCmd: string,
alternative: string,
version: string,
verb = 'replaced',
) {
exitWithExpectedError(stripIndent`
Note: the command "balena ${oldCmd}" was ${verb} in CLI version ${version}.
Please use "balena ${alternative}" instead.
`);
}
function removed(oldCmd: string, alternative: string, version: string) {
let msg = `Note: the command "balena ${oldCmd}" was removed in CLI version ${version}.`;
if (alternative) {
msg = [msg, alternative].join('\n');
}
exitWithExpectedError(msg);
}
const stopAlternative =
'Please use "balena ssh -s" to access the host OS, then use `balena-engine stop`.';
const cmds: { [cmd: string]: [(...args: any) => void, ...string[]] } = {
sync: [replaced, 'push', 'v11.0.0', 'removed'],
'local logs': [replaced, 'logs', 'v11.0.0'],
'local push': [replaced, 'push', 'v11.0.0'],
'local scan': [replaced, 'scan', 'v11.0.0'],
'local ssh': [replaced, 'ssh', 'v11.0.0'],
'local stop': [removed, stopAlternative, 'v11.0.0'],
};
let cmd: string | undefined;
if (argvSlice.length > 1) {
cmd = [argvSlice[0], argvSlice[1]].join(' ');
} else if (argvSlice.length > 0) {
cmd = argvSlice[0];
}
if (cmd && Object.getOwnPropertyNames(cmds).includes(cmd)) {
cmds[cmd][0](cmd, ...cmds[cmd].slice(1));
}
}
/**
* Determine whether the CLI command has been converted from Capitano to ocif.
* @param argvSlice process.argv.slice(2)
*/
function isOclifCommand(argvSlice: string[]): boolean {
// Look for commands that have been transitioned to oclif
if (argvSlice.length > 1) {
// balena env add
if (argvSlice[0] === 'env' && argvSlice[1] === 'add') {
return true;
}
}
return false;
}
/**
* CLI entrypoint, but see also `bin/balena` and `bin/balena-dev` which
* call this function.
*/
export function run(): void {
// globalInit() must be called very early on (before other imports) because
// it sets up Sentry error reporting, global HTTP proxy settings, balena-sdk
// shared options, and performs node version requirement checks.
require('./app-common').globalInit();
routeCliFramework(process.argv);
}

View File

@ -104,7 +104,7 @@ const messages: {
$ balena login`,
};
exports.handle = function(error: any) {
export function handleError(error: any) {
let message = interpret(error);
if (message == null) {
return;
@ -122,4 +122,4 @@ exports.handle = function(error: any) {
// Ignore any errors (from error logging, or timeouts)
})
.finally(() => process.exit(error.exitCode || 1));
};
}

25
lib/global.d.ts vendored Normal file
View File

@ -0,0 +1,25 @@
/**
* @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.
*/
interface Dictionary<T> {
[key: string]: T;
}
declare module '*/package.json' {
export const name: string;
export const version: string;
}

View File

@ -46,9 +46,9 @@ export interface DeviceDeployOptions {
dockerfilePath?: string;
registrySecrets: RegistrySecrets;
nocache: boolean;
live: boolean;
nolive: boolean;
detached: boolean;
service?: string;
services?: string[];
system: boolean;
env: string[];
}
@ -149,10 +149,11 @@ export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
if (!semver.satisfies(version, '>=7.21.4')) {
exitWithExpectedError(versionError);
}
if (opts.live && !semver.satisfies(version, '>=9.7.0')) {
exitWithExpectedError(
new Error('Using livepush requires a supervisor >= v9.7.0'),
if (!opts.nolive && !semver.satisfies(version, '>=9.7.0')) {
globalLogger.logWarn(
`Using livepush requires a balena supervisor version >= 9.7.0. A live session will not be started.`,
);
opts.nolive = true;
}
} catch {
exitWithExpectedError(versionError);
@ -180,7 +181,7 @@ export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
const deviceInfo = await api.getDeviceInformation();
let buildLogs: Dictionary<string> | undefined;
if (opts.live) {
if (!opts.nolive) {
buildLogs = {};
}
const buildTasks = await performBuilds(
@ -216,7 +217,7 @@ export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
// Now that we've set the target state, the device will do it's thing
// so we can either just display the logs, or start a livepush session
// (whilst also display logs)
if (opts.live) {
if (!opts.nolive) {
const livepush = new LivepushManager({
api,
buildContext: opts.source,
@ -228,14 +229,15 @@ export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
deployOpts: opts,
});
const promises = [livepush.init()];
globalLogger.logLivepush('Watching for file changes...');
const promises: Array<Bluebird<void> | Promise<void>> = [livepush.init()];
// Only show logs if we're not detaching
if (!opts.detached) {
console.log();
const logStream = await api.getLogStream();
globalLogger.logInfo('Streaming device logs...');
promises.push(
displayDeviceLogs(logStream, globalLogger, opts.system, opts.service),
displayDeviceLogs(logStream, globalLogger, opts.system, opts.services),
);
} else {
globalLogger.logLivepush(
@ -254,7 +256,12 @@ export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
// Now all we need to do is stream back the logs
const logStream = await api.getLogStream();
globalLogger.logInfo('Streaming device logs...');
await displayDeviceLogs(logStream, globalLogger, opts.system, opts.service);
await displayDeviceLogs(
logStream,
globalLogger,
opts.system,
opts.services,
);
}
}

View File

@ -37,11 +37,11 @@ export function displayDeviceLogs(
logs: Readable,
logger: Logger,
system: boolean,
filterService?: string,
filterServices?: string[],
): Bluebird<void> {
return new Bluebird((resolve, reject) => {
logs.on('data', log => {
displayLogLine(log, logger, system, filterService);
displayLogLine(log, logger, system, filterServices);
});
logs.on('error', reject);
@ -64,11 +64,11 @@ function displayLogLine(
log: string | Buffer,
logger: Logger,
system: boolean,
filterService?: string,
filterServices?: string[],
): void {
try {
const obj: Log = JSON.parse(log.toString());
displayLogObject(obj, logger, system, filterService);
displayLogObject(obj, logger, system, filterServices);
} catch (e) {
logger.logDebug(`Dropping device log due to failed parsing: ${e}`);
}
@ -78,7 +78,7 @@ export function displayLogObject<T extends Log>(
obj: T,
logger: Logger,
system: boolean,
filterService?: string,
filterServices?: string[],
): void {
let toPrint: string;
if (obj.timestamp != null) {
@ -88,8 +88,8 @@ export function displayLogObject<T extends Log>(
}
if (obj.serviceName != null) {
if (filterService) {
if (obj.serviceName !== filterService) {
if (filterServices) {
if (!_.includes(filterServices, obj.serviceName)) {
return;
}
} else if (system) {
@ -99,7 +99,7 @@ export function displayLogObject<T extends Log>(
const colourFn = getServiceColourFn(obj.serviceName);
toPrint += ` ${colourFn(`[${obj.serviceName}]`)}`;
} else if (filterService != null && !system) {
} else if (filterServices != null && !system) {
// We have a system log here but we are filtering based
// on a service, so drop this too
return;

128
lib/utils/device/ssh.ts Normal file
View File

@ -0,0 +1,128 @@
/*
Copyright 2016-2019 Balena
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 { ContainerInfo } from 'dockerode';
export interface DeviceSSHOpts {
address: string;
port?: number;
verbose: boolean;
service?: string;
}
export const deviceContainerEngineBinary = `$(if [ -f /usr/bin/balena ]; then echo "balena"; else echo "docker"; fi)`;
export async function performLocalDeviceSSH(
opts: DeviceSSHOpts,
): Promise<void> {
const childProcess = await import('child_process');
const reduce = await import('lodash/reduce');
const { getSubShellCommand } = await import('../helpers');
const { exitWithExpectedError } = await import('../patterns');
const { stripIndent } = await import('common-tags');
const os = await import('os');
let command = '';
if (opts.service != null) {
// Get the containers which are on-device. Currently we
// are single application, which means we can assume any
// container which fulfills the form of
// $serviceName_$appId_$releaseId is what we want. Once
// we have multi-app, we should show a dialog which
// allows the user to choose the correct container
const Docker = await import('dockerode');
const escapeRegex = await import('lodash/escapeRegExp');
const docker = new Docker({
host: opts.address,
port: 2375,
});
const regex = new RegExp(`\\/?${escapeRegex(opts.service)}_\\d+_\\d+`);
const nameRegex = /\/?([a-zA-Z0-9_]+)_\d+_\d+/;
let allContainers: ContainerInfo[];
try {
allContainers = await docker.listContainers();
} catch (_e) {
exitWithExpectedError(stripIndent`
Could not access docker daemon on device ${opts.address}.
Please ensure the device is in local mode.`);
return;
}
const serviceNames: string[] = [];
const containers = allContainers
.map(container => {
for (const name of container.Names) {
if (regex.test(name)) {
return { id: container.Id, name };
}
const match = name.match(nameRegex);
if (match) {
serviceNames.push(match[1]);
}
}
return;
})
.filter(c => c != null);
if (containers.length === 0) {
exitWithExpectedError(
`Could not find a service on device with name ${opts.service}. ${
serviceNames.length > 0
? `Available services:\n${reduce(
serviceNames,
(str, name) => `${str}\t${name}\n`,
'',
)}`
: ''
}`,
);
}
if (containers.length > 1) {
exitWithExpectedError(stripIndent`
Found more than one container with a service name ${opts.service}.
This state is not supported, please contact support.
`);
}
// Getting a command to work on all platforms is a pain,
// so we just define slightly different ones for windows
if (os.platform() !== 'win32') {
const shellCmd = `/bin/sh -c "if [ -e /bin/bash ]; then exec /bin/bash; else exec /bin/sh; fi"`;
command = `'${deviceContainerEngineBinary}' exec -ti ${
containers[0]!.id
} '${shellCmd}'`;
} else {
const shellCmd = `/bin/sh -c "if [ -e /bin/bash ]; then exec /bin/bash; else exec /bin/sh; fi"`;
command = `${deviceContainerEngineBinary} exec -ti ${
containers[0]!.id
} ${shellCmd}`;
}
}
// Generate the SSH command
const sshCommand = `ssh \
${opts.verbose ? '-vvv' : ''} \
-t \
-p ${opts.port ? opts.port : 22222} \
-o LogLevel=ERROR \
-o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
root@${opts.address} ${command}`;
const subShell = getSubShellCommand(sshCommand);
childProcess.spawn(subShell.program, subShell.args, { stdio: 'inherit' });
}

View File

@ -223,3 +223,50 @@ export function retry<T>(
}
return promise;
}
/**
* Return a compare(a, b) function suitable for use as the argument for the
* sort() method of an array. That function will use the given manuallySortedArray
* as "sorting guidance":
* - If both a and b are found in the manuallySortedArray, the returned
* compare(a, b) function will follow that ordering.
* - If neither a nor b are found in the manuallySortedArray, the returned
* compare(a, b) function will compare a and b using the standard '<' and
* '>' Javascript operators.
* - If only a or only b are found in the manuallySortedArray, the returned
* compare(a, b) function will treat the element that was found as being
* "smaller than" the not-found element (i.e. found elements appear before
* not-found elements in sorted order).
*
* The equalityFunc(a, x) argument is a function used to compare the items
* being sorted against the items in the manuallySortedArray. For example, if
* equalityFunc was (a, x) => a.startsWith(x), where a is an item being sorted
* and x is an item in the manuallySortedArray, then the manuallySortedArray
* could contain prefix substrings to guide the sorting.
*
* @param manuallySortedArray A pre-sorted array to guide the sorting
* @param equalityFunc An optional function used to compare the items being
* sorted against items in manuallySortedArray. It should return true if
* the two items compare equal, otherwise false. The arguments are the
* same as provided by the standard Javascript array.findIndex() method.
*/
export function getManualSortCompareFunction<T, U = T>(
manuallySortedArray: U[],
equalityFunc: (a: T, x: U, index: number, array: U[]) => boolean,
): (a: T, b: T) => number {
return function(a: T, b: T): number {
const indexA = manuallySortedArray.findIndex((x, index, array) =>
equalityFunc(a, x, index, array),
);
const indexB = manuallySortedArray.findIndex((x, index, array) =>
equalityFunc(b, x, index, array),
);
if (indexA >= 0 && indexB >= 0) {
return indexA - indexB;
} else if (indexA < 0 && indexB < 0) {
return a < b ? -1 : a > b ? 1 : 0;
} else {
return indexA < 0 ? 1 : -1;
}
};
}

53
lib/utils/oclif-utils.ts Normal file
View File

@ -0,0 +1,53 @@
/**
* @license
* Copyright 2019 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as Config from '@oclif/config';
export const convertedCommands = {
'env:add': 'env add',
};
/**
* This class is a partial copy-and-paste of
* @oclif/plugin-help/command/CommandHelp, which is used to generate oclif's
* command help output.
*/
export class CommandHelp {
constructor(public command: { args: any[] }) {}
protected arg(arg: Config.Command['args'][0]): string {
const name = arg.name.toUpperCase();
if (arg.required) {
return `${name}`;
}
return `[${name}]`;
}
public defaultUsage(): string {
return CommandHelp.compact([
// this.command.id,
this.command.args
.filter(a => !a.hidden)
.map(a => this.arg(a))
.join(' '),
]).join(' ');
}
public static compact<T>(array: Array<T | undefined>): T[] {
return array.filter((a): a is T => !!a);
}
}

View File

@ -98,7 +98,7 @@ export async function startRemoteBuild(build: RemoteBuild): Promise<void> {
output: process.stdout,
});
rl.on('SIGINT', () => process.emit('SIGINT'));
rl.on('SIGINT', () => process.emit('SIGINT' as any));
}
return new Bluebird((resolve, reject) => {

View File

@ -15,7 +15,7 @@
* limitations under the License.
*/
import * as Bluebird from 'bluebird';
import { spawn } from 'child_process';
import { spawn, StdioOptions } from 'child_process';
import { TypedError } from 'typed-error';
import { getSubShellCommand } from './helpers';
@ -45,7 +45,7 @@ export async function exec(
root@${deviceIp} \
${cmd}`;
const stdio = ['ignore', stdout ? 'pipe' : 'inherit', 'ignore'];
const stdio: StdioOptions = ['ignore', stdout ? 'pipe' : 'inherit', 'ignore'];
const { program, args } = getSubShellCommand(command);
const exitCode = await new Bluebird<number>((resolve, reject) => {

View File

@ -1,4 +1,20 @@
import { spawn } from 'child_process';
/**
* @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, StdioOptions } from 'child_process';
import * as Bluebird from 'bluebird';
import * as rindle from 'rindle';
@ -7,9 +23,14 @@ export async function executeWithPrivileges(
command: string[],
stderr?: NodeJS.WritableStream,
): Promise<string> {
const stdio: StdioOptions = [
'inherit',
'inherit',
stderr ? 'pipe' : 'inherit',
];
const opts = {
stdio: ['inherit', 'inherit', stderr ? 'pipe' : 'inherit'],
env: process.env,
stdio,
};
const args = process.argv

View File

@ -22,6 +22,7 @@ const IP_REGEX = new RegExp(
/\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b/,
);
const DOTLOCAL_REGEX = new RegExp(/^([a-zA-Z0-9-]+\.)+local$/);
const UUID_REGEX = new RegExp(/^[0-9a-f]+$/);
export function validateEmail(input: string) {
if (!validEmail(input)) {
@ -54,3 +55,21 @@ export function validateIPAddress(input: string): boolean {
export function validateDotLocalUrl(input: string): boolean {
return DOTLOCAL_REGEX.test(input);
}
export function validateLongUuid(input: string): boolean {
if (input.length !== 32 && input.length !== 64) {
return false;
}
return UUID_REGEX.test(input);
}
export function validateShortUuid(input: string): boolean {
if (input.length !== 7) {
return false;
}
return UUID_REGEX.test(input);
}
export function validateUuid(input: string): boolean {
return validateLongUuid(input) || validateShortUuid(input);
}

View File

@ -10,7 +10,7 @@
},
"preferGlobal": true,
"files": [
"bin/balena",
"bin/run",
"build/",
"doc/",
"lib/"
@ -31,13 +31,15 @@
]
},
"scripts": {
"prebuild": "rimraf build/ build-bin/ build-zip/ && patch-package",
"build": "npm run build:src && npm run build:bin",
"postinstall": "patch-package",
"prebuild": "rimraf build/ build-bin/",
"build": "npm run build:src",
"build:src": "npm run prettify && npm run lint && npm run build:fast && npm run build:doc",
"build:fast": "gulp build && tsc",
"build:doc": "mkdirp doc/ && ts-node automation/capitanodoc/index.ts > doc/cli.markdown",
"build:bin": "ts-node --type-check -P automation automation/build-bin.ts",
"release": "npm run build && ts-node --type-check -P automation automation/deploy-bin.ts",
"build:doc": "mkdirp doc/ && ts-node --type-check -P automation/tsconfig.json automation/capitanodoc/index.ts > doc/cli.markdown",
"build:standalone": "ts-node --type-check -P automation/tsconfig.json automation/run.ts build:standalone",
"build:installer": "ts-node --type-check -P automation/tsconfig.json automation/run.ts build:installer",
"release": "ts-node --type-check -P automation/tsconfig.json automation/run.ts release",
"pretest": "npm run build",
"test": "gulp test",
"test:fast": "npm run build:fast && gulp test",
@ -45,7 +47,6 @@
"watch": "gulp watch",
"prettify": "prettier --write \"{lib,tests,automation,typings}/**/*.ts\" --config ./node_modules/resin-lint/config/.prettierrc",
"lint": "resin-lint lib/ tests/ && resin-lint --typescript automation/ lib/ typings/ tests/",
"prepublish": "require-npm4-to-publish",
"prepublishOnly": "npm run build"
},
"keywords": [
@ -60,33 +61,44 @@
"author": "Juan Cruz Viotti <juan@balena.io>",
"license": "Apache-2.0",
"engines": {
"node": ">=6.0"
"node": ">=8.0"
},
"oclif": {
"bin": "balena",
"commands": "./build/actions-oclif",
"macos": {
"identifier": "io.balena.cli"
}
},
"devDependencies": {
"@oclif/config": "^1.12.12",
"@oclif/dev-cli": "1.22.0",
"@oclif/parser": "^3.7.3",
"@types/archiver": "2.1.2",
"@types/bluebird": "3.5.21",
"@types/chokidar": "^1.7.5",
"@types/common-tags": "1.4.0",
"@types/dockerode": "2.5.5",
"@types/es6-promise": "0.0.32",
"@types/fs-extra": "5.0.4",
"@types/fs-extra": "7.0.0",
"@types/is-root": "1.0.0",
"@types/lodash": "4.14.112",
"@types/mixpanel": "2.14.0",
"@types/mkdirp": "0.5.2",
"@types/mz": "0.0.32",
"@types/net-keepalive": "^0.4.0",
"@types/node": "6.14.2",
"@types/node": "10.14.5",
"@types/prettyjson": "0.0.28",
"@types/raven": "2.5.1",
"@types/request": "2.48.1",
"@types/rimraf": "^2.0.2",
"@types/shell-escape": "^0.2.0",
"@types/stream-to-promise": "2.2.0",
"@types/tar-stream": "1.6.0",
"@types/through2": "2.0.33",
"catch-uncommitted": "^1.3.0",
"ent": "^2.2.0",
"filehound": "^1.17.0",
"fs-extra": "^5.0.0",
"fs-extra": "^8.0.1",
"gulp": "^4.0.1",
"gulp-coffee": "^2.2.0",
"gulp-inline-source": "^2.1.0",
@ -96,14 +108,16 @@
"patch-package": "^6.1.2",
"pkg": "~4.3.8",
"prettier": "^1.17.0",
"publish-release": "^1.3.3",
"require-npm4-to-publish": "^1.0.0",
"publish-release": "^1.6.0",
"resin-lint": "^3.0.1",
"rewire": "^3.0.2",
"ts-node": "^4.0.1",
"typescript": "2.8.1"
"shell-escape": "^0.2.0",
"ts-node": "^8.1.0",
"typescript": "3.4.3"
},
"dependencies": {
"@oclif/command": "^1.5.12",
"@oclif/errors": "^1.2.2",
"@resin.io/valid-email": "^0.1.0",
"@zeit/dockerignore": "0.0.3",
"JSONStream": "^1.0.3",
@ -128,7 +142,7 @@
"color-hash": "^1.0.3",
"columnify": "^1.5.2",
"common-tags": "^1.7.2",
"denymount": "~2.2.0",
"denymount": "^2.3.0",
"docker-progress": "^4.0.0",
"docker-qemu-transpose": "^0.5.3",
"docker-toolbelt": "^3.3.7",
@ -154,8 +168,9 @@
"mkdirp": "^0.5.1",
"moment": "^2.24.0",
"moment-duration-format": "~2.2.2",
"mz": "^2.6.0",
"mz": "^2.7.0",
"node-cleanup": "^2.1.2",
"oclif": "^1.13.1",
"opn": "^5.5.0",
"prettyjson": "^1.1.3",
"progress-stream": "^2.0.0",
@ -189,7 +204,6 @@
},
"optionalDependencies": {
"net-keepalive": "^1.2.1",
"removedrive": "^1.0.0",
"windosu": "^0.2.0"
}
}

View File

@ -0,0 +1,224 @@
diff --git a/node_modules/@oclif/dev-cli/lib/commands/pack/win.js b/node_modules/@oclif/dev-cli/lib/commands/pack/win.js
index a9d4276..75c2f8b 100644
--- a/node_modules/@oclif/dev-cli/lib/commands/pack/win.js
+++ b/node_modules/@oclif/dev-cli/lib/commands/pack/win.js
@@ -3,11 +3,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
const command_1 = require("@oclif/command");
const qq = require("qqjs");
const Tarballs = require("../../tarballs");
+const { fixPath } = require("../../util");
+
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 !== undefined ? flags.targets.split(',') : undefined;
+ const buildConfig = await Tarballs.buildConfig(flags.root, {targets});
const { config } = buildConfig;
await Tarballs.build(buildConfig, { platform: 'win32', pack: false });
const arches = buildConfig.targets.filter(t => t.platform === 'win32').map(t => t.arch);
@@ -17,7 +20,7 @@ class PackWin extends command_1.Command {
await qq.write([installerBase, `bin/${config.bin}`], scripts.sh(config));
await qq.write([installerBase, `${config.bin}.nsi`], scripts.nsis(config, arch));
await qq.mv(buildConfig.workspace({ platform: 'win32', arch }), [installerBase, 'client']);
- await qq.x(`makensis ${installerBase}/${config.bin}.nsi | grep -v "\\[compress\\]" | grep -v "^File: Descending to"`);
+ await qq.x(`makensis ${fixPath(installerBase)}/${config.bin}.nsi | grep -v "\\[compress\\]" | grep -v "^File: Descending to"`)
const o = buildConfig.dist(`win/${config.bin}-v${buildConfig.version}-${arch}.exe`);
await qq.mv([installerBase, 'installer.exe'], o);
this.log(`built ${o}`);
@@ -40,6 +43,7 @@ class PackWin extends command_1.Command {
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)'}),
};
exports.default = PackWin;
const scripts = {
diff --git a/node_modules/@oclif/dev-cli/lib/tarballs/build.js b/node_modules/@oclif/dev-cli/lib/tarballs/build.js
index 3e613e0..621d52b 100644
--- a/node_modules/@oclif/dev-cli/lib/tarballs/build.js
+++ b/node_modules/@oclif/dev-cli/lib/tarballs/build.js
@@ -19,6 +19,9 @@ const pack = async (from, to) => {
async function build(c, options = {}) {
const { xz, config } = c;
const prevCwd = qq.cwd();
+
+ console.log(`[patched @oclif/dev-cli] 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());
@@ -34,6 +37,30 @@ async function build(c, options = {}) {
await qq.mv(f, '.');
await qq.rm('package', tarball, 'bin/run.cmd');
};
+ const copyCLI = async() => {
+ const ws = c.workspace();
+ await qq.emptyDir(ws);
+ qq.cd(ws);
+ const sources = [
+ 'bin', 'build', 'patches', 'typings', 'CHANGELOG.md', 'INSTALL.md',
+ 'LICENSE', 'package.json', 'package-lock.json', 'README.md',
+ 'TROUBLESHOOTING.md',
+ ];
+ for (const source of sources) {
+ console.log(`cp "${source}" -> "${ws}"`);
+ try {
+ await qq.cp(path.join(c.root, source), ws);
+ } catch (err) {
+ // OK if package-lock.json doesn't exist
+ if (source !== 'package-lock.json') {
+ throw err;
+ }
+ }
+ }
+ // rename the original balena-cli ./bin/balena entry point for oclif compatibility
+ await qq.mv('bin/balena', 'bin/run');
+ await qq.rm('bin/run.cmd');
+ }
const updatePJSON = async () => {
qq.cd(c.workspace());
const pjson = await qq.readJSON('package.json');
@@ -55,7 +82,11 @@ async function build(c, options = {}) {
if (!await qq.exists(lockpath)) {
lockpath = qq.join(c.root, 'npm-shrinkwrap.json');
}
- await qq.cp(lockpath, '.');
+ try {
+ await qq.cp(lockpath, '.');
+ } catch (err) {
+ console.log('WARNING: found neiter package-lock.json nor npm-shrinkwrap.json')
+ }
await qq.x('npm install --production');
}
};
@@ -124,7 +155,8 @@ async function build(c, options = {}) {
await qq.writeJSON(c.dist(config.s3Key('manifest')), manifest);
};
log_1.log(`gathering workspace for ${config.bin} to ${c.workspace()}`);
- await extractCLI(await packCLI());
+ // await extractCLI(await packCLI());
+ await copyCLI();
await updatePJSON();
await addDependencies();
await bin_1.writeBinScripts({ config, baseWorkspace: c.workspace(), nodeVersion: c.nodeVersion });
diff --git a/node_modules/@oclif/dev-cli/lib/tarballs/node.js b/node_modules/@oclif/dev-cli/lib/tarballs/node.js
index 343eb00..865d5a5 100644
--- a/node_modules/@oclif/dev-cli/lib/tarballs/node.js
+++ b/node_modules/@oclif/dev-cli/lib/tarballs/node.js
@@ -1,19 +1,45 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const errors_1 = require("@oclif/errors");
+const { isMSYS2 } = require('qqjs');
const path = require("path");
const qq = require("qqjs");
const log_1 = require("../log");
-async function checkFor7Zip() {
- try {
- await qq.x('7z', { stdio: [0, null, 2] });
+const { fixPath } = require("../util");
+let try_install_7zip = true;
+async function checkFor7Zip(projectRootPath) {
+ let zPaths = [
+ fixPath(path.join(projectRootPath, 'node_modules', '7zip', '7zip-lite', '7z.exe')),
+ '7z',
+ ];
+ let foundPath = '';
+ for (const zPath of zPaths) {
+ try {
+ console.log(`probing 7zip at "${zPath}"...`);
+ await qq.x(zPath, { stdio: [0, null, 2] });
+ foundPath = zPath;
+ break;
+ }
+ catch (err) {}
}
- catch (err) {
- if (err.code === 127)
- errors_1.error('install 7-zip to package windows tarball');
- else
- throw err;
+ if (foundPath) {
+ console.log(`found 7zip at "${foundPath}"`);
+ } else if (try_install_7zip) {
+ try_install_7zip = false;
+ console.log(`attempting "npm install 7zip"...`);
+ qq.pushd(projectRootPath);
+ try {
+ await qq.x('npm', ['install', '--no-save', '7zip']);
+ } catch (err) {
+ errors_1.error('install 7-zip to package windows tarball', true);
+ } finally {
+ qq.popd();
+ }
+ return checkFor7Zip(projectRootPath);
+ } else {
+ errors_1.error('install 7-zip to package windows tarball', true);
}
+ return foundPath;
}
async function fetchNodeBinary({ nodeVersion, output, platform, arch, tmp }) {
if (arch === 'arm')
@@ -21,8 +47,9 @@ async function fetchNodeBinary({ nodeVersion, output, platform, arch, tmp }) {
let nodeBase = `node-v${nodeVersion}-${platform}-${arch}`;
let tarball = path.join(tmp, 'node', `${nodeBase}.tar.xz`);
let url = `https://nodejs.org/dist/v${nodeVersion}/${nodeBase}.tar.xz`;
- if (platform === 'win32') {
- await checkFor7Zip();
+ let zPath = '';
+ if (platform === 'win32') {
+ zPath = await checkFor7Zip(path.join(tmp, '..'));
nodeBase = `node-v${nodeVersion}-win-${arch}`;
tarball = path.join(tmp, 'node', `${nodeBase}.7z`);
url = `https://nodejs.org/dist/v${nodeVersion}/${nodeBase}.7z`;
@@ -40,7 +67,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 qq.x(`grep ${path.basename(tarball)} ${fixPath(shasums)} | ${shaCmd}`, { cwd: basedir });
};
const extract = async () => {
log_1.log(`extracting ${nodeBase}`);
@@ -50,7 +78,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 qq.x(`"${zPath}" x -bd -y ${fixPath(tarball)} > /dev/null`);
await qq.mv([nodeBase, 'node.exe'], cache);
qq.popd();
}
diff --git a/node_modules/@oclif/dev-cli/lib/util.js b/node_modules/@oclif/dev-cli/lib/util.js
index 17368b4..9d3fcf9 100644
--- a/node_modules/@oclif/dev-cli/lib/util.js
+++ b/node_modules/@oclif/dev-cli/lib/util.js
@@ -1,6 +1,7 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const _ = require("lodash");
+const { isCygwin, isMinGW, isMSYS2 } = require('qqjs');
function castArray(input) {
if (input === undefined)
return [];
@@ -40,3 +41,17 @@ function sortBy(arr, fn) {
}
exports.sortBy = sortBy;
exports.template = (context) => (t) => _.template(t || '')(context);
+
+function fixPath(badPath) {
+ console.log(`fixPath MSYSTEM=${process.env.MSYSTEM} OSTYPE=${process.env.OSTYPE} isMSYS2=${isMSYS2} isMingGW=${isMinGW} isCygwin=${isCygwin}`);
+ // 'c:\myfolder' -> '/c/myfolder' or '/cygdrive/c/myfolder'
+ let fixed = badPath.replace(/\\/g, '/');
+ if (isMSYS2 || isMinGW) {
+ fixed = fixed.replace(/^([a-zA-Z]):/, '/$1');
+ } else if (isCygwin) {
+ fixed = fixed.replace(/^([a-zA-Z]):/, '/cygdrive/$1');
+ }
+ console.log(`[patched @oclif/dev-cli] fixPath before="${badPath}" after="${fixed}"`);
+ return fixed;
+}
+exports.fixPath = fixPath;

View File

@ -1,38 +0,0 @@
diff --git a/node_modules/denymount/lib/index.js b/node_modules/denymount/lib/index.js
index 93b8e59..86d53dc 100644
--- a/node_modules/denymount/lib/index.js
+++ b/node_modules/denymount/lib/index.js
@@ -24,7 +24,9 @@ var utils = require('./utils');
* @module denymount
*/
-var EXECUTABLE_PATH = path.join(__dirname, '..', 'bin', 'denymount');
+var EXECUTABLE_PATH = process.pkg
+ ? path.join(path.dirname(process.execPath), 'denymount')
+ : path.join(__dirname, '..', 'bin', 'denymount');
/**
* @summary Prevent automatic mounting of an OS X disk
diff --git a/node_modules/denymount/lib/index.js.rej b/node_modules/denymount/lib/index.js.rej
new file mode 100644
index 0000000..a2c0516
--- /dev/null
+++ b/node_modules/denymount/lib/index.js.rej
@@ -0,0 +1,17 @@
+***************
+*** 24,30 ****
+ * @module denymount
+ */
+
+- var EXECUTABLE_PATH = path.join(__dirname, '..', 'bin', 'denymount');
+
+ /**
+ * @summary Prevent automatic mounting of an OS X disk
+--- 24,30 ----
+ * @module denymount
+ */
+
++ var EXECUTABLE_PATH = path.join(path.dirname(process.execPath), 'denymount', 'bin', 'denymount');
+
+ /**
+ * @summary Prevent automatic mounting of an OS X disk

View File

@ -0,0 +1,53 @@
diff --git a/node_modules/qqjs/node_modules/execa/index.js b/node_modules/qqjs/node_modules/execa/index.js
index 06f3969..7ab1b66 100644
--- a/node_modules/qqjs/node_modules/execa/index.js
+++ b/node_modules/qqjs/node_modules/execa/index.js
@@ -14,6 +14,17 @@ const stdio = require('./lib/stdio');
const TEN_MEGABYTES = 1000 * 1000 * 10;
+// 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');
+
+console.log(`[patched execa] detected "${
+ isCygwin ? 'Cygwin' : isMinGW ? 'MinGW' : isMSYS2 ? 'MSYS2' : 'standard'
+}" environment (MSYSTEM="${process.env.MSYSTEM}")`);
+
function handleArgs(cmd, args, opts) {
let parsed;
@@ -104,13 +115,21 @@ function handleShell(fn, cmd, opts) {
opts = Object.assign({}, opts);
- if (process.platform === 'win32') {
+ if (isMSYS2 || isMinGW || isCygwin) {
+ file = 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' : file)));
+ }
+ else if (process.platform === 'win32') {
opts.__winShell = true;
file = process.env.comspec || 'cmd.exe';
args = ['/s', '/c', `"${cmd}"`];
opts.windowsVerbatimArguments = true;
}
+ console.log(`[patched execa] handleShell file="${file}" args="[${args}]"`);
+
if (opts.shell) {
file = opts.shell;
delete opts.shell;
@@ -364,3 +383,7 @@ module.exports.sync = (cmd, args, opts) => {
module.exports.shellSync = (cmd, opts) => handleShell(module.exports.sync, cmd, opts);
module.exports.spawn = util.deprecate(module.exports, 'execa.spawn() is deprecated. Use execa() instead.');
+
+module.exports.isCygwin = isCygwin;
+module.exports.isMinGW = isMinGW;
+module.exports.isMSYS2 = isMSYS2;

16
patches/qqjs+0.3.10.patch Normal file
View File

@ -0,0 +1,16 @@
diff --git a/node_modules/qqjs/lib/exec.js b/node_modules/qqjs/lib/exec.js
index 835f565..84bb5be 100644
--- a/node_modules/qqjs/lib/exec.js
+++ b/node_modules/qqjs/lib/exec.js
@@ -5,6 +5,11 @@ const m = {
m: {},
get execa() { return this.m.execa = this.m.execa || require('execa'); },
};
+const { isCygwin, isMinGW, isMSYS2 } = require('execa');
+exports.isCygwin = isCygwin;
+exports.isMinGW = isMinGW;
+exports.isMSYS2 = isMSYS2;
+console.log(`qqjs exec.js MSYSTEM=${process.env.MSYSTEM} OSTYPE=${process.env.OSTYPE} isMSYS2=${isMSYS2} isMingGW=${isMinGW} isCygwin=${isCygwin}`);
/**
* easy access to process.env
*/

View File

@ -1,7 +1,7 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"target": "es2017",
"outDir": "build",
"strict": true,
"strictPropertyInitialization": false,
@ -11,20 +11,13 @@
"removeComments": true,
"sourceMap": true,
"skipLibCheck": true,
"lib": [
// es5 defaults:
"dom",
"es5",
"scripthost",
// some specific es6 bits we're sure are safe:
"es2015.collection",
"es2015.iterable",
"es2016.array.include"
"typeRoots" : [
"./node_modules/@types",
"./node_modules/etcher-sdk/typings",
"./typings"
]
},
"include": [
"./typings/*.d.ts",
"./node_modules/etcher-sdk/typings/**/*.d.ts",
"./lib/**/*.ts"
]
}

View File

@ -1 +0,0 @@
declare module '@resin.io/valid-email';

View File

@ -14,6 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
declare module 'balena-device-init' {
import { DeviceType } from 'balena-sdk';
import * as Promise from 'bluebird';

View File

@ -1,5 +0,0 @@
declare module 'balena-sync' {
import { CommandDefinition } from 'capitano';
export function capitano(tool: 'balena-cli'): CommandDefinition;
}

22
typings/balena-sync/index.d.ts vendored Normal file
View File

@ -0,0 +1,22 @@
/**
* @license
* Copyright 2019 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
declare module 'balena-sync' {
import { CommandDefinition } from 'capitano';
export function capitano(tool: 'balena-cli'): CommandDefinition;
}

1
typings/bash/index.d.ts vendored Normal file
View File

@ -0,0 +1 @@
declare module 'bash';

View File

@ -1,3 +1,20 @@
/**
* @license
* Copyright 2019 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
declare module 'capitano' {
export function parse(argv: string[]): Cli;
@ -19,11 +36,11 @@ declare module 'capitano' {
signature: string;
description: string;
help: string;
options?: OptionDefinition[];
options?: Partial<OptionDefinition[]>;
permission?: 'user';
root?: boolean;
primary?: boolean;
action(params: P, options: O, done: () => void): void;
action(params: P, options: Partial<O>, done: () => void): void;
}
export interface Command {

View File

@ -14,6 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
declare module 'color-hash' {
interface Hasher {
hex(text: string): string;

View File

@ -1,13 +0,0 @@
declare module 'dockerfile-template' {
/**
* Variables which define what will be replaced, and what they will be replaced with.
*/
export interface TemplateVariables {
[key: string]: string;
}
export function process(
content: string,
variables: TemplateVariables,
): string;
}

30
typings/dockerfile-template/index.d.ts vendored Normal file
View File

@ -0,0 +1,30 @@
/**
* @license
* Copyright 2019 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
declare module 'dockerfile-template' {
/**
* Variables which define what will be replaced, and what they will be replaced with.
*/
export interface TemplateVariables {
[key: string]: string;
}
export function process(
content: string,
variables: TemplateVariables,
): string;
}

1
typings/ent.d.ts vendored
View File

@ -1 +0,0 @@
declare module 'ent';

18
typings/ent/index.d.ts vendored Normal file
View File

@ -0,0 +1,18 @@
/**
* @license
* Copyright 2019 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
declare module 'ent';

28
typings/filehound/index.d.ts vendored Normal file
View File

@ -0,0 +1,28 @@
/**
* @license
* Copyright 2019 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
declare module '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<string[]>;
}
}

3
typings/global.d.ts vendored
View File

@ -1,3 +0,0 @@
interface Dictionary<T> {
[key: string]: T;
}

View File

@ -14,6 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
declare module 'inquirer-dynamic-list' {
interface Choice {
name: string;

View File

@ -14,6 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
declare module 'nplugm' {
import Promise = require('bluebird');
export function list(regexp: RegExp): Promise<string[]>;

View File

@ -1,4 +0,0 @@
declare module '*/package.json' {
export const name: string;
export const version: string;
}

20
typings/pkg/index.d.ts vendored Normal file
View File

@ -0,0 +1,20 @@
/**
* @license
* Copyright 2019 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
declare module 'pkg' {
export function exec(args: string[]): Promise<void>;
}

View File

@ -1,6 +0,0 @@
declare module 'president' {
export function execute(
command: string[],
callback: (err: Error) => void,
): void;
}

23
typings/president/index.d.ts vendored Normal file
View File

@ -0,0 +1,23 @@
/**
* @license
* Copyright 2019 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
declare module 'president' {
export function execute(
command: string[],
callback: (err: Error) => void,
): void;
}

39
typings/publish-release/index.d.ts vendored Normal file
View File

@ -0,0 +1,39 @@
/**
* @license
* Copyright 2019 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
declare module '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;
}

View File

@ -1 +0,0 @@
declare module 'resin-cli-form';

18
typings/resin-cli-form/index.d.ts vendored Normal file
View File

@ -0,0 +1,18 @@
/**
* @license
* Copyright 2019 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
declare module 'resin-cli-form';

View File

@ -1 +0,0 @@
declare module 'resin-cli-visuals';

18
typings/resin-cli-visuals/index.d.ts vendored Normal file
View File

@ -0,0 +1,18 @@
/**
* @license
* Copyright 2019 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
declare module 'resin-cli-visuals';

View File

@ -1,5 +0,0 @@
declare module 'resin-image-fs' {
import Promise = require('bluebird');
export function readFile(options: {}): Promise<string>;
}

22
typings/resin-image-fs/index.d.ts vendored Normal file
View File

@ -0,0 +1,22 @@
/**
* @license
* Copyright 2019 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
declare module 'resin-image-fs' {
import Promise = require('bluebird');
export function readFile(options: {}): Promise<string>;
}

18
typings/resin.io/index.d.ts vendored Normal file
View File

@ -0,0 +1,18 @@
/**
* @license
* Copyright 2019 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
declare module '@resin.io/valid-email';

View File

@ -14,6 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
declare module 'rindle' {
export function extract(
stream: NodeJS.ReadableStream,