mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-01-29 15:44:26 +00:00
Merge pull request #1285 from balena-io/release-v11
Release balena-cli v11.0.0
This commit is contained in:
commit
abc2cfd14c
1
.gitignore
vendored
1
.gitignore
vendored
@ -40,6 +40,7 @@ balenarc.yml
|
||||
build/
|
||||
build-bin/
|
||||
build-zip/
|
||||
dist/
|
||||
|
||||
# Ignore fast-boot cache file
|
||||
**/.fast-boot.json
|
||||
|
16
.travis.yml
16
.travis.yml
@ -3,15 +3,21 @@ os:
|
||||
- linux
|
||||
- osx
|
||||
node_js:
|
||||
- "6"
|
||||
before_install:
|
||||
- npm -g install npm@4
|
||||
script: npm run ci
|
||||
- "10"
|
||||
script:
|
||||
- node --version
|
||||
- npm --version
|
||||
- npm run ci
|
||||
notifications:
|
||||
email: false
|
||||
deploy:
|
||||
- provider: script
|
||||
script: npm run release
|
||||
script:
|
||||
- node --version
|
||||
- npm --version
|
||||
- npm run build:standalone
|
||||
- npm run build:installer
|
||||
- npm run release
|
||||
skip_cleanup: true
|
||||
on:
|
||||
tags: true
|
||||
|
65
CONTRIBUTING.md
Normal file
65
CONTRIBUTING.md
Normal 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).
|
70
INSTALL.md
70
INSTALL.md
@ -1,30 +1,70 @@
|
||||
# balena CLI Installation Instructions
|
||||
|
||||
The easiest and recommended way of installing the CLI on all platforms (Windows, Linux, macOS) is
|
||||
to use the [Standalone Installation](#standalone-installation) described below. Some specific CLI
|
||||
commands have a few extra installation steps: see section [Additional Dependencies](#additional-dependencies).
|
||||
There are 3 options to choose from to install balena's CLI:
|
||||
|
||||
* [Executable Installer](#executable-installer): the easiest method, using the traditional
|
||||
graphical desktop application installers for Windows and macOS (coming soon for Linux users too).
|
||||
* [Standalone Zip Package](#standalone-zip-package): these are plain zip files with the balena CLI
|
||||
executable in them. Recommended for scripted installation in CI (continuous integration)
|
||||
environments.
|
||||
* [NPM Installation](#npm-installation): recommended for developers who may be interested in
|
||||
integrating the balena CLI in their existing Node.js projects or workflow.
|
||||
|
||||
Some specific CLI commands have a few extra installation steps: see section [Additional
|
||||
Dependencies](#additional-dependencies).
|
||||
|
||||
> **Windows users:** We now have a [YouTube video tutorial](https://www.youtube.com/watch?v=2LApclXFqsg)
|
||||
for installing and getting started with the balena CLI on Windows!
|
||||
|
||||
## Standalone Installation
|
||||
## Executable Installer
|
||||
|
||||
1. Download the latest zip file for your OS from https://github.com/balena-io/balena-cli/releases.
|
||||
(Note that "[Darwin](https://en.wikipedia.org/wiki/Darwin_(operating_system))" is the
|
||||
appropriate zip file for macOS.)
|
||||
2. Extract the zip file contents to any folder you choose. The extracted contents will include a
|
||||
`balena-cli` folder.
|
||||
3. Add the `balena-cli` folder to the system's `PATH` environment variable. See instructions for:
|
||||
[Windows](https://www.computerhope.com/issues/ch000549.htm) |
|
||||
[Linux](https://stackoverflow.com/questions/14637979/how-to-permanently-set-path-on-linux-unix) |
|
||||
[macOS](https://www.architectryan.com/2012/10/02/add-to-the-path-on-mac-os-x-mountain-lion/#.Uydjga1dXDg)
|
||||
_Please note: the executable installers are in **beta** status (recently introduced)._
|
||||
|
||||
Check that the installation was successful by opening or re-opening a command terminal window
|
||||
(so that the PATH environment variable changes take effect), and running these commands:
|
||||
1. Download the latest installer from the [releases page](https://github.com/balena-io/balena-cli/releases).
|
||||
Look for a file name that ends with "installer-BETA", for example:
|
||||
`balena-cli-v10.13.6-windows-x64-installer-BETA.exe`
|
||||
`balena-cli-v10.13.6-macOS-x64-installer-BETA.pkg`
|
||||
2. Double click to run. Your system may raise a pop-up warning that the installer is from an
|
||||
"unknown publisher" or "unidentified developer". Check the following instructions for how
|
||||
to get through the warnings:
|
||||
[Windows](https://github.com/balena-io/balena-cli/issues/1250) or
|
||||
[macOS](https://github.com/balena-io/balena-cli/issues/1251).
|
||||
(We are looking at how to get the installers digitally signed to avoid the warnings.)
|
||||
|
||||
After the installation completes, close and re-open any open command terminal windows so that the
|
||||
changes made by the installer to the PATH environment variable can take effect. Check that the
|
||||
installation was successful by running these commands:
|
||||
|
||||
* `balena` - should print the balena CLI help
|
||||
* `balena version` - should print the installed CLI version
|
||||
|
||||
> Note: If you had previously installed the CLI using a standalone zip package, it may be a good
|
||||
> idea to check your system's `PATH` environment variable for duplicate entries, as the terminal
|
||||
> will use the entry that comes first. Check the [Standalone Zip Package](#standalone-zip-package)
|
||||
> instructions for how to modify the PATH variable.
|
||||
|
||||
By default, the CLI is installed to the following folders:
|
||||
|
||||
OS | Folders
|
||||
--- | ---
|
||||
Windows: | `C:\Program Files\balena-cli\`
|
||||
macOS: | `/usr/local/lib/balena-cli/` <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.
|
||||
|
42
README.md
42
README.md
@ -29,9 +29,10 @@ are supported. We are aware of users also having a good experience with alternat
|
||||
including:
|
||||
|
||||
* Microsoft's [Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/about)
|
||||
(a.k.a. Microsoft's "bash for Windows 10").
|
||||
* [Git for Windows](https://git-for-windows.github.io/).
|
||||
* [MinGW](http://www.mingw.org): install the `msys-rsync` and `msys-openssh` packages too.
|
||||
(a.k.a. Microsoft's "bash for Windows 10")
|
||||
* [Git for Windows](https://git-for-windows.github.io/)
|
||||
* [MSYS](http://www.mingw.org/wiki/MSYS) and [MSYS2](https://www.msys2.org/) (install the
|
||||
`msys-rsync` and `msys-openssh` packages too)
|
||||
|
||||
On **macOS** and **Linux,** the standard terminal window is supported. _Optionally,_ `bash` command
|
||||
auto completion may be enabled by copying the
|
||||
@ -52,14 +53,14 @@ $ balena login
|
||||
|
||||
HTTP(S) proxies can be configured through any of the following methods, in order of preference:
|
||||
|
||||
* Set the \`BALENARC_PROXY\` environment variable in URL format (with protocol, host, port, and
|
||||
* Set the `BALENARC_PROXY` environment variable in URL format (with protocol, host, port, and
|
||||
optionally basic auth).
|
||||
* Alternatively, use the [balena config file](https://www.npmjs.com/package/balena-settings-client#documentation)
|
||||
(project-specific or user-level) and set the \`proxy\` setting. It can be:
|
||||
(project-specific or user-level) and set the `proxy` setting. It can be:
|
||||
* A string in URL format, or
|
||||
* An object in the [global-tunnel-ng options format](https://www.npmjs.com/package/global-tunnel-ng#options) (which allows more control).
|
||||
* Alternatively, set the conventional \`https_proxy\` / \`HTTPS_PROXY\` / \`http_proxy\` / \`HTTP_PROXY\`
|
||||
environment variable (in the same standard URL format).
|
||||
* Alternatively, set the conventional `https_proxy` / `HTTPS_PROXY` / `http_proxy` / `HTTP_PROXY`
|
||||
environment variable (in the same standard URL format).
|
||||
|
||||
To get a proxy to work with the `balena ssh` command, check the
|
||||
[installation instructions](https://github.com/balena-io/balena-cli/blob/master/INSTALL.md).
|
||||
@ -78,30 +79,11 @@ If you come across any problems or would like to get in touch:
|
||||
* For bug reports or feature requests,
|
||||
[have a look at the GitHub issues or create a new one](https://github.com/balena-io/balena-cli/issues/).
|
||||
|
||||
## Contributing
|
||||
## Contributing (including editing documentation files)
|
||||
|
||||
The balena CLI is an open source project and your contribution is welcome!
|
||||
|
||||
The CLI was originally written in [CoffeeScript](https://coffeescript.org), but we have decided to
|
||||
migrate to [TypeScript](https://www.typescriptlang.org/) in order to take advantage of static
|
||||
typing and formal programming interfaces. The migration is taking place gradually, as part of
|
||||
maintenance work or the implementation of new features.
|
||||
|
||||
After cloning this repository and running `npm install` you can build the CLI using `npm run build`.
|
||||
You can then run the generated build using `./bin/balena`.
|
||||
In order to ease development:
|
||||
|
||||
* you can build the CLI using the `npm run build:fast` variant which skips some of the build steps or
|
||||
* you can use `./bin/balena-dev` which live transpiles the sources of the CLI.
|
||||
|
||||
In either case, before opening a PR be sure to also test your changes with `npm test`.
|
||||
|
||||
## Note on editing this README document
|
||||
|
||||
This file is edited/created by hand, but it is then automatically parsed to extract selected
|
||||
subsections for the [CLI's web documentation page](https://www.balena.io/docs/reference/cli/).
|
||||
The code that parses this file is in [`automation/capitanodoc/capitanodoc.ts`
|
||||
](https://github.com/balena-io/balena-cli/blob/master/automation/capitanodoc/capitanodoc.ts).
|
||||
Please have a look at the [CONTRIBUTING.md](./CONTRIBUTING.md) file for some guidance before
|
||||
submitting a pull request or updating documentation (because some files are automatically
|
||||
generated). Thank you for your help and interest!
|
||||
|
||||
## License
|
||||
|
||||
|
12
appveyor.yml
12
appveyor.yml
@ -1,6 +1,8 @@
|
||||
# appveyor file
|
||||
# http://www.appveyor.com/docs/appveyor-yml
|
||||
|
||||
image: Visual Studio 2017
|
||||
|
||||
init:
|
||||
- git config --global core.autocrlf input
|
||||
|
||||
@ -14,12 +16,12 @@ matrix:
|
||||
# what combinations to test
|
||||
environment:
|
||||
matrix:
|
||||
- nodejs_version: 6
|
||||
- nodejs_version: 10
|
||||
|
||||
install:
|
||||
- ps: Install-Product node $env:nodejs_version x64
|
||||
- npm install -g npm@4
|
||||
- set PATH=%APPDATA%\npm;%PATH%
|
||||
- npm config set python 'C:\Python27\python.exe'
|
||||
- npm install
|
||||
|
||||
build: off
|
||||
@ -27,8 +29,12 @@ build: off
|
||||
test_script:
|
||||
- node --version
|
||||
- npm --version
|
||||
- cmd: npm test
|
||||
- npm test
|
||||
|
||||
deploy_script:
|
||||
- node --version
|
||||
- npm --version
|
||||
- npm run build:standalone
|
||||
- npm run build:installer
|
||||
- IF "%APPVEYOR_REPO_TAG%" == "true" (npm run release)
|
||||
- IF NOT "%APPVEYOR_REPO_TAG%" == "true" (echo 'Not tagged, skipping deploy')
|
||||
|
128
automation/build-bin.ts
Executable file → Normal file
128
automation/build-bin.ts
Executable file → Normal 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');
|
||||
}
|
||||
|
@ -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'],
|
||||
|
25
automation/capitanodoc/doc-types.d.ts
vendored
25
automation/capitanodoc/doc-types.d.ts
vendored
@ -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 };
|
||||
|
@ -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.
|
||||
|
@ -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');
|
||||
}
|
||||
|
@ -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) {}
|
||||
|
||||
|
38
automation/custom-types.d.ts
vendored
38
automation/custom-types.d.ts
vendored
@ -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;
|
||||
}
|
@ -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
112
automation/run.ts
Normal 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();
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
469
doc/cli.markdown
469
doc/cli.markdown
@ -21,9 +21,10 @@ are supported. We are aware of users also having a good experience with alternat
|
||||
including:
|
||||
|
||||
* Microsoft's [Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/about)
|
||||
(a.k.a. Microsoft's "bash for Windows 10").
|
||||
* [Git for Windows](https://git-for-windows.github.io/).
|
||||
* [MinGW](http://www.mingw.org): install the `msys-rsync` and `msys-openssh` packages too.
|
||||
(a.k.a. Microsoft's "bash for Windows 10")
|
||||
* [Git for Windows](https://git-for-windows.github.io/)
|
||||
* [MSYS](http://www.mingw.org/wiki/MSYS) and [MSYS2](https://www.msys2.org/) (install the
|
||||
`msys-rsync` and `msys-openssh` packages too)
|
||||
|
||||
On **macOS** and **Linux,** the standard terminal window is supported. _Optionally,_ `bash` command
|
||||
auto completion may be enabled by copying the
|
||||
@ -44,14 +45,14 @@ $ balena login
|
||||
|
||||
HTTP(S) proxies can be configured through any of the following methods, in order of preference:
|
||||
|
||||
* Set the \`BALENARC_PROXY\` environment variable in URL format (with protocol, host, port, and
|
||||
* Set the `BALENARC_PROXY` environment variable in URL format (with protocol, host, port, and
|
||||
optionally basic auth).
|
||||
* Alternatively, use the [balena config file](https://www.npmjs.com/package/balena-settings-client#documentation)
|
||||
(project-specific or user-level) and set the \`proxy\` setting. It can be:
|
||||
(project-specific or user-level) and set the `proxy` setting. It can be:
|
||||
* A string in URL format, or
|
||||
* An object in the [global-tunnel-ng options format](https://www.npmjs.com/package/global-tunnel-ng#options) (which allows more control).
|
||||
* Alternatively, set the conventional \`https_proxy\` / \`HTTPS_PROXY\` / \`http_proxy\` / \`HTTP_PROXY\`
|
||||
environment variable (in the same standard URL format).
|
||||
* Alternatively, set the conventional `https_proxy` / `HTTPS_PROXY` / `http_proxy` / `HTTP_PROXY`
|
||||
environment variable (in the same standard URL format).
|
||||
|
||||
To get a proxy to work with the `balena ssh` command, check the
|
||||
[installation instructions](https://github.com/balena-io/balena-cli/blob/master/INSTALL.md).
|
||||
@ -84,7 +85,6 @@ If you come across any problems or would like to get in touch:
|
||||
|
||||
- [login](#login)
|
||||
- [logout](#logout)
|
||||
- [signup](#signup)
|
||||
- [whoami](#whoami)
|
||||
|
||||
- Device
|
||||
@ -109,7 +109,7 @@ If you come across any problems or would like to get in touch:
|
||||
|
||||
- [envs](#envs)
|
||||
- [env rm <id>](#env-rm-id)
|
||||
- [env add <key> [value]](#env-add-key-value)
|
||||
- [env add <name> [value]](#env-add-name-value)
|
||||
- [env rename <id> <value>](#env-rename-id-value)
|
||||
|
||||
- Tags
|
||||
@ -137,13 +137,9 @@ If you come across any problems or would like to get in touch:
|
||||
|
||||
- [logs <uuidOrDevice>](#logs-uuidordevice)
|
||||
|
||||
- Sync
|
||||
|
||||
- [sync [uuid]](#sync-uuid)
|
||||
|
||||
- SSH
|
||||
|
||||
- [ssh [uuid]](#ssh-uuid)
|
||||
- [ssh <applicationOrDevice> [serviceName]](#ssh-applicationordevice-servicename)
|
||||
- [tunnel <uuid>](#tunnel-uuid)
|
||||
|
||||
- Notes
|
||||
@ -182,11 +178,6 @@ If you come across any problems or would like to get in touch:
|
||||
|
||||
- [local configure <target>](#local-configure-target)
|
||||
- [local flash <image>](#local-flash-image)
|
||||
- [local logs [deviceIp]](#local-logs-deviceip)
|
||||
- [local scan](#local-scan)
|
||||
- [local ssh [deviceIp]](#local-ssh-deviceip)
|
||||
- [local push [deviceIp]](#local-push-deviceip)
|
||||
- [local stop [deviceIp]](#local-stop-deviceip)
|
||||
|
||||
- Deploy
|
||||
|
||||
@ -338,21 +329,6 @@ Examples:
|
||||
|
||||
$ balena logout
|
||||
|
||||
## signup
|
||||
|
||||
Use this command to signup for a balena account.
|
||||
|
||||
If signup is successful, you'll be logged in to your new user automatically.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena signup
|
||||
Email: johndoe@acme.com
|
||||
Password: ***********
|
||||
|
||||
$ balena whoami
|
||||
johndoe
|
||||
|
||||
## whoami
|
||||
|
||||
Use this command to find out the current logged in username and email address.
|
||||
@ -633,43 +609,52 @@ confirm non interactively
|
||||
|
||||
device
|
||||
|
||||
## env add <key> [value]
|
||||
## env add NAME [VALUE]
|
||||
|
||||
Use this command to add an enviroment or config variable to an application
|
||||
or device.
|
||||
Add an environment or config variable to an application or device, as selected
|
||||
by the respective command-line options.
|
||||
|
||||
If value is omitted, the tool will attempt to use the variable's value
|
||||
as defined in your host machine.
|
||||
If VALUE is omitted, the CLI will attempt to use the value of the environment
|
||||
variable of same name in the CLI process' environment. In this case, a warning
|
||||
message will be printed. Use `--quiet` to suppress it.
|
||||
|
||||
Use the `--device` option if you want to assign the environment variable
|
||||
to a specific device.
|
||||
|
||||
If the value is grabbed from the environment, a warning message will be printed.
|
||||
Use `--quiet` to remove it.
|
||||
|
||||
Service-specific variables are not currently supported. The following
|
||||
examples set variables that apply to all services in an app or device.
|
||||
Service-specific variables are not currently supported. The given command line
|
||||
examples variables that apply to all services in an app or device.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena env add EDITOR vim --application MyApp
|
||||
$ balena env add TERM --application MyApp
|
||||
$ balena env add EDITOR vim --application MyApp
|
||||
$ balena env add EDITOR vim --device 7cf02a6
|
||||
|
||||
### Arguments
|
||||
|
||||
#### NAME
|
||||
|
||||
environment or config variable name
|
||||
|
||||
#### VALUE
|
||||
|
||||
variable value; if omitted, use value from CLI's environment
|
||||
|
||||
### Options
|
||||
|
||||
#### --application, -a, --app <application>
|
||||
#### -a, --application APPLICATION
|
||||
|
||||
application name
|
||||
|
||||
#### --device, -d <device>
|
||||
#### -d, --device DEVICE
|
||||
|
||||
device uuid
|
||||
device UUID
|
||||
|
||||
#### -q, --quiet
|
||||
|
||||
suppress warning messages
|
||||
|
||||
## env rename <id> <value>
|
||||
|
||||
Use this command to change the value of an application or device
|
||||
enviroment variable.
|
||||
environment variable.
|
||||
|
||||
The --device option selects a device instead of an application.
|
||||
|
||||
@ -865,6 +850,7 @@ Examples:
|
||||
|
||||
$ balena logs 192.168.0.31
|
||||
$ balena logs 192.168.0.31 --service my-service
|
||||
$ balena logs 192.168.0.31 --service my-service-1 --service my-service-2
|
||||
|
||||
$ balena logs 23c73a1.local --system
|
||||
$ balena logs 23c73a1.local --system --service my-service
|
||||
@ -877,140 +863,56 @@ continuously stream output
|
||||
|
||||
#### --service, -s <service>
|
||||
|
||||
Only show logs for a single service. This can be used in combination with --system
|
||||
Reject logs not originating from this service.
|
||||
This can be used in combination with --system or other --service flags.
|
||||
|
||||
#### --system, -S
|
||||
|
||||
Only show system logs. This can be used in combination with --service.
|
||||
|
||||
# Sync
|
||||
|
||||
## sync [uuid]
|
||||
|
||||
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
Deprecation notice: please note that `balena sync` is deprecated and will
|
||||
be removed in a future release of the CLI. We are working on an exciting
|
||||
replacement that will be released soon!
|
||||
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
Warning: 'balena sync' requires an openssh-compatible client and 'rsync' to
|
||||
be correctly installed in your shell environment. For more information (including
|
||||
Windows support) please check the README here: https://github.com/balena-io/balena-cli
|
||||
|
||||
Use this command to sync your local changes to a certain device on the fly.
|
||||
|
||||
After every 'balena sync' the updated settings will be saved in
|
||||
'<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 <path>
|
||||
|
||||
local directory path to synchronize to device
|
||||
|
||||
#### --destination, -d <path>
|
||||
|
||||
destination path on device
|
||||
|
||||
#### --ignore, -i <paths>
|
||||
|
||||
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 <command>
|
||||
|
||||
execute a command before syncing
|
||||
|
||||
#### --after, -a <command>
|
||||
|
||||
execute a command after syncing
|
||||
|
||||
#### --port, -t <port>
|
||||
|
||||
ssh port
|
||||
|
||||
#### --progress, -p
|
||||
|
||||
show progress
|
||||
|
||||
#### --verbose, -v
|
||||
|
||||
increase verbosity
|
||||
|
||||
# SSH
|
||||
|
||||
## ssh [uuid]
|
||||
## ssh <applicationOrDevice> [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 <port>
|
||||
|
||||
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 <uuid>
|
||||
|
||||
@ -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 <certificate.crt>
|
||||
|
||||
@ -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 <dockerPort>
|
||||
#### --dockerPort <dockerPort>
|
||||
|
||||
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 <service>
|
||||
|
||||
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 <name>
|
||||
|
||||
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 <timeout>
|
||||
|
||||
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 <container>
|
||||
|
||||
name of container to access
|
||||
|
||||
#### --port, -p <port>
|
||||
|
||||
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 <path>
|
||||
|
||||
root of project directory to push
|
||||
|
||||
#### --destination, -d <path>
|
||||
|
||||
destination path on device container
|
||||
|
||||
#### --ignore, -i <paths>
|
||||
|
||||
comma delimited paths to ignore when syncing with 'rsync'
|
||||
|
||||
#### --skip-gitignore
|
||||
|
||||
do not parse excluded/included files from .gitignore
|
||||
|
||||
#### --before, -b <command>
|
||||
|
||||
execute a command before pushing
|
||||
|
||||
#### --after, -a <command>
|
||||
|
||||
execute a command after pushing
|
||||
|
||||
#### --progress, -p
|
||||
|
||||
show progress
|
||||
|
||||
#### --skip-logs
|
||||
|
||||
do not stream logs after push
|
||||
|
||||
#### --verbose, -v
|
||||
|
||||
increase verbosity
|
||||
|
||||
#### --app-name, -n <name>
|
||||
|
||||
application name - may contain lowercase characters, digits and one or more dashes. It may not start or end with a dash.
|
||||
|
||||
#### --build-triggers, -r <files>
|
||||
|
||||
comma delimited file list that will trigger a container rebuild if changed
|
||||
|
||||
#### --force-build, -f
|
||||
|
||||
force a container build and run
|
||||
|
||||
#### --env, -e <env>
|
||||
|
||||
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 <name>
|
||||
|
||||
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
150
lib/actions-oclif/env/add.ts
vendored
Normal 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;
|
||||
}
|
@ -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'
|
||||
|
@ -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() {
|
||||
|
@ -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
35
lib/actions/help_ts.ts
Normal 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];
|
||||
}
|
@ -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')
|
||||
|
@ -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)
|
||||
|
@ -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')
|
||||
|
@ -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 })
|
||||
|
@ -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')
|
||||
|
@ -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'
|
||||
|
@ -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
|
@ -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
504
lib/actions/ssh.ts
Normal 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;
|
||||
}
|
||||
},
|
||||
};
|
@ -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;
|
||||
}
|
@ -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 || '',
|
||||
|
@ -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
107
lib/app-common.ts
Normal 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
37
lib/app-oclif.ts
Normal 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
140
lib/app.ts
Normal 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);
|
||||
}
|
@ -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
25
lib/global.d.ts
vendored
Normal 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;
|
||||
}
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
128
lib/utils/device/ssh.ts
Normal 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' });
|
||||
}
|
@ -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
53
lib/utils/oclif-utils.ts
Normal 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);
|
||||
}
|
||||
}
|
@ -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) => {
|
||||
|
@ -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) => {
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
52
package.json
52
package.json
@ -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"
|
||||
}
|
||||
}
|
||||
|
224
patches/@oclif+dev-cli+1.22.0.patch
Normal file
224
patches/@oclif+dev-cli+1.22.0.patch
Normal 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;
|
@ -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
|
53
patches/qqjs++execa+0.10.0.patch
Normal file
53
patches/qqjs++execa+0.10.0.patch
Normal 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
16
patches/qqjs+0.3.10.patch
Normal 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
|
||||
*/
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
1
typings/@resin-valid-email.d.ts
vendored
1
typings/@resin-valid-email.d.ts
vendored
@ -1 +0,0 @@
|
||||
declare module '@resin.io/valid-email';
|
@ -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';
|
5
typings/balena-sync.d.ts
vendored
5
typings/balena-sync.d.ts
vendored
@ -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
22
typings/balena-sync/index.d.ts
vendored
Normal 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
1
typings/bash/index.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
declare module 'bash';
|
@ -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 {
|
@ -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;
|
13
typings/dockerfile-template.d.ts
vendored
13
typings/dockerfile-template.d.ts
vendored
@ -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
30
typings/dockerfile-template/index.d.ts
vendored
Normal 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
1
typings/ent.d.ts
vendored
@ -1 +0,0 @@
|
||||
declare module 'ent';
|
18
typings/ent/index.d.ts
vendored
Normal file
18
typings/ent/index.d.ts
vendored
Normal 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
28
typings/filehound/index.d.ts
vendored
Normal 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
3
typings/global.d.ts
vendored
@ -1,3 +0,0 @@
|
||||
interface Dictionary<T> {
|
||||
[key: string]: T;
|
||||
}
|
@ -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;
|
@ -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[]>;
|
4
typings/package.json.d.ts
vendored
4
typings/package.json.d.ts
vendored
@ -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
20
typings/pkg/index.d.ts
vendored
Normal 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>;
|
||||
}
|
6
typings/president.d.ts
vendored
6
typings/president.d.ts
vendored
@ -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
23
typings/president/index.d.ts
vendored
Normal 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
39
typings/publish-release/index.d.ts
vendored
Normal 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;
|
||||
}
|
1
typings/resin-cli-form.d.ts
vendored
1
typings/resin-cli-form.d.ts
vendored
@ -1 +0,0 @@
|
||||
declare module 'resin-cli-form';
|
18
typings/resin-cli-form/index.d.ts
vendored
Normal file
18
typings/resin-cli-form/index.d.ts
vendored
Normal 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';
|
1
typings/resin-cli-visuals.d.ts
vendored
1
typings/resin-cli-visuals.d.ts
vendored
@ -1 +0,0 @@
|
||||
declare module 'resin-cli-visuals';
|
18
typings/resin-cli-visuals/index.d.ts
vendored
Normal file
18
typings/resin-cli-visuals/index.d.ts
vendored
Normal 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';
|
5
typings/resin-image-fs.d.ts
vendored
5
typings/resin-image-fs.d.ts
vendored
@ -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
22
typings/resin-image-fs/index.d.ts
vendored
Normal 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
18
typings/resin.io/index.d.ts
vendored
Normal 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';
|
@ -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,
|
Loading…
x
Reference in New Issue
Block a user