Auto-merge for PR #743 via VersionBot

Start seriously converting the CLI to TypeScript
This commit is contained in:
resin-io-versionbot[bot] 2018-01-09 22:37:41 +00:00 committed by GitHub
commit 29145dfc2d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
64 changed files with 1351 additions and 857 deletions

View File

@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file
automatically by Versionist. DO NOT EDIT THIS FILE MANUALLY!
This project adheres to [Semantic Versioning](http://semver.org/).
## v6.12.3 - 2018-01-09
* Lint TypeScript and CoffeeScript with resin-lint #743 [Tim Perry]
* Move documentation generation to TypeScript #743 [Tim Perry]
* Convert most of utils to TypeScript #743 [Tim Perry]
## v6.12.2 - 2018-01-09
* Convert windows paths to posix when passing to tar #748 [Andrew Shirley]

View File

@ -16,13 +16,13 @@ declare module 'filehound' {
declare module 'publish-release' {
interface PublishOptions {
token: string,
owner: string,
repo: string,
tag: string,
name: string,
reuseRelease?: boolean
assets: string[]
token: string;
owner: string;
repo: string;
tag: string;
name: string;
reuseRelease?: boolean;
assets: string[];
}
interface Release {

View File

@ -5,6 +5,7 @@ import * as fs from 'fs-extra';
import * as mkdirp from 'mkdirp';
import * as publishRelease from 'publish-release';
import * as archiver from 'archiver';
import * as packageJSON from '../package.json';
const publishReleaseAsync = Promise.promisify(publishRelease);
const mkdirpAsync = Promise.promisify<string | null, string>(mkdirp);
@ -12,14 +13,14 @@ const mkdirpAsync = Promise.promisify<string | null, string>(mkdirp);
const { GITHUB_TOKEN } = process.env;
const ROOT = path.join(__dirname, '..');
const version = 'v' + require('../package.json').version;
const version = 'v' + packageJSON.version;
const outputFile = path.join(ROOT, 'build-zip', `resin-cli-${version}-${os.platform()}-${os.arch()}.zip`);
mkdirpAsync(path.dirname(outputFile)).then(() => new Promise((resolve, reject) => {
console.log('Zipping build...');
let archive = archiver('zip', {
zlib: { level: 7 }
zlib: { level: 7 },
});
archive.directory(path.join(ROOT, 'build-bin'), 'resin-cli');
@ -44,7 +45,7 @@ mkdirpAsync(path.dirname(outputFile)).then(() => new Promise((resolve, reject) =
tag: version,
name: `Resin-CLI ${version}`,
reuseRelease: true,
assets: [outputFile]
assets: [outputFile],
});
}).then((release) => {
console.log(`Release ${version} successful: ${release.html_url}`);

View File

@ -10,6 +10,7 @@
"sourceMap": true
},
"include": [
"./**/*.ts"
"./**/*.ts",
"../typings/*.d.ts"
]
}

114
capitanodoc.ts Normal file
View File

@ -0,0 +1,114 @@
export = {
title: 'Resin CLI Documentation',
introduction: `\
This tool allows you to interact with the resin.io api from the comfort of your command line.
Please make sure your system meets the requirements as specified in the [README](https://github.com/resin-io/resin-cli).
To get started download the CLI from npm.
$ npm install resin-cli -g
Then authenticate yourself:
$ resin login
Now you have access to all the commands referenced below.
## Proxy support
The CLI does support HTTP(S) proxies.
You can configure the proxy using several methods (in order of their precedence):
* set the \`RESINRC_PROXY\` environment variable in the URL format (with protocol, host, port, and optionally the basic auth),
* use the [resin config file](https://www.npmjs.com/package/resin-settings-client#documentation) (project-specific or user-level)
and set the \`proxy\` setting. This can be:
* a string in the URL format,
* or an object following [this format](https://www.npmjs.com/package/global-tunnel-ng#options), which allows more control,
* or set the conventional \`https_proxy\` / \`HTTPS_PROXY\` / \`http_proxy\` / \`HTTP_PROXY\`
environment variable (in the same standard URL format).\
`,
categories: [
{
title: 'Application',
files: [ 'build/actions/app.js' ]
},
{
title: 'Authentication',
files: [ 'build/actions/auth.js' ]
},
{
title: 'Device',
files: [ 'build/actions/device.js' ]
},
{
title: 'Environment Variables',
files: [ 'build/actions/environment-variables.js' ]
},
{
title: 'Help',
files: [ 'build/actions/help.js' ]
},
{
title: 'Information',
files: [ 'build/actions/info.js' ]
},
{
title: 'Keys',
files: [ 'build/actions/keys.js' ]
},
{
title: 'Logs',
files: [ 'build/actions/logs.js' ]
},
{
title: 'Sync',
files: [ 'build/actions/sync.js' ]
},
{
title: 'SSH',
files: [ 'build/actions/ssh.js' ]
},
{
title: 'Notes',
files: [ 'build/actions/notes.js' ]
},
{
title: 'OS',
files: [ 'build/actions/os.js' ]
},
{
title: 'Config',
files: [ 'build/actions/config.js' ]
},
{
title: 'Preload',
files: [ 'build/actions/preload.js' ]
},
{
title: 'Settings',
files: [ 'build/actions/settings.js' ]
},
{
title: 'Wizard',
files: [ 'build/actions/wizard.js' ]
},
{
title: 'Local',
files: [ 'build/actions/local/index.js' ]
},
{
title: 'Deploy',
files: [
'build/actions/build.js',
'build/actions/deploy.js'
]
},
{
title: 'Utilities',
files: [ 'build/actions/util.js' ]
},
]
};

View File

@ -254,7 +254,7 @@ web-based login
credential-based login
#### --email, --e,u, --e,u &#60;email&#62;
#### --email, -e, -u &#60;email&#62;
email
@ -310,7 +310,7 @@ Examples:
### Options
#### --application, --a,app, --a,app &#60;application&#62;
#### --application, -a, --app &#60;application&#62;
application name
@ -464,7 +464,7 @@ Examples:
### Options
#### --application, --a,app, --a,app &#60;application&#62;
#### --application, -a, --app &#60;application&#62;
application name
@ -482,7 +482,7 @@ Examples:
### Options
#### --application, --a,app, --a,app &#60;application&#62;
#### --application, -a, --app &#60;application&#62;
application name
@ -529,7 +529,7 @@ Example:
### Options
#### --application, --a,app, --a,app &#60;application&#62;
#### --application, -a, --app &#60;application&#62;
application name
@ -589,7 +589,7 @@ Examples:
### Options
#### --application, --a,app, --a,app &#60;application&#62;
#### --application, -a, --app &#60;application&#62;
application name
@ -845,7 +845,7 @@ Examples:
### Options
#### --device, --d,dev, --d,dev &#60;device&#62;
#### --device, -d, --dev &#60;device&#62;
device uuid
@ -937,7 +937,7 @@ Examples:
show advanced configuration options
#### --application, --a,app, --a,app &#60;application&#62;
#### --application, -a, --app &#60;application&#62;
application name
@ -1081,7 +1081,7 @@ Examples:
### Options
#### --application, --a,app, --a,app &#60;application&#62;
#### --application, -a, --app &#60;application&#62;
application name

14
extras/capitanodoc/doc-types.d.ts vendored Normal file
View File

@ -0,0 +1,14 @@
import { CommandDefinition } from 'capitano';
export interface Document {
title: string;
introduction: string;
categories: Category[];
}
export interface Category {
title: string;
commands: CommandDefinition[];
}
export { CommandDefinition as Command };

View File

@ -1,46 +0,0 @@
_ = require('lodash')
path = require('path')
capitanodoc = require('../../capitanodoc')
markdown = require('./markdown')
result = {}
result.title = capitanodoc.title
result.introduction = capitanodoc.introduction
result.categories = []
for commandCategory in capitanodoc.categories
category = {}
category.title = commandCategory.title
category.commands = []
for file in commandCategory.files
actions = require(path.join(process.cwd(), file))
if actions.signature?
category.commands.push(_.omit(actions, 'action'))
else
for actionName, actionCommand of actions
category.commands.push(_.omit(actionCommand, 'action'))
result.categories.push(category)
result.toc = _.cloneDeep(result.categories)
result.toc = _.map result.toc, (category) ->
category.commands = _.map category.commands, (command) ->
return {
signature: command.signature
anchor: '#' + command.signature
.replace(/\s/g,'-')
.replace(/</g, '60-')
.replace(/>/g, '-62-')
.replace(/\[/g, '')
.replace(/\]/g, '-')
.replace(/--/g, '-')
.replace(/\.\.\./g, '')
.replace(/\|/g, '')
.toLowerCase()
}
return category
console.log(markdown.display(result))

View File

@ -0,0 +1,34 @@
import capitanodoc = require('../../capitanodoc');
import * as _ from 'lodash';
import * as path from 'path';
import * as markdown from './markdown';
import { Document, Category } from './doc-types';
const result = <Document> {};
result.title = capitanodoc.title;
result.introduction = capitanodoc.introduction;
result.categories = [];
for (let commandCategory of capitanodoc.categories) {
const category = <Category> {};
category.title = commandCategory.title;
category.commands = [];
for (let file of commandCategory.files) {
// tslint:disable-next-line:no-var-requires
const actions: any = require(path.join(process.cwd(), file));
if (actions.signature) {
category.commands.push(_.omit(actions, 'action'));
} else {
for (let actionName of Object.keys(actions)) {
const actionCommand = actions[actionName];
category.commands.push(_.omit(actionCommand, 'action'));
}
}
}
result.categories.push(category);
}
console.log(markdown.render(result));

View File

@ -1,66 +0,0 @@
_ = require('lodash')
ent = require('ent')
utils = require('./utils')
exports.command = (command) ->
result = """
## #{ent.encode(command.signature)}
#{command.help}\n
"""
if not _.isEmpty(command.options)
result += '\n### Options'
for option in command.options
result += """
\n\n#### #{utils.parseSignature(option)}
#{option.description}
"""
result += '\n'
return result
exports.category = (category) ->
result = """
# #{category.title}\n
"""
for command in category.commands
result += '\n' + exports.command(command)
return result
exports.toc = (toc) ->
result = '''
# Table of contents\n
'''
for category in toc
result += """
\n- #{category.title}\n\n
"""
for command in category.commands
result += """
\t- [#{ent.encode(command.signature)}](#{command.anchor})\n
"""
return result
exports.display = (doc) ->
result = """
# #{doc.title}
#{doc.introduction}
#{exports.toc(doc.toc)}
"""
for category in doc.categories
result += '\n' + exports.category(category)
return result

View File

@ -0,0 +1,68 @@
import * as _ from 'lodash';
import * as ent from 'ent';
import * as utils from './utils';
import { Document, Category, Command } from './doc-types';
export function renderCommand(command: Command) {
let result = `## ${ent.encode(command.signature)}\n\n${command.help}\n`;
if (!_.isEmpty(command.options)) {
result += '\n### Options';
for (let option of command.options!) {
result += `\n\n#### ${utils.parseSignature(option)}\n\n${option.description}`;
}
result += '\n';
}
return result;
}
export function renderCategory(category: Category) {
let result = `# ${category.title}\n`;
for (let command of category.commands) {
result += `\n${renderCommand(command)}`;
}
return result;
}
function getAnchor(command: Command) {
return '#' + command.signature
.replace(/\s/g,'-')
.replace(/</g, '60-')
.replace(/>/g, '-62-')
.replace(/\[/g, '')
.replace(/\]/g, '-')
.replace(/--/g, '-')
.replace(/\.\.\./g, '')
.replace(/\|/g, '')
.toLowerCase();
}
export function renderToc(categories: Category[]) {
let result = `# Table of contents\n`;
for (let category of categories) {
result += `\n- ${category.title}\n\n`;
for (let command of category.commands) {
result += `\t- [${ent.encode(command.signature)}](${getAnchor(command)})\n`;
}
}
return result;
}
export function render(doc: Document) {
let result = `# ${doc.title}\n\n${doc.introduction}\n\n${renderToc(doc.categories)}`;
for (let category of doc.categories) {
result += `\n${renderCategory(category)}`;
}
return result;
}

View File

@ -1,26 +0,0 @@
_ = require('lodash')
ent = require('ent')
exports.getOptionPrefix = (signature) ->
if signature.length > 1
return '--'
else
return '-'
exports.getOptionSignature = (signature) ->
return "#{exports.getOptionPrefix(signature)}#{signature}"
exports.parseSignature = (option) ->
result = exports.getOptionSignature(option.signature)
if not _.isEmpty(option.alias)
if _.isString(option.alias)
result += ", #{exports.getOptionSignature(option.alias)}"
else
for alias in option.alias
result += ", #{exports.getOptionSignature(option.alias)}"
if option.parameter?
result += " <#{option.parameter}>"
return ent.encode(result)

View File

@ -0,0 +1,33 @@
import { OptionDefinition } from 'capitano';
import * as _ from 'lodash';
import * as ent from 'ent';
export function getOptionPrefix(signature: string) {
if (signature.length > 1) {
return '--';
} else {
return '-';
}
}
export function getOptionSignature(signature: string) {
return `${getOptionPrefix(signature)}${signature}`;
}
export function parseSignature(option: OptionDefinition) {
let result = getOptionSignature(option.signature);
if (_.isArray(option.alias)) {
for (let alias of option.alias) {
result += `, ${getOptionSignature(alias)}`;
}
} else if (_.isString(option.alias)) {
result += `, ${getOptionSignature(option.alias)}`;
}
if (option.parameter) {
result += ` <${option.parameter}>`;
}
return ent.encode(result);
}

17
extras/tsconfig.json Normal file
View File

@ -0,0 +1,17 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"outDir": "build",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"preserveConstEnums": true,
"removeComments": true,
"sourceMap": true
},
"include": [
"../typings/*.d.ts",
"./**/*.ts"
]
}

View File

@ -1,15 +1,12 @@
path = require('path')
gulp = require('gulp')
coffee = require('gulp-coffee')
coffeelint = require('gulp-coffeelint')
inlinesource = require('gulp-inline-source')
mocha = require('gulp-mocha')
shell = require('gulp-shell')
packageJSON = require('./package.json')
OPTIONS =
config:
coffeelint: path.join(__dirname, 'coffeelint.json')
files:
coffee: [ 'lib/**/*.coffee', 'gulpfile.coffee' ]
app: 'lib/**/*.coffee'
@ -23,18 +20,11 @@ gulp.task 'pages', ->
.pipe(inlinesource())
.pipe(gulp.dest('build/auth/pages'))
gulp.task 'coffee', [ 'lint' ], ->
gulp.task 'coffee', ->
gulp.src(OPTIONS.files.app)
.pipe(coffee(bare: true, header: true))
.pipe(gulp.dest(OPTIONS.directories.build))
gulp.task 'lint', ->
gulp.src(OPTIONS.files.coffee)
.pipe(coffeelint({
optFile: OPTIONS.config.coffeelint
}))
.pipe(coffeelint.reporter())
gulp.task 'test', ->
gulp.src(OPTIONS.files.tests, read: false)
.pipe(mocha({

View File

@ -8,7 +8,7 @@ getBundleInfo = Promise.method (options) ->
if options.application?
# An application was provided
return helpers.getAppInfo(options.application)
return helpers.getArchAndDeviceType(options.application)
.then (app) ->
return [app.arch, app.device_type]
else if options.arch? and options.deviceType?

View File

@ -271,7 +271,6 @@ exports.generate =
Promise = require('bluebird')
writeFileAsync = Promise.promisify(require('fs').writeFile)
resin = require('resin-sdk-preconfigured')
_ = require('lodash')
form = require('resin-cli-form')
deviceConfig = require('resin-device-config')
prettyjson = require('prettyjson')

View File

@ -40,7 +40,7 @@ showPushProgress = (message) ->
getBundleInfo = (options) ->
helpers = require('../utils/helpers')
helpers.getAppInfo(options.appName)
helpers.getArchAndDeviceType(options.appName)
.then (app) ->
[app.arch, app.device_type]

View File

@ -401,7 +401,6 @@ exports.init =
tmp.setGracefulCleanup()
resin = require('resin-sdk-preconfigured')
helpers = require('../utils/helpers')
patterns = require('../utils/patterns')
Promise.try ->

View File

@ -1,4 +1,4 @@
###
/*
Copyright 2016-2017 Resin.io
Licensed under the Apache License, Version 2.0 (the "License");
@ -12,15 +12,19 @@ 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.
###
*/
exports.version =
signature: 'version'
description: 'output the version number'
help: '''
Display the Resin CLI version.
'''
action: (params, options, done) ->
packageJSON = require('../../package.json')
console.log(packageJSON.version)
return done()
import { CommandDefinition } from 'capitano';
export const version: CommandDefinition = {
signature: 'version',
description: 'output the version number',
help: `\
Display the Resin CLI version.\
`,
async action(_params, _options, done) {
const packageJSON = await import('../../package.json');
console.log(packageJSON.version);
return done();
},
};

View File

@ -44,7 +44,6 @@ module.exports =
permission: 'user'
primary: true
action: (params, options, done) ->
_ = require('lodash')
resin = require('resin-sdk-preconfigured')
moment = require('moment')

View File

@ -63,7 +63,7 @@ selectApplicationCommit = (builds) ->
if builds.length == 0
expectedError('This application has no successful builds.')
DEFAULT_CHOICE = {'name': LATEST, 'value': LATEST}
DEFAULT_CHOICE = { 'name': LATEST, 'value': LATEST }
choices = [ DEFAULT_CHOICE ].concat builds.map (build) ->
name: "#{build.push_timestamp} - #{build.commit_hash}"
value: build.commit_hash
@ -150,8 +150,6 @@ module.exports =
_ = require('lodash')
Promise = require('bluebird')
resin = require('resin-sdk-preconfigured')
streamToPromise = require('stream-to-promise')
form = require('resin-cli-form')
preload = require('resin-preload')
errors = require('resin-errors')
visuals = require('resin-cli-visuals')

View File

@ -1,4 +1,4 @@
###
/*
Copyright 2016-2017 Resin.io
Licensed under the Apache License, Version 2.0 (the "License");
@ -12,23 +12,27 @@ 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.
###
*/
exports.list =
signature: 'settings'
description: 'print current settings'
help: '''
Use this command to display detected settings
import { CommandDefinition } from 'capitano';
Examples:
export const list: CommandDefinition = {
signature: 'settings',
description: 'print current settings',
help: `\
Use this command to display detected settings
$ resin settings
'''
action: (params, options, done) ->
resin = require('resin-sdk-preconfigured')
prettyjson = require('prettyjson')
Examples:
resin.settings.getAll()
$ resin settings\
`,
async action(_params, _options, done) {
const resin = (await import('resin-sdk')).fromSharedOptions();
const prettyjson = await import('prettyjson');
return resin.settings.getAll()
.then(prettyjson.render)
.then(console.log)
.nodeify(done)
.nodeify(done);
},
};

View File

@ -1,4 +1,4 @@
###
/*
Copyright 2016-2017 Resin.io
Licensed under the Apache License, Version 2.0 (the "License");
@ -12,6 +12,7 @@ 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.
###
*/
module.exports = require('resin-sync').capitano('resin-cli')
import * as ResinSync from 'resin-sync';
export = ResinSync.capitano('resin-cli');

View File

@ -56,7 +56,6 @@ globalTunnel.initialize(proxy)
# TODO: make this a feature of capitano https://github.com/resin-io/capitano/issues/48
global.PROXY_CONFIG = globalTunnel.proxyConfig
_ = require('lodash')
Promise = require('bluebird')
capitano = require('capitano')
capitanoExecuteAsync = Promise.promisify(capitano.execute)

View File

@ -1 +1 @@
exports.sentryDsn = 'https://56d2a46124614b01b0f4086897e96110:6e175465accc41b595a96947155f61fb@sentry.io/149239'
export const sentryDsn = 'https://56d2a46124614b01b0f4086897e96110:6e175465accc41b595a96947155f61fb@sentry.io/149239';

View File

@ -1,38 +0,0 @@
###
Copyright 2016-2017 Resin.io
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.
###
chalk = require('chalk')
errors = require('resin-cli-errors')
patterns = require('./utils/patterns')
Raven = require('raven')
Promise = require('bluebird')
captureException = Promise.promisify(Raven.captureException.bind(Raven))
exports.handle = (error) ->
message = errors.interpret(error)
return if not message?
if process.env.DEBUG
message = error.stack
patterns.printErrorMessage(message)
captureException(error)
.timeout(1000)
.catch(-> # Ignore any errors (from error logging, or timeouts)
).finally ->
process.exit(error.exitCode or 1)

39
lib/errors.ts Normal file
View File

@ -0,0 +1,39 @@
/*
Copyright 2016-2017 Resin.io
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 errors = require('resin-cli-errors');
import patterns = require('./utils/patterns');
import Raven = require('raven');
import Promise = require('bluebird');
const captureException = Promise.promisify<string, Error>(Raven.captureException, { context: Raven });
exports.handle = function(error: any) {
let message = errors.interpret(error);
if ((message == null)) { return; }
if (process.env.DEBUG) {
message = error.stack;
}
patterns.printErrorMessage(message);
return captureException(error)
.timeout(1000)
.catch(function() {
// Ignore any errors (from error logging, or timeouts)
}).finally(() => process.exit(error.exitCode || 1));
};

View File

@ -1,34 +0,0 @@
_ = require('lodash')
Mixpanel = require('mixpanel')
Raven = require('raven')
Promise = require('bluebird')
resin = require('resin-sdk-preconfigured')
packageJSON = require('../package.json')
exports.getLoggerInstance = _.memoize ->
return resin.models.config.getMixpanelToken().then(Mixpanel.init)
exports.trackCommand = (capitanoCommand) ->
capitanoStateGetMatchCommandAsync = Promise.promisify(require('capitano').state.getMatchCommand)
return Promise.props
resinUrl: resin.settings.get('resinUrl')
username: resin.auth.whoami().catchReturn(undefined)
mixpanel: exports.getLoggerInstance()
.then ({ username, resinUrl, mixpanel }) ->
return capitanoStateGetMatchCommandAsync(capitanoCommand.command).then (command) ->
Raven.mergeContext(user: {
id: username,
username
})
mixpanel.track "[CLI] #{command.signature.toString()}",
distinct_id: username
argv: process.argv.join(' ')
version: packageJSON.version
node: process.version
arch: process.arch
resinUrl: resinUrl
platform: process.platform
command: capitanoCommand
.timeout(100)
.catchReturn()

42
lib/events.ts Normal file
View File

@ -0,0 +1,42 @@
import * as Capitano from 'capitano';
import _ = require('lodash');
import Mixpanel = require('mixpanel');
import Raven = require('raven');
import Promise = require('bluebird');
import ResinSdk = require('resin-sdk');
import packageJSON = require('../package.json');
const resin = ResinSdk.fromSharedOptions();
const getMatchCommandAsync = Promise.promisify(Capitano.state.getMatchCommand);
const getMixpanel = _.memoize<any>(() => resin.models.config.getAll().get('mixpanelToken').then(Mixpanel.init));
export function trackCommand(capitanoCli: Capitano.Cli) {
return Promise.props({
resinUrl: resin.settings.get('resinUrl'),
username: resin.auth.whoami().catchReturn(undefined),
mixpanel: getMixpanel(),
}).then(({ username, resinUrl, mixpanel }) => {
return getMatchCommandAsync(capitanoCli.command).then((command) => {
Raven.mergeContext({
user: {
id: username,
username,
},
});
return mixpanel.track(`[CLI] ${command.signature.toString()}`, {
distinct_id: username,
argv: process.argv.join(' '),
version: packageJSON.version,
node: process.version,
arch: process.arch,
resinUrl,
platform: process.platform,
command: capitanoCli,
});
});
})
.timeout(100)
.catchReturn(undefined);
}

View File

@ -1,97 +0,0 @@
###
Copyright 2016-2017 Resin.io
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.
###
exports.generateBaseConfig = (application, options) ->
Promise = require('bluebird')
_ = require('lodash')
deviceConfig = require('resin-device-config')
resin = require('resin-sdk-preconfigured')
options = _.mapValues options, (value, key) ->
if key == 'appUpdatePollInterval'
value * 60 * 1000
else
value
Promise.props
userId: resin.auth.getUserId()
username: resin.auth.whoami()
apiUrl: resin.settings.get('apiUrl')
vpnUrl: resin.settings.get('vpnUrl')
registryUrl: resin.settings.get('registryUrl')
deltaUrl: resin.settings.get('deltaUrl')
pubNubKeys: resin.models.config.getPubNubKeys()
mixpanelToken: resin.models.config.getMixpanelToken()
.then (results) ->
deviceConfig.generate
application: application
user:
id: results.userId
username: results.username
endpoints:
api: results.apiUrl
vpn: results.vpnUrl
registry: results.registryUrl
delta: results.deltaUrl
pubnub: results.pubNubKeys
mixpanel:
token: results.mixpanelToken
, options
exports.generateApplicationConfig = (application, options) ->
exports.generateBaseConfig(application, options)
.tap (config) ->
authenticateWithApplicationKey(config, application.id)
exports.generateDeviceConfig = (device, deviceApiKey, options) ->
resin = require('resin-sdk-preconfigured')
resin.models.application.get(device.application_name)
.then (application) ->
exports.generateBaseConfig(application, options)
.tap (config) ->
# Device API keys are only safe for ResinOS 2.0.3+. We could somehow obtain
# the expected version for this config and generate one when we know it's safe,
# but instead for now we fall back to app keys unless the user has explicitly opted in.
if deviceApiKey?
authenticateWithDeviceKey(config, device.uuid, deviceApiKey)
else
authenticateWithApplicationKey(config, application.id)
.then (config) ->
# Associate a device, to prevent the supervisor
# from creating another one on its own.
config.registered_at = Math.floor(Date.now() / 1000)
config.deviceId = device.id
config.uuid = device.uuid
return config
authenticateWithApplicationKey = (config, applicationNameOrId) ->
resin = require('resin-sdk-preconfigured')
resin.models.application.generateApiKey(applicationNameOrId)
.then (apiKey) ->
config.apiKey = apiKey
return config
authenticateWithDeviceKey = (config, uuid, customDeviceApiKey) ->
Promise = require('bluebird')
resin = require('resin-sdk-preconfigured')
Promise.try ->
customDeviceApiKey || resin.models.device.generateDeviceKey(uuid)
.then (deviceApiKey) ->
config.deviceApiKey = deviceApiKey
return config

108
lib/utils/config.ts Normal file
View File

@ -0,0 +1,108 @@
/*
Copyright 2016-2017 Resin.io
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 Promise = require('bluebird');
import ResinSdk = require('resin-sdk');
import _ = require('lodash');
import deviceConfig = require('resin-device-config');
const resin = ResinSdk.fromSharedOptions();
export function generateBaseConfig(application: ResinSdk.Application, options: {}) {
options = _.mapValues(options, function(value, key) {
if (key === 'appUpdatePollInterval') {
return value * 60 * 1000;
} else {
return value;
}
});
return Promise.props({
userId: resin.auth.getUserId(),
username: resin.auth.whoami(),
apiUrl: resin.settings.get('apiUrl'),
vpnUrl: resin.settings.get('vpnUrl'),
registryUrl: resin.settings.get('registryUrl'),
deltaUrl: resin.settings.get('deltaUrl'),
apiConfig: resin.models.config.getAll(),
}).then((results) => {
return deviceConfig.generate({
application,
user: {
id: results.userId,
username: results.username,
},
endpoints: {
api: results.apiUrl,
vpn: results.vpnUrl,
registry: results.registryUrl,
delta: results.deltaUrl,
},
pubnub: results.apiConfig.pubnub,
mixpanel: {
token: results.apiConfig.mixpanelToken,
},
}, options);
});
}
export function generateApplicationConfig(application: ResinSdk.Application, options: {}) {
return generateBaseConfig(application, options)
.tap(config => addApplicationKey(config, application.id));
}
export function generateDeviceConfig(
device: ResinSdk.Device & { application_name: string },
deviceApiKey: string | null,
options: {},
) {
return resin.models.application.get(device.application_name)
.then(application => {
return generateBaseConfig(application, options)
.tap((config) => {
// Device API keys are only safe for ResinOS 2.0.3+. We could somehow obtain
// the expected version for this config and generate one when we know it's safe,
// but instead for now we fall back to app keys unless the user has explicitly opted in.
if (deviceApiKey) {
return addDeviceKey(config, device.uuid, deviceApiKey);
} else {
return addApplicationKey(config, application.id);
}
});
}).then((config) => {
// Associate a device, to prevent the supervisor
// from creating another one on its own.
config.registered_at = Math.floor(Date.now() / 1000);
config.deviceId = device.id;
config.uuid = device.uuid;
return config;
});
}
function addApplicationKey(config: any, applicationNameOrId: string | number) {
return resin.models.application.generateApiKey(applicationNameOrId)
.tap((apiKey) => {
config.apiKey = apiKey;
});
}
function addDeviceKey(config: any, uuid: string, customDeviceApiKey: string) {
return Promise.try(() => {
return customDeviceApiKey || resin.models.device.generateDeviceKey(uuid);
}).tap((deviceApiKey) => {
config.deviceApiKey = deviceApiKey;
});
}

View File

@ -1,135 +0,0 @@
###
Copyright 2016-2017 Resin.io
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.
###
Promise = require('bluebird')
exports.getGroupDefaults = (group) ->
_ = require('lodash')
return _.chain(group)
.get('options')
.map (question) ->
return [ question.name, question.default ]
.fromPairs()
.value()
exports.stateToString = (state) ->
_str = require('underscore.string')
chalk = require('chalk')
percentage = _str.lpad(state.percentage, 3, '0') + '%'
result = "#{chalk.blue(percentage)} #{chalk.cyan(state.operation.command)}"
switch state.operation.command
when 'copy'
return "#{result} #{state.operation.from.path} -> #{state.operation.to.path}"
when 'replace'
return "#{result} #{state.operation.file.path}, #{state.operation.copy} -> #{state.operation.replace}"
when 'run-script'
return "#{result} #{state.operation.script}"
else
throw new Error("Unsupported operation: #{state.operation.type}")
exports.sudo = (command) ->
_ = require('lodash')
os = require('os')
if os.platform() isnt 'win32'
console.log('If asked please type your computer password to continue')
command = _.union(_.take(process.argv, 2), command)
presidentExecuteAsync = Promise.promisify(require('president').execute)
return presidentExecuteAsync(command)
exports.getManifest = (image, deviceType) ->
rindle = require('rindle')
imagefs = require('resin-image-fs')
resin = require('resin-sdk-preconfigured')
# Attempt to read manifest from the first
# partition, but fallback to the API if
# we encounter any errors along the way.
imagefs.read
image: image
partition:
primary: 1
path: '/device-type.json'
.then(rindle.extractAsync)
.then(JSON.parse)
.catch ->
resin.models.device.getManifestBySlug(deviceType)
exports.osProgressHandler = (step) ->
rindle = require('rindle')
visuals = require('resin-cli-visuals')
step.on('stdout', process.stdout.write.bind(process.stdout))
step.on('stderr', process.stderr.write.bind(process.stderr))
step.on 'state', (state) ->
return if state.operation.command is 'burn'
console.log(exports.stateToString(state))
progressBars =
write: new visuals.Progress('Writing Device OS')
check: new visuals.Progress('Validating Device OS')
step.on 'burn', (state) ->
progressBars[state.type].update(state)
return rindle.wait(step)
exports.getAppInfo = (application) ->
resin = require('resin-sdk-preconfigured')
_ = require('lodash')
Promise.join(
getApplication(application),
resin.models.config.getDeviceTypes(),
(app, config) ->
config = _.find(config, 'slug': app.device_type)
if !config?
throw new Error('Could not read application information!')
app.arch = config.arch
return app
)
getApplication = (application) ->
resin = require('resin-sdk-preconfigured')
# Check for an app of the form `user/application`, and send
# this off to a special handler (before importing any modules)
if (match = /(\w+)\/(\w+)/.exec(application))
return resin.models.application.getAppWithOwner(match[2], match[1])
return resin.models.application.get(application)
# A function to reliably execute a command
# in all supported operating systems, including
# different Windows environments like `cmd.exe`
# and `Cygwin`.
exports.getSubShellCommand = (command) ->
os = require('os')
if os.platform() is 'win32'
return {
program: 'cmd.exe'
args: [ '/s', '/c', command ]
}
else
return {
program: '/bin/sh'
args: [ '-c', command ]
}

150
lib/utils/helpers.ts Normal file
View File

@ -0,0 +1,150 @@
/*
Copyright 2016-2017 Resin.io
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 os = require('os');
import Promise = require('bluebird');
import _ = require('lodash');
import chalk from 'chalk';
import rindle = require('rindle');
import imagefs = require('resin-image-fs');
import visuals = require('resin-cli-visuals');
import ResinSdk = require('resin-sdk');
import { execute } from 'president';
import { InitializeEmitter, OperationState } from 'resin-device-init';
const extractStreamAsync = Promise.promisify(rindle.extract);
const waitStreamAsync = Promise.promisify(rindle.wait);
const presidentExecuteAsync = Promise.promisify(execute);
const resin = ResinSdk.fromSharedOptions();
export function getGroupDefaults(
group: { options: { name: string, default?: string }[] },
): { [name: string]: string | undefined } {
return _.chain(group)
.get('options')
.map((question) => [ question.name, question.default ])
.fromPairs()
.value();
}
export function stateToString(state: OperationState) {
const percentage = _.padStart(`${state.percentage}`, 3, '0');
const result = `${chalk.blue(percentage + '%')} ${chalk.cyan(state.operation.command)}`;
switch (state.operation.command) {
case 'copy':
return `${result} ${state.operation.from.path} -> ${state.operation.to.path}`;
case 'replace':
return `${result} ${state.operation.file.path}, ${state.operation.copy} -> ${state.operation.replace}`;
case 'run-script':
return `${result} ${state.operation.script}`;
default:
throw new Error(`Unsupported operation: ${state.operation.command}`);
}
}
export function sudo(command: string[]) {
if (os.platform() !== 'win32') {
console.log('If asked please type your computer password to continue');
}
command = _.union(_.take(process.argv, 2), command);
return presidentExecuteAsync(command);
}
export function getManifest(image: string, deviceType: string): Promise<ResinSdk.DeviceType> {
// Attempt to read manifest from the first
// partition, but fallback to the API if
// we encounter any errors along the way.
return imagefs.read({
image,
partition: {
primary: 1,
},
path: '/device-type.json',
})
.then(extractStreamAsync)
.then(JSON.parse)
.catch(() => resin.models.device.getManifestBySlug(deviceType));
}
export function osProgressHandler(step: InitializeEmitter) {
step.on('stdout', process.stdout.write.bind(process.stdout));
step.on('stderr', process.stderr.write.bind(process.stderr));
step.on('state', function(state) {
if (state.operation.command === 'burn') { return; }
console.log(exports.stateToString(state));
});
const progressBars = {
write: new visuals.Progress('Writing Device OS'),
check: new visuals.Progress('Validating Device OS'),
};
step.on('burn', state => progressBars[state.type].update(state));
return waitStreamAsync(step);
}
export function getArchAndDeviceType(applicationName: string): Promise<{ arch: string, device_type: string }> {
return Promise.join(
getApplication(applicationName),
resin.models.config.getDeviceTypes(),
function (app, deviceTypes) {
const config = _.find(deviceTypes, { slug: app.device_type });
if (!config) {
throw new Error('Could not read application information!');
}
return { device_type: app.device_type, arch: config.arch };
},
);
}
function getApplication(applicationName: string) {
// Check for an app of the form `user/application`, and send
// that off to a special handler (before importing any modules)
const match = /(\w+)\/(\w+)/.exec(applicationName);
if (match) {
return resin.models.application.getAppByOwner(match[2], match[1]);
}
return resin.models.application.get(applicationName);
}
// A function to reliably execute a command
// in all supported operating systems, including
// different Windows environments like `cmd.exe`
// and `Cygwin`.
export function getSubShellCommand(command: string) {
if (os.platform() === 'win32') {
return {
program: 'cmd.exe',
args: [ '/s', '/c', command ],
};
} else {
return {
program: '/bin/sh',
args: [ '-c', command ],
};
}
}

View File

@ -1,46 +0,0 @@
eol = require('os').EOL
module.exports = class Logger
constructor: ->
{ StreamLogger } = require('resin-stream-logger')
colors = require('colors')
_ = require('lodash')
logger = new StreamLogger()
logger.addPrefix('build', colors.blue('[Build]'))
logger.addPrefix('info', colors.cyan('[Info]'))
logger.addPrefix('debug', colors.magenta('[Debug]'))
logger.addPrefix('success', colors.green('[Success]'))
logger.addPrefix('warn', colors.yellow('[Warn]'))
logger.addPrefix('error', colors.red('[Error]'))
@streams =
build: logger.createLogStream('build'),
info: logger.createLogStream('info'),
debug: logger.createLogStream('debug'),
success: logger.createLogStream('success'),
warn: logger.createLogStream('warn'),
error: logger.createLogStream('error')
_.mapKeys @streams, (stream, key) ->
if key isnt 'debug'
stream.pipe(process.stdout)
else
stream.pipe(process.stdout) if process.env.DEBUG?
@formatMessage = logger.formatWithPrefix.bind(logger)
logInfo: (msg) ->
@streams.info.write(msg + eol)
logDebug: (msg) ->
@streams.debug.write(msg + eol)
logSuccess: (msg) ->
@streams.success.write(msg + eol)
logWarn: (msg) ->
@streams.warn.write(msg + eol)
logError: (msg) ->
@streams.error.write(msg + eol)

64
lib/utils/logger.ts Normal file
View File

@ -0,0 +1,64 @@
import { EOL as eol } from 'os';
import _ = require('lodash');
import chalk from 'chalk';
import { StreamLogger } from 'resin-stream-logger';
export class Logger {
public streams: {
build: NodeJS.ReadWriteStream;
info: NodeJS.ReadWriteStream;
debug: NodeJS.ReadWriteStream;
success: NodeJS.ReadWriteStream;
warn: NodeJS.ReadWriteStream;
error: NodeJS.ReadWriteStream;
};
public formatMessage: (name: string, message: string) => string;
constructor() {
const logger = new StreamLogger();
logger.addPrefix('build', chalk.blue('[Build]'));
logger.addPrefix('info', chalk.cyan('[Info]'));
logger.addPrefix('debug', chalk.magenta('[Debug]'));
logger.addPrefix('success', chalk.green('[Success]'));
logger.addPrefix('warn', chalk.yellow('[Warn]'));
logger.addPrefix('error', chalk.red('[Error]'));
this.streams = {
build: logger.createLogStream('build'),
info: logger.createLogStream('info'),
debug: logger.createLogStream('debug'),
success: logger.createLogStream('success'),
warn: logger.createLogStream('warn'),
error: logger.createLogStream('error'),
};
_.forEach(this.streams, function(stream, key) {
if (key !== 'debug' || process.env.DEBUG) {
stream.pipe(process.stdout);
}
});
this.formatMessage = logger.formatWithPrefix.bind(logger);
}
logInfo(msg: string) {
return this.streams.info.write(msg + eol);
}
logDebug(msg: string) {
return this.streams.debug.write(msg + eol);
}
logSuccess(msg: string) {
return this.streams.success.write(msg + eol);
}
logWarn(msg: string) {
return this.streams.warn.write(msg + eol);
}
logError(msg: string) {
return this.streams.error.write(msg + eol);
}
}

View File

@ -1,22 +0,0 @@
exports.reachingOut = '''
If you need help, or just want to say hi, don't hesitate in reaching out at:
GitHub: https://github.com/resin-io/resin-cli/issues/new
Forums: https://forums.resin.io
'''
exports.getHelp = '''
If you need help, don't hesitate in contacting us at:
GitHub: https://github.com/resin-io/resin-cli/issues/new
Forums: https://forums.resin.io
'''
exports.resinAsciiArt = '''
______ _ _
| ___ \\ (_) (_)
| |_/ /___ ___ _ _ __ _ ___
| // _ \\/ __| | '_ \\ | |/ _ \\
| |\\ \\ __/\\__ \\ | | | |_| | (_) |
\\_| \\_\\___||___/_|_| |_(_)_|\\___/
'''

22
lib/utils/messages.ts Normal file
View File

@ -0,0 +1,22 @@
export const reachingOut = `\
If you need help, or just want to say hi, don't hesitate in reaching out at:
GitHub: https://github.com/resin-io/resin-cli/issues/new
Forums: https://forums.resin.io\
`;
export const getHelp = `\
If you need help, don't hesitate in contacting us at:
GitHub: https://github.com/resin-io/resin-cli/issues/new
Forums: https://forums.resin.io\
`;
export const resinAsciiArt = `\
______ _ _
| ___ \\ (_) (_)
| |_/ /___ ___ _ _ __ _ ___
| // _ \\/ __| | '_ \\ | |/ _ \\
| |\\ \\ __/\\__ \\ | | | |_| | (_) |
\\_| \\_\\___||___/_|_| |_(_)_|\\___/\
`;

View File

@ -1,181 +0,0 @@
###
Copyright 2016-2017 Resin.io
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.
###
_ = require('lodash')
Promise = require('bluebird')
form = require('resin-cli-form')
visuals = require('resin-cli-visuals')
resin = require('resin-sdk-preconfigured')
chalk = require('chalk')
validation = require('./validation')
messages = require('./messages')
exports.authenticate = (options) ->
return form.run [
message: 'Email:'
name: 'email'
type: 'input'
validate: validation.validateEmail
,
message: 'Password:'
name: 'password'
type: 'password'
],
override: options
.then(resin.auth.login)
.then(resin.auth.twoFactor.isPassed)
.then (isTwoFactorAuthPassed) ->
return if isTwoFactorAuthPassed
return form.ask
message: 'Two factor auth challenge:'
name: 'code'
type: 'input'
.then(resin.auth.twoFactor.challenge)
.catch (error) ->
resin.auth.logout().then ->
if error.name is 'ResinRequestError' and error.statusCode is 401
throw new Error('Invalid two factor authentication code')
throw error
exports.askLoginType = ->
return form.ask
message: 'How would you like to login?'
name: 'loginType'
type: 'list'
choices: [
name: 'Web authorization (recommended)'
value: 'web'
,
name: 'Credentials'
value: 'credentials'
,
name: 'Authentication token'
value: 'token'
,
name: 'I don\'t have a Resin account!'
value: 'register'
]
exports.selectDeviceType = ->
resin.models.device.getSupportedDeviceTypes().then (deviceTypes) ->
return form.ask
message: 'Device Type'
type: 'list'
choices: deviceTypes
exports.confirm = (yesOption, message, yesMessage) ->
Promise.try ->
if yesOption
console.log(yesMessage) if yesMessage
return true
return form.ask
message: message
type: 'confirm'
default: false
.then (confirmed) ->
if not confirmed
throw new Error('Aborted')
exports.selectApplication = (filter) ->
resin.models.application.hasAny().then (hasAnyApplications) ->
if not hasAnyApplications
throw new Error('You don\'t have any applications')
return resin.models.application.getAll()
.filter(filter or _.constant(true))
.then (applications) ->
return form.ask
message: 'Select an application'
type: 'list'
choices: _.map applications, (application) ->
return {
name: "#{application.app_name} (#{application.device_type})"
value: application.app_name
}
exports.selectOrCreateApplication = ->
resin.models.application.hasAny().then (hasAnyApplications) ->
return if not hasAnyApplications
resin.models.application.getAll().then (applications) ->
applications = _.map applications, (application) ->
return {
name: "#{application.app_name} (#{application.device_type})"
value: application.app_name
}
applications.unshift
name: 'Create a new application'
value: null
return form.ask
message: 'Select an application'
type: 'list'
choices: applications
.then (application) ->
return application if application?
form.ask
message: 'Choose a Name for your new application'
type: 'input'
validate: validation.validateApplicationName
exports.awaitDevice = (uuid) ->
resin.models.device.getName(uuid).then (deviceName) ->
spinner = new visuals.Spinner("Waiting for #{deviceName} to come online")
poll = ->
resin.models.device.isOnline(uuid).then (isOnline) ->
if isOnline
spinner.stop()
console.info("The device **#{deviceName}** is online!")
return
else
# Spinner implementation is smart enough to
# not start again if it was already started
spinner.start()
return Promise.delay(3000).then(poll)
console.info("Waiting for #{deviceName} to connect to resin...")
poll().return(uuid)
exports.inferOrSelectDevice = (preferredUuid) ->
resin.models.device.getAll()
.filter (device) ->
device.is_online
.then (onlineDevices) ->
if _.isEmpty(onlineDevices)
throw new Error('You don\'t have any devices online')
return form.ask
message: 'Select a device'
type: 'list'
default: if preferredUuid in _.map(onlineDevices, 'uuid') then preferredUuid else onlineDevices[0].uuid
choices: _.map onlineDevices, (device) ->
return {
name: "#{device.name or 'Untitled'} (#{device.uuid.slice(0, 7)})"
value: device.uuid
}
exports.printErrorMessage = (message) ->
console.error(chalk.red(message))
console.error(chalk.red("\n#{messages.getHelp}\n"))
exports.expectedError = (message) ->
if message instanceof Error
message = message.message
exports.printErrorMessage(message)
process.exit(1)

234
lib/utils/patterns.ts Normal file
View File

@ -0,0 +1,234 @@
/*
Copyright 2016-2017 Resin.io
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 _ = require('lodash');
import Promise = require('bluebird');
import form = require('resin-cli-form');
import visuals = require('resin-cli-visuals');
import ResinSdk = require('resin-sdk');
import chalk from 'chalk';
import validation = require('./validation');
import messages = require('./messages');
const resin = ResinSdk.fromSharedOptions();
export function authenticate(options: {}): Promise<void> {
return form.run([{
message: 'Email:',
name: 'email',
type: 'input',
validate: validation.validateEmail,
}, {
message: 'Password:',
name: 'password',
type: 'password',
}], { override: options })
.then(resin.auth.login)
.then(resin.auth.twoFactor.isPassed)
.then((isTwoFactorAuthPassed: boolean) => {
if (isTwoFactorAuthPassed) { return; }
return form.ask({
message: 'Two factor auth challenge:',
name: 'code',
type: 'input',
})
.then(resin.auth.twoFactor.challenge)
.catch((error: any) => {
return resin.auth.logout().then(() => {
if ((error.name === 'ResinRequestError') && (error.statusCode === 401)) {
throw new Error('Invalid two factor authentication code');
}
throw error;
});
});
});
}
export function askLoginType() {
return form.ask({
message: 'How would you like to login?',
name: 'loginType',
type: 'list',
choices: [{
name: 'Web authorization (recommended)',
value: 'web',
}, {
name: 'Credentials',
value: 'credentials',
}, {
name: 'Authentication token',
value: 'token',
}, {
name: 'I don\'t have a Resin account!',
value: 'register',
}],
});
}
export function selectDeviceType() {
return resin.models.device.getSupportedDeviceTypes()
.then(deviceTypes => {
return form.ask({
message: 'Device Type',
type: 'list',
choices: deviceTypes,
});
});
}
export function confirm(yesOption: string, message: string, yesMessage: string) {
return Promise.try(function () {
if (yesOption) {
if (yesMessage) { console.log(yesMessage); }
return true;
}
return form.ask({
message,
type: 'confirm',
default: false,
});
}).then(function(confirmed) {
if (!confirmed) {
throw new Error('Aborted');
}
});
}
export function selectApplication(filter: (app: ResinSdk.Application) => boolean) {
resin.models.application.hasAny().then(function(hasAnyApplications) {
if (!hasAnyApplications) {
throw new Error('You don\'t have any applications');
}
return resin.models.application.getAll();
})
.filter(filter || _.constant(true))
.then(applications => {
return form.ask({
message: 'Select an application',
type: 'list',
choices: _.map(applications, application =>
({
name: `${application.app_name} (${application.device_type})`,
value: application.app_name,
}),
)});
});
}
export function selectOrCreateApplication() {
return resin.models.application.hasAny().then((hasAnyApplications) => {
if (!hasAnyApplications) return;
return resin.models.application.getAll().then((applications) => {
const appOptions = _.map<
ResinSdk.Application,
{ name: string, value: string | null }
>(applications, application => ({
name: `${application.app_name} (${application.device_type})`,
value: application.app_name,
}));
appOptions.unshift({
name: 'Create a new application',
value: null,
});
return form.ask({
message: 'Select an application',
type: 'list',
choices: appOptions,
});
});
}).then((application) => {
if (application) {
return application;
}
return form.ask({
message: 'Choose a Name for your new application',
type: 'input',
validate: validation.validateApplicationName,
});
});
}
export function awaitDevice(uuid: string) {
return resin.models.device.getName(uuid)
.then((deviceName) => {
const spinner = new visuals.Spinner(`Waiting for ${deviceName} to come online`);
const poll = (): Promise<void> => {
return resin.models.device.isOnline(uuid)
.then(function(isOnline) {
if (isOnline) {
spinner.stop();
console.info(`The device **${deviceName}** is online!`);
return;
} else {
// Spinner implementation is smart enough to
// not start again if it was already started
spinner.start();
return Promise.delay(3000).then(poll);
}
});
};
console.info(`Waiting for ${deviceName} to connect to resin...`);
return poll().return(uuid);
});
}
export function inferOrSelectDevice(preferredUuid: string) {
return resin.models.device.getAll()
.filter<ResinSdk.Device>((device) => device.is_online)
.then((onlineDevices) => {
if (_.isEmpty(onlineDevices)) {
throw new Error('You don\'t have any devices online');
}
const defaultUuid = _.map(onlineDevices, 'uuid').includes(preferredUuid) ?
preferredUuid :
onlineDevices[0].uuid;
return form.ask({
message: 'Select a device',
type: 'list',
default: defaultUuid,
choices: _.map(onlineDevices, device => ({
name: `${device.name || 'Untitled'} (${device.uuid.slice(0, 7)})`,
value: device.uuid,
}),
)});
});
}
export function printErrorMessage(message: string) {
console.error(chalk.red(message));
console.error(chalk.red(`\n${messages.getHelp}\n`));
}
export function expectedError(message: string | Error) {
if (message instanceof Error) {
({ message } = message);
}
printErrorMessage(message);
process.exit(1);
}

View File

@ -1,4 +1,4 @@
###
/*
Copyright 2016-2017 Resin.io
Licensed under the Apache License, Version 2.0 (the "License");
@ -12,18 +12,22 @@ 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.
###
*/
nplugm = require('nplugm')
_ = require('lodash')
capitano = require('capitano')
patterns = require('./patterns')
import nplugm = require('nplugm');
import _ = require('lodash');
import capitano = require('capitano');
import patterns = require('./patterns');
exports.register = (regex) ->
nplugm.list(regex).map (plugin) ->
command = require(plugin)
command.plugin = true
return capitano.command(command) if not _.isArray(command)
return _.each(command, capitano.command)
.catch (error) ->
patterns.printErrorMessage(error.message)
export function register(regex: RegExp): Promise<void> {
return nplugm.list(regex).map(async function(plugin: any) {
const command = await import(plugin);
command.plugin = true;
if (!_.isArray(command)) {
return capitano.command(command);
}
return _.each(command, capitano.command);
}).catch((error: Error) => {
return patterns.printErrorMessage(error.message);
});
}

View File

@ -1,17 +0,0 @@
exports.buffer = (stream, bufferFile) ->
Promise = require('bluebird')
fs = require('fs')
fileWriteStream = fs.createWriteStream(bufferFile)
new Promise (resolve, reject) ->
stream
.on('error', reject)
.on('end', resolve)
.pipe(fileWriteStream)
.then ->
new Promise (resolve, reject) ->
fs.createReadStream(bufferFile)
.on 'open', ->
resolve(this)
.on('error', reject)

19
lib/utils/streams.ts Normal file
View File

@ -0,0 +1,19 @@
export async function buffer(stream: NodeJS.ReadableStream, bufferFile: string) {
const Promise = await import('bluebird');
const fs = await import('fs');
const fileWriteStream = fs.createWriteStream(bufferFile);
return new Promise(function(resolve, reject) {
stream
.on('error', reject)
.on('end', resolve)
.pipe(fileWriteStream);
}).then(() => new Promise(function(resolve, reject) {
const stream = fs.createReadStream(bufferFile);
stream
.on('open', () => resolve(stream))
.on('error', reject);
}));
}

View File

@ -1,40 +0,0 @@
###
Copyright 2016-2017 Resin.io
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.
###
updateNotifier = require('update-notifier')
isRoot = require('is-root')
packageJSON = require('../../package.json')
# Check for an update once a day. 1 day granularity should be
# enough, rather than every run.
resinUpdateInterval = 1000 * 60 * 60 * 24 * 1
# `update-notifier` creates files to make the next
# running time ask for updated, however this can lead
# to ugly EPERM issues if those files are created as root.
if not isRoot()
notifier = updateNotifier
pkg: packageJSON
updateCheckInterval: resinUpdateInterval
exports.hasAvailableUpdate = ->
return notifier?
exports.notify = ->
return if not exports.hasAvailableUpdate()
notifier.notify(defer: false)
if notifier.update?
console.log('Notice that you might need administrator privileges depending on your setup\n')

47
lib/utils/update.ts Normal file
View File

@ -0,0 +1,47 @@
/*
Copyright 2016-2017 Resin.io
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 UpdateNotifier from 'update-notifier';
import isRoot = require('is-root');
import packageJSON = require('../../package.json');
// Check for an update once a day. 1 day granularity should be
// enough, rather than every run.
const resinUpdateInterval = 1000 * 60 * 60 * 24 * 1;
let notifier: UpdateNotifier.UpdateNotifier;
// `update-notifier` creates files to make the next
// running time ask for updated, however this can lead
// to ugly EPERM issues if those files are created as root.
if (!isRoot()) {
notifier = UpdateNotifier({
pkg: packageJSON,
updateCheckInterval: resinUpdateInterval,
});
}
export function notify() {
if (!notifier) {
return;
}
notifier.notify({ defer: false });
if (notifier.update != null) {
console.log('Notice that you might need administrator privileges depending on your setup\n');
}
}

View File

@ -1,4 +1,4 @@
###
/*
Copyright 2016-2017 Resin.io
Licensed under the Apache License, Version 2.0 (the "License");
@ -12,24 +12,30 @@ 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.
###
*/
validEmail = require('@resin.io/valid-email')
import validEmail = require('@resin.io/valid-email');
exports.validateEmail = (input) ->
if not validEmail(input)
return 'Email is not valid'
export function validateEmail(input: string) {
if (!validEmail(input)) {
return 'Email is not valid';
}
return true
return true;
}
exports.validatePassword = (input) ->
if input.length < 8
return 'Password should be 8 characters long'
export function validatePassword(input: string) {
if (input.length < 8) {
return 'Password should be 8 characters long';
}
return true
return true;
}
exports.validateApplicationName = (input) ->
if input.length < 4
return 'The application name should be at least 4 characters'
export function validateApplicationName(input: string) {
if (input.length < 4) {
return 'The application name should be at least 4 characters';
}
return true
return true;
}

View File

@ -1,6 +1,6 @@
{
"name": "resin-cli",
"version": "6.12.2",
"version": "6.12.3",
"description": "The official resin.io CLI tool",
"main": "./build/actions/index.js",
"homepage": "https://github.com/resin-io/resin-cli",
@ -31,15 +31,15 @@
"scripts": {
"prebuild": "rimraf build/ build-bin/ build-zip/",
"build": "npm run build:src && npm run build:bin",
"build:src": "gulp build && tsc && npm run doc",
"build:src": "npm run lint && gulp build && tsc && npm run build:doc",
"build:doc": "mkdirp doc/ && ts-node extras/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",
"pretest": "npm run build",
"test": "gulp test",
"ci": "npm run test && catch-uncommitted",
"doc": "mkdirp doc/ && coffee extras/capitanodoc/index.coffee > doc/cli.markdown",
"watch": "gulp watch",
"lint": "gulp lint",
"lint": "resin-lint lib/ tests/ && resin-lint --typescript automation/ extras/ lib/ typings/ tests/",
"prepublish": "require-npm4-to-publish",
"prepublishOnly": "npm run build"
},
@ -54,15 +54,18 @@
},
"devDependencies": {
"@types/archiver": "^2.0.1",
"@types/bluebird": "^3.5.19",
"@types/fs-extra": "^5.0.0",
"@types/is-root": "^1.0.0",
"@types/mkdirp": "^0.5.2",
"@types/prettyjson": "0.0.28",
"@types/raven": "^2.1.2",
"catch-uncommitted": "^1.0.0",
"ent": "^2.2.0",
"filehound": "^1.16.2",
"fs-extra": "^5.0.0",
"gulp": "^3.9.0",
"gulp-coffee": "^2.2.0",
"gulp-coffeelint": "^0.6.0",
"gulp-inline-source": "^2.1.0",
"gulp-mocha": "^2.0.0",
"gulp-shell": "^0.5.2",
@ -70,8 +73,9 @@
"pkg": "^4.3.0-beta.1",
"publish-release": "^1.3.3",
"require-npm4-to-publish": "^1.0.0",
"resin-lint": "^1.5.0",
"ts-node": "^4.0.1",
"typescript": "^2.6.1"
"typescript": "2.4.0"
},
"dependencies": {
"@resin.io/valid-email": "^0.1.0",
@ -82,7 +86,7 @@
"bluebird": "^3.3.3",
"body-parser": "^1.14.1",
"capitano": "^1.7.0",
"chalk": "^1.1.3",
"chalk": "^2.3.0",
"coffee-script": "^1.12.6",
"columnify": "^1.5.2",
"denymount": "^2.2.0",
@ -129,7 +133,7 @@
"resin-sdk": "^7.0.0",
"resin-sdk-preconfigured": "^6.9.0",
"resin-settings-client": "^3.6.1",
"resin-stream-logger": "^0.0.4",
"resin-stream-logger": "^0.1.0",
"resin-sync": "^9.2.3",
"rimraf": "^2.4.3",
"rindle": "^1.0.0",

View File

@ -8,9 +8,20 @@
"noUnusedParameters": true,
"preserveConstEnums": true,
"removeComments": true,
"sourceMap": true
"sourceMap": true,
"lib": [
// es5 defaults:
"dom",
"es5",
"scripthost",
// some specific es6 bits we're sure are safe:
"es2015.collection",
"es2015.iterable",
"es2016.array.include"
]
},
"include": [
"./typings/*.d.ts",
"./lib/**/*.ts"
]
}

1
typings/@resin-valid-email.d.ts vendored Normal file
View File

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

53
typings/capitano.d.ts vendored Normal file
View File

@ -0,0 +1,53 @@
declare module 'capitano' {
export function parse(argv: string[]): Cli;
export interface Cli {
command: string;
options: {};
global: {};
}
export interface OptionDefinition {
signature: string;
description: string;
parameter?: string;
boolean?: boolean;
alias?: string | string[];
}
export interface CommandDefinition<P = {}, O = {}> {
signature: string;
description: string;
help: string;
options?: OptionDefinition[];
permission?: 'user';
action(params: P, options: O, done: () => void): void;
}
export interface Command {
signature: Signature;
options: Option[];
isWildcard(): boolean;
}
export interface Signature {
hasParameters(): boolean;
hasVariadicParameters(): boolean;
isWildcard(): boolean;
allowsStdin(): boolean;
}
export interface Option {
signature: Signature;
alias: string | string[];
boolean: boolean;
parameter: string;
required: boolean | string;
}
export function command(command: CommandDefinition): void;
export const state: {
getMatchCommand: (signature: string, callback: (e: Error, cmd: Command) => void) => void
};
}

1
typings/mixpanel.d.ts vendored Normal file
View File

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

4
typings/nplugm.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
declare module 'nplugm' {
import Promise = require('bluebird');
export function list(regexp: RegExp): Promise<Array<string>>;
}

4
typings/package.json.d.ts vendored Normal file
View File

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

3
typings/president.d.ts vendored Normal file
View File

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

1
typings/resin-cli-errors.d.ts vendored Normal file
View File

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

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

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

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

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

1
typings/resin-device-config.d.ts vendored Normal file
View File

@ -0,0 +1 @@
declare module 'resin-device-config';

60
typings/resin-device-init.d.ts vendored Normal file
View File

@ -0,0 +1,60 @@
declare module 'resin-device-init' {
import * as Promise from 'bluebird';
import { EventEmitter } from 'events';
interface OperationState {
operation: CopyOperation | ReplaceOperation | RunScriptOperation | BurnOperation;
percentage: number;
}
interface Operation {
command: string;
}
interface CopyOperation extends Operation {
command: 'copy';
from: { path: string };
to: { path: string };
}
interface ReplaceOperation extends Operation {
command: 'replace';
copy: string;
replace: string;
file: {
path: string;
};
}
interface RunScriptOperation extends Operation {
command: 'run-script';
script: string;
arguments?: string[];
}
interface BurnOperation extends Operation {
command: 'burn';
image?: string;
}
interface BurnProgress {
type: 'write' | 'check';
percentage: number;
transferred: number;
length: number;
remaining: number;
eta: number;
runtime: number;
delta: number;
speed: number;
}
interface InitializeEmitter {
on(event: 'stdout', callback: (msg: string) => void): void;
on(event: 'stderr', callback: (msg: string) => void): void;
on(event: 'state', callback: (state: OperationState) => void): void;
on(event: 'burn', callback: (state: BurnProgress) => void): void;
}
export function initialize(image: string, deviceType: string, config: {}): Promise<InitializeEmitter>;
}

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

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

5
typings/resin-sdk-preconfigured.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
declare module 'resin-sdk-preconfigured' {
import { ResinSDK } from 'resin-sdk';
let sdk: ResinSDK;
export = sdk;
}

5
typings/resin-sync.d.ts vendored Normal file
View File

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

13
typings/rindle.d.ts vendored Normal file
View File

@ -0,0 +1,13 @@
declare module 'rindle' {
export function extract(
stream: NodeJS.ReadableStream,
callback: (error: Error, data: string) => void,
): void;
export function wait(
stream: {
on(event: string, callback: Function): void;
},
callback: (error: Error, data: string) => void,
): void;
}

53
typings/update-notifier.d.ts vendored Normal file
View File

@ -0,0 +1,53 @@
// Based on the official types at https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/update-notifier/index.d.ts
// but fixed to handle options correctly
declare module 'update-notifier' {
export = UpdateNotifier;
function UpdateNotifier(settings?: UpdateNotifier.Settings): UpdateNotifier.UpdateNotifier;
namespace UpdateNotifier {
class UpdateNotifier {
constructor(settings?: Settings);
update: UpdateInfo;
check(): void;
checkNpm(): void;
notify(customMessage?: NotifyOptions): void;
}
interface Settings {
pkg?: Package;
callback?(update?: UpdateInfo): any;
packageName?: string;
packageVersion?: string;
updateCheckInterval?: number; // in milliseconds, default 1000 * 60 * 60 * 24 (1 day)
}
interface BoxenOptions {
padding: number;
margin: number;
align: string;
borderColor: string;
borderStyle: string;
}
interface NotifyOptions {
message?: string;
defer?: boolean;
boxenOpts?: BoxenOptions;
}
interface Package {
name: string;
version: string;
}
interface UpdateInfo {
latest: string;
current: string;
type: string;
name: string;
}
}
}