Convert most of utils to TypeScript

Change-Type: patch
This commit is contained in:
Tim Perry 2017-12-20 22:46:01 +01:00
parent 4b511c47f0
commit ffffd447f2
36 changed files with 965 additions and 642 deletions

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

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

@ -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 { Command } from "capitano";
export const version: Command = {
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

@ -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 { Command } from "capitano";
Examples:
export const list: Command = {
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,6 @@ 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')
export = require('resin-sync').capitano('resin-cli');

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

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

@ -0,0 +1,109 @@
/*
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'),
pubNubKeys: resin.models.config.getAll().get('pubnub'),
mixpanelToken: resin.models.config.getAll().get('mixpanelToken')
}).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.pubNubKeys,
mixpanel: {
token: results.mixpanelToken
}
}, options);
});
};
export function generateApplicationConfig(application: ResinSdk.Application, options: {}) {
return generateBaseConfig(application, options)
.tap(config => authenticateWithApplicationKey(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 != null) {
return authenticateWithDeviceKey(config, device.uuid, deviceApiKey);
} else {
return 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;
});
};
function authenticateWithApplicationKey(config: any, applicationNameOrId: string | number) {
return resin.models.application.generateApiKey(applicationNameOrId)
.tap((apiKey) => {
config.apiKey = apiKey;
});
};
function authenticateWithDeviceKey(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 ]
}

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

@ -0,0 +1,146 @@
/*
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 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);
const presidentExecuteAsync = Promise.promisify(execute);
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(Promise.promisify(rindle.extract))
.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; }
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 Promise.promisify(rindle.wait)(step);
};
export function getArchAndDeviceType(applicationName: string): Promise<{ arch: string, device_type: string }> {
return Promise.join(
getApplication(applicationName),
resin.models.config.getDeviceTypes(),
function (app, deviceTypes) {
let config = _.find(deviceTypes, { 'slug': app.device_type });
if (config == null) {
throw new Error('Could not read application information!');
}
return { device_type: app.device_type, arch: config.arch };
}
);
};
function getApplication(applicationName: string) {
let match;
// 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(applicationName)) {
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)

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

@ -0,0 +1,66 @@
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')
};
_.mapKeys(this.streams, function(stream, key) {
if (key !== 'debug') {
return stream.pipe(process.stdout);
} else {
if (process.env.DEBUG != null) { return 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,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)

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

@ -0,0 +1,228 @@
/*
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: {}) {
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) =>
resin.auth.logout().then(function() {
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) => {
let appOptions: { name: string, value: string | null }[];
appOptions = _.map(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 != null) 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((device: ResinSdk.Device) => device.is_online)
.then((onlineDevices: ResinSdk.Device[]) => {
if (_.isEmpty(onlineDevices)) {
throw new Error('You don\'t have any devices online');
}
let defaultUuid = Array.from(_.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)
exports.register = (regex: RegExp) => {
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)

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

@ -0,0 +1,20 @@
import { ReadStream } from 'fs';
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) {
return stream
.on('error', reject)
.on('end', resolve)
.pipe(fileWriteStream);
}).then(() => new Promise(function(resolve, reject) {
fs.createReadStream(bufferFile)
.on('open', function(this: ReadStream) {
resolve(this);
}).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')

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

@ -0,0 +1,51 @@
/*
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 hasAvailableUpdate() {
return notifier != null;
}
export function notify() {
if (!exports.hasAvailableUpdate()) {
return;
}
notifier.notify({ defer: false });
if (notifier.update != null) {
return console.log('Notice that you might need administrator privileges depending on your setup\n');
}
};

View File

@ -16,7 +16,7 @@ limitations under the License.
import validEmail = require('@resin.io/valid-email');
exports.validateEmail = function(input: string) {
export function validateEmail(input: string) {
if (!validEmail(input)) {
return 'Email is not valid';
}
@ -24,7 +24,7 @@ exports.validateEmail = function(input: string) {
return true;
};
exports.validatePassword = function(input: string) {
export function validatePassword(input: string) {
if (input.length < 8) {
return 'Password should be 8 characters long';
}
@ -32,7 +32,7 @@ exports.validatePassword = function(input: string) {
return true;
};
exports.validateApplicationName = function(input: string) {
export function validateApplicationName(input: string) {
if (input.length < 4) {
return 'The application name should be at least 4 characters';
}

View File

@ -54,8 +54,12 @@
},
"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",
@ -71,7 +75,7 @@
"publish-release": "^1.3.3",
"require-npm4-to-publish": "^1.0.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,7 +8,17 @@
"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",

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

@ -0,0 +1,36 @@
declare module 'capitano' {
export function parse(argv: string[]): Cli;
export interface Cli {
command: string;
options: {};
global: {};
}
export interface CommandOption {
signature: string;
description: string;
parameter?: string;
boolean?: boolean;
alias?: string | string[];
}
export interface Command<P = {}, O = {}> {
signature: string;
description: string;
help: string;
options?: CommandOption[],
permission?: 'user',
action(params: P, options: O, done: () => void): void;
}
export interface BuiltCommand {
signature: {}
}
export function command(command: Command): void;
export const state: {
getMatchCommand: (signature: string, callback: (e: Error, cmd: BuiltCommand) => 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-device-config.d.ts vendored Normal file
View File

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

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

@ -0,0 +1,59 @@
declare module 'resin-device-init' {
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-preconfiguredasd' {
import { ResinSDK } from 'resin-sdk';
let sdk: ResinSDK;
export = sdk;
}

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;
}
}
}