diff --git a/build/actions/app.js b/build/actions/app.js index 65a58271..cf042423 100644 --- a/build/actions/app.js +++ b/build/actions/app.js @@ -1,5 +1,5 @@ (function() { - var _, commandOptions, events, helpers, resin, vcs, visuals; + var _, commandOptions, events, helpers, patterns, resin, vcs, visuals; _ = require('lodash'); @@ -15,6 +15,8 @@ helpers = require('../utils/helpers'); + patterns = require('../utils/patterns'); + exports.create = { signature: 'app create ', description: 'create an application', @@ -33,7 +35,7 @@ if (hasApplication) { throw new Error('You already have an application with that name!'); } - }).then(helpers.selectDeviceType).then(function(deviceType) { + }).then(patterns.selectDeviceType).then(function(deviceType) { return resin.models.application.create(params.name, deviceType); }).then(function(application) { console.info("Application created: " + application.app_name + " (" + application.device_type + ", id " + application.id + ")"); @@ -88,7 +90,7 @@ options: [commandOptions.yes], permission: 'user', action: function(params, options, done) { - return helpers.confirm(options.yes, 'Are you sure you want to delete the application?').then(function() { + return patterns.confirm(options.yes, 'Are you sure you want to delete the application?').then(function() { return resin.models.application.remove(params.name); }).tap(function() { return resin.models.application.get(params.name).then(function(application) { @@ -117,7 +119,7 @@ }).then(function() { var message; message = "Are you sure you want to associate " + currentDirectory + " with " + params.name + "?"; - return helpers.confirm(options.yes, message); + return patterns.confirm(options.yes, message); }).then(function() { return resin.models.application.get(params.name).get('git_repository').then(function(gitRepository) { return vcs.initialize(currentDirectory).then(function() { diff --git a/build/actions/device.js b/build/actions/device.js index 4ae341a6..365b98ed 100644 --- a/build/actions/device.js +++ b/build/actions/device.js @@ -1,5 +1,5 @@ (function() { - var Promise, _, async, capitano, commandOptions, deviceConfig, events, form, helpers, htmlToText, image, inject, manager, pine, registerDevice, resin, vcs, visuals; + var Promise, _, capitano, commandOptions, events, form, fs, helpers, init, patterns, resin, rimraf, stepHandler, umount, vcs, visuals; Promise = require('bluebird'); @@ -7,31 +7,25 @@ _ = require('lodash'); - async = require('async'); - resin = require('resin-sdk'); visuals = require('resin-cli-visuals'); vcs = require('resin-vcs'); - manager = require('resin-image-manager'); - - image = require('resin-image'); - - inject = require('resin-config-inject'); - - registerDevice = require('resin-register-device'); - - pine = require('resin-pine'); - - deviceConfig = require('resin-device-config'); - form = require('resin-cli-form'); events = require('resin-cli-events'); - htmlToText = require('html-to-text'); + init = require('resin-device-init'); + + fs = Promise.promisifyAll(require('fs')); + + rimraf = Promise.promisify(require('rimraf')); + + umount = Promise.promisifyAll(require('umount')); + + patterns = require('../utils/patterns'); helpers = require('../utils/helpers'); @@ -80,7 +74,7 @@ options: [commandOptions.yes], permission: 'user', action: function(params, options, done) { - return helpers.confirm(options.yes, 'Are you sure you want to delete the device?').then(function() { + return patterns.confirm(options.yes, 'Are you sure you want to delete the device?').then(function() { return resin.models.device.remove(params.uuid); }).tap(function() { return events.send('device.delete', { @@ -122,155 +116,83 @@ } }; + stepHandler = function(step) { + var bar; + step.on('stdout', _.bind(process.stdout.write, process.stdout)); + step.on('stderr', _.bind(process.stderr.write, process.stderr)); + step.on('state', function(state) { + if (state.operation.command === 'burn') { + return; + } + return console.log(helpers.stateToString(state)); + }); + bar = new visuals.Progress('Writing Device OS'); + step.on('burn', _.bind(bar.update, bar)); + return new Promise(function(resolve, reject) { + step.on('error', reject); + return step.on('end', resolve); + }); + }; + exports.init = { - signature: 'device init [device]', + signature: 'device init', description: 'initialise a device with resin os', - help: 'Use this command to download the OS image of a certain application and write it to an SD Card.\n\nNote that this command requires admin privileges.\n\nIf `device` is omitted, you will be prompted to select a device interactively.\n\nNotice this command asks for confirmation interactively.\nYou can avoid this by passing the `--yes` boolean option.\n\nYou can quiet the progress bar and other logging information by passing the `--quiet` boolean option.\n\nYou need to configure the network type and other settings:\n\nEthernet:\n You can setup the device OS to use ethernet by setting the `--network` option to "ethernet".\n\nWifi:\n You can setup the device OS to use wifi by setting the `--network` option to "wifi".\n If you set "network" to "wifi", you will need to specify the `--ssid` and `--key` option as well.\n\nYou can omit network related options to be asked about them interactively.\n\nExamples:\n\n $ resin device init\n $ resin device init --application MyApp\n $ resin device init --application MyApp --network ethernet\n $ resin device init /dev/disk2 --application MyApp --network wifi --ssid MyNetwork --key secret', - options: [commandOptions.optionalApplication, commandOptions.network, commandOptions.wifiSsid, commandOptions.wifiKey], + help: 'Use this command to download the OS image of a certain application and write it to an SD Card.\n\nNotice this command may ask for confirmation interactively.\nYou can avoid this by passing the `--yes` boolean option.\n\nExamples:\n\n $ resin device init\n $ resin device init --application MyApp', + options: [commandOptions.optionalApplication, commandOptions.yes], permission: 'user', root: true, action: function(params, options, done) { - var networkOptions; - networkOptions = { - network: options.network, - wifiSsid: options.ssid, - wifiKey: options.key - }; - return async.waterfall([ - function(callback) { - if (options.application != null) { - return callback(null, options.application); - } - return vcs.getApplicationName(process.cwd()).nodeify(callback); - }, function(applicationName, callback) { - options.application = applicationName; - return resin.models.application.has(options.application).nodeify(callback); - }, function(hasApplication, callback) { - if (!hasApplication) { - return callback(new Error("Invalid application: " + options.application)); - } - if (params.device != null) { - return callback(null, params.device); - } - return form.ask({ - type: 'drive', - message: 'Select a drive' - }).nodeify(callback); - }, function(device, callback) { - var message; - params.device = device; - message = "This will completely erase " + params.device + ". Are you sure you want to continue?"; - if (options.yes) { - return callback(null, true); - } else { - return form.ask({ - message: message, - type: 'confirm', - "default": false - }).nodeify(callback); - } - }, function(confirmed, callback) { - if (!confirmed) { - return done(); - } - if (networkOptions.network != null) { - return callback(); - } - return form.run([ - { - message: 'Network Type', - name: 'network', - type: 'list', - choices: ['ethernet', 'wifi'] - }, { - message: 'Wifi Ssid', - name: 'wifiSsid', - type: 'input', - when: { - network: 'wifi' - } - }, { - message: 'Wifi Key', - name: 'wifiKey', - type: 'input', - when: { - network: 'wifi' - } - } - ]).then(function(parameters) { - return _.extend(networkOptions, parameters); - }).nodeify(callback); - }, function(callback) { - console.info("Checking application: " + options.application); - return resin.models.application.get(options.application).nodeify(callback); - }, function(application, callback) { - return async.parallel({ - manifest: function(callback) { - console.info('Getting device manifest for the application'); - return resin.models.device.getManifestBySlug(application.device_type).nodeify(callback); - }, - config: function(callback) { - console.info('Fetching application configuration'); - return deviceConfig.get(options.application, networkOptions).nodeify(callback); - } - }, callback); - }, function(results, callback) { - params.manifest = results.manifest; - console.info('Associating the device'); - return registerDevice.register(pine, results.config, function(error, device) { - if (error != null) { - return callback(error); - } - results.config.deviceId = device.id; - results.config.uuid = device.uuid; - results.config.registered_at = Math.floor(Date.now() / 1000); - params.uuid = results.config.uuid; - return callback(null, results); - }); - }, function(results, callback) { - var bar, spinner; - console.info('Initializing device operating system image'); - console.info('This may take a few minutes'); - if (process.env.DEBUG) { - console.log(results.config); - } - bar = new visuals.Progress('Downloading Device OS'); - spinner = new visuals.Spinner('Downloading Device OS (size unknown)'); - return manager.configure(params.manifest, results.config, function(error, imagePath, removeCallback) { - spinner.stop(); - return callback(error, imagePath, removeCallback); - }, function(state) { - if (state != null) { - return bar.update(state); - } else { - return spinner.start(); - } - }); - }, function(configuredImagePath, removeCallback, callback) { - var bar; - console.info('The base image was cached to improve initialization time of similar devices'); - console.info('Attempting to write operating system image to drive'); - bar = new visuals.Progress('Writing Device OS'); - return image.write({ - device: params.device, - image: configuredImagePath, - progress: _.bind(bar.update, bar) - }, function(error) { - if (error != null) { - return callback(error); - } - return callback(null, configuredImagePath, removeCallback); - }); - }, function(temporalImagePath, removeCallback, callback) { - console.info('Image written successfully'); - return removeCallback(callback); - }, function(callback) { - return resin.models.device.get(params.uuid).nodeify(callback); - }, function(device, callback) { - console.info("Device created: " + device.name); - return callback(null, params.uuid); + return Promise["try"](function() { + if (options.application != null) { + return options.application; } - ], done); + return vcs.getApplicationName(process.cwd()); + }).then(resin.models.application.get).then(function(application) { + console.info('Getting configuration options'); + return patterns.askDeviceOptions(application.device_type).tap(function(answers) { + var message; + if (answers.drive != null) { + message = "This will erase " + answers.drive + ". Are you sure?"; + return patterns.confirm(options.yes, message)["return"](answers.drive).then(umount.umountAsync); + } + }).then(function(answers) { + console.info('Getting device operating system'); + return patterns.download(application.device_type).then(function(temporalPath) { + var uuid; + uuid = resin.models.device.generateUUID(); + console.log("Registering to " + application.app_name + ": " + uuid); + return resin.models.device.register(application.app_name, uuid).tap(function(device) { + console.log('Configuring operating system'); + return init.configure(temporalPath, device.uuid, answers).then(stepHandler).then(function() { + console.log('Initializing device'); + return init.initialize(temporalPath, device.uuid, answers).then(stepHandler); + }).tap(function() { + if (answers.drive == null) { + return; + } + return umount.umountAsync(answers.drive).tap(function() { + return console.log("You can safely remove " + answers.drive + " now"); + }); + }); + }).then(function(device) { + console.log('Done'); + return device.uuid; + })["finally"](function() { + return fs.statAsync(temporalPath).then(function(stat) { + if (stat.isDirectory()) { + return rimraf(temporalPath); + } + return fs.unlinkAsync(temporalPath); + })["catch"](function(error) { + if (error.code === 'ENOENT') { + return; + } + throw error; + }); + }); + }); + }); + }).nodeify(done); } }; diff --git a/build/actions/environment-variables.js b/build/actions/environment-variables.js index 46c46c59..02316a6b 100644 --- a/build/actions/environment-variables.js +++ b/build/actions/environment-variables.js @@ -1,5 +1,5 @@ (function() { - var Promise, _, commandOptions, events, helpers, resin, visuals; + var Promise, _, commandOptions, events, patterns, resin, visuals; Promise = require('bluebird'); @@ -13,7 +13,7 @@ commandOptions = require('./command-options'); - helpers = require('../utils/helpers'); + patterns = require('../utils/patterns'); exports.list = { signature: 'envs', @@ -58,7 +58,7 @@ options: [commandOptions.yes, commandOptions.booleanDevice], permission: 'user', action: function(params, options, done) { - return helpers.confirm(options.yes, 'Are you sure you want to delete the environment variable?').then(function() { + return patterns.confirm(options.yes, 'Are you sure you want to delete the environment variable?').then(function() { if (options.device) { resin.models.environmentVariables.device.remove(params.id); return events.send('deviceEnvironmentVariable.delete', { diff --git a/build/actions/keys.js b/build/actions/keys.js index 88f65b02..a0e5a45a 100644 --- a/build/actions/keys.js +++ b/build/actions/keys.js @@ -1,5 +1,5 @@ (function() { - var Promise, _, capitano, commandOptions, events, fs, helpers, resin, visuals; + var Promise, _, capitano, commandOptions, events, fs, patterns, resin, visuals; Promise = require('bluebird'); @@ -17,7 +17,7 @@ commandOptions = require('./command-options'); - helpers = require('../utils/helpers'); + patterns = require('../utils/patterns'); exports.list = { signature: 'keys', @@ -51,7 +51,7 @@ options: [commandOptions.yes], permission: 'user', action: function(params, options, done) { - return helpers.confirm(options.yes, 'Are you sure you want to delete the key?').then(function() { + return patterns.confirm(options.yes, 'Are you sure you want to delete the key?').then(function() { return resin.models.key.remove(params.id); }).tap(function() { return events.send('publicKey.delete', { diff --git a/build/actions/wizard.js b/build/actions/wizard.js index 132d6dca..794ad288 100644 --- a/build/actions/wizard.js +++ b/build/actions/wizard.js @@ -1,5 +1,5 @@ (function() { - var Promise, capitano, form, helpers, mkdirp, resin; + var Promise, capitano, form, mkdirp, patterns, resin; Promise = require('bluebird'); @@ -11,7 +11,7 @@ form = require('resin-cli-form'); - helpers = require('../utils/helpers'); + patterns = require('../utils/patterns'); exports.wizard = { signature: 'quickstart [name]', @@ -24,18 +24,18 @@ if (params.name != null) { return; } - return helpers.selectApplication().tap(function(applicationName) { + return patterns.selectApplication().tap(function(applicationName) { return capitano.runAsync("app create " + applicationName); }).then(function(applicationName) { return params.name = applicationName; }); }).then(function() { return capitano.runAsync("device init --application " + params.name); - }).tap(helpers.awaitDevice).then(function(uuid) { + }).tap(patterns.awaitDevice).then(function(uuid) { return capitano.runAsync("device " + uuid); }).tap(function() { return console.log('Your device is ready, lets start pushing some code!'); - }).then(helpers.selectProjectDirectory).tap(mkdirp).tap(process.chdir).then(function() { + }).then(patterns.selectProjectDirectory).tap(mkdirp).tap(process.chdir).then(function() { return capitano.runAsync("app associate " + params.name); }).then(function(remoteUrl) { console.log("Resin git remote added: " + remoteUrl); diff --git a/build/utils/helpers.js b/build/utils/helpers.js index 6a8bc932..14f2b90c 100644 --- a/build/utils/helpers.js +++ b/build/utils/helpers.js @@ -1,97 +1,37 @@ (function() { - var Promise, _, form, resin, visuals; + var _, chalk, os; _ = require('lodash'); - Promise = require('bluebird'); + _.str = require('underscore.string'); - form = require('resin-cli-form'); + os = require('os'); - visuals = require('resin-cli-visuals'); + chalk = require('chalk'); - resin = require('resin-sdk'); - - exports.selectDeviceType = function() { - return form.ask({ - message: 'Device Type', - type: 'list', - choices: ['Raspberry Pi', 'Raspberry Pi 2', 'BeagleBone Black'] - }); + exports.getOperatingSystem = function() { + var platform; + platform = os.platform(); + if (platform === 'darwin') { + platform = 'osx'; + } + return platform; }; - exports.confirm = function(yesOption, message) { - return Promise["try"](function() { - if (yesOption) { - return true; - } - return form.ask({ - message: message, - type: 'confirm', - "default": false - }); - }).then(function(confirmed) { - if (!confirmed) { - throw new Error('Aborted'); - } - }); - }; - - exports.selectApplication = function() { - return resin.models.application.hasAny().then(function(hasAnyApplications) { - if (!hasAnyApplications) { - return; - } - return resin.models.application.getAll().then(function(applications) { - applications = _.pluck(applications, 'app_name'); - applications.unshift({ - name: 'Create a new application', - value: null - }); - return form.ask({ - message: 'Select an application', - type: 'list', - choices: applications - }); - }); - }).then(function(application) { - if (application != null) { - return application; - } - return form.ask({ - message: 'Choose a Name for your new application', - type: 'input' - }); - }); - }; - - exports.selectProjectDirectory = function() { - return resin.settings.get('projectsDirectory').then(function(projectsDirectory) { - return form.ask({ - message: 'Please choose a directory for your code', - type: 'input', - "default": projectsDirectory - }); - }); - }; - - exports.awaitDevice = function(uuid) { - var poll, spinner; - spinner = new visuals.Spinner("Awaiting device: " + uuid); - poll = function() { - return resin.models.device.isOnline(uuid).then(function(isOnline) { - if (isOnline) { - spinner.stop(); - console.info("Device became online: " + uuid); - } else { - spinner.start(); - return Promise.delay(3000).then(poll); - } - }); - }; - return resin.models.device.getName(uuid).then(function(deviceName) { - console.info("Waiting for " + deviceName + " to connect to resin..."); - return poll()["return"](uuid); - }); + exports.stateToString = function(state) { + var percentage, result; + percentage = _.str.lpad(state.percentage, 3, '0') + '%'; + 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.type); + } }; }).call(this); diff --git a/build/utils/patterns.js b/build/utils/patterns.js new file mode 100644 index 00000000..4427f6e9 --- /dev/null +++ b/build/utils/patterns.js @@ -0,0 +1,131 @@ +(function() { + var Promise, _, form, helpers, manager, resin, visuals; + + _ = require('lodash'); + + Promise = require('bluebird'); + + form = require('resin-cli-form'); + + visuals = require('resin-cli-visuals'); + + resin = require('resin-sdk'); + + manager = require('resin-image-manager'); + + helpers = require('./helpers'); + + exports.selectDeviceType = function() { + return resin.models.device.getSupportedDeviceTypes().then(function(deviceTypes) { + return form.ask({ + message: 'Device Type', + type: 'list', + choices: deviceTypes + }); + }); + }; + + exports.confirm = function(yesOption, message) { + return Promise["try"](function() { + if (yesOption) { + return true; + } + return form.ask({ + message: message, + type: 'confirm', + "default": false + }); + }).then(function(confirmed) { + if (!confirmed) { + throw new Error('Aborted'); + } + }); + }; + + exports.selectApplication = function() { + return resin.models.application.hasAny().then(function(hasAnyApplications) { + if (!hasAnyApplications) { + return; + } + return resin.models.application.getAll().then(function(applications) { + applications = _.pluck(applications, 'app_name'); + applications.unshift({ + name: 'Create a new application', + value: null + }); + return form.ask({ + message: 'Select an application', + type: 'list', + choices: applications + }); + }); + }).then(function(application) { + if (application != null) { + return application; + } + return form.ask({ + message: 'Choose a Name for your new application', + type: 'input' + }); + }); + }; + + exports.selectProjectDirectory = function() { + return resin.settings.get('projectsDirectory').then(function(projectsDirectory) { + return form.ask({ + message: 'Please choose a directory for your code', + type: 'input', + "default": projectsDirectory + }); + }); + }; + + exports.awaitDevice = function(uuid) { + var poll, spinner; + spinner = new visuals.Spinner("Awaiting device: " + uuid); + poll = function() { + return resin.models.device.isOnline(uuid).then(function(isOnline) { + if (isOnline) { + spinner.stop(); + console.info("Device became online: " + uuid); + } else { + spinner.start(); + return Promise.delay(3000).then(poll); + } + }); + }; + return resin.models.device.getName(uuid).then(function(deviceName) { + console.info("Waiting for " + deviceName + " to connect to resin..."); + return poll()["return"](uuid); + }); + }; + + exports.askDeviceOptions = function(deviceType) { + return resin.models.config.getDeviceOptions(deviceType).then(form.run).then(function(answers) { + if (answers.os == null) { + answers.os = helpers.getOperatingSystem(); + } + return answers; + }); + }; + + exports.download = function(deviceType) { + return manager.get(deviceType).then(function(stream) { + var bar, spinner; + bar = new visuals.Progress('Downloading Device OS'); + spinner = new visuals.Spinner('Downloading Device OS (size unknown)'); + stream.on('progress', function(state) { + if (state != null) { + return bar.update(state); + } else { + return spinner.start(); + } + }); + stream.on('end', function() { + return spinner.stop(); + }); + return manager.pipeTemporal(stream); + }); + }; + +}).call(this); diff --git a/lib/actions/app.coffee b/lib/actions/app.coffee index 165f4faf..fa6a4a98 100644 --- a/lib/actions/app.coffee +++ b/lib/actions/app.coffee @@ -5,6 +5,7 @@ commandOptions = require('./command-options') vcs = require('resin-vcs') events = require('resin-cli-events') helpers = require('../utils/helpers') +patterns = require('../utils/patterns') exports.create = signature: 'app create ' @@ -42,7 +43,7 @@ exports.create = if hasApplication throw new Error('You already have an application with that name!') - .then(helpers.selectDeviceType).then (deviceType) -> + .then(patterns.selectDeviceType).then (deviceType) -> return resin.models.application.create(params.name, deviceType) .then (application) -> console.info("Application created: #{application.app_name} (#{application.device_type}, id #{application.id})") @@ -128,7 +129,7 @@ exports.remove = options: [ commandOptions.yes ] permission: 'user' action: (params, options, done) -> - helpers.confirm(options.yes, 'Are you sure you want to delete the application?').then -> + patterns.confirm(options.yes, 'Are you sure you want to delete the application?').then -> resin.models.application.remove(params.name) .tap -> resin.models.application.get(params.name).then (application) -> @@ -162,7 +163,7 @@ exports.associate = .then -> message = "Are you sure you want to associate #{currentDirectory} with #{params.name}?" - helpers.confirm(options.yes, message) + patterns.confirm(options.yes, message) .then -> resin.models.application.get(params.name).get('git_repository').then (gitRepository) -> diff --git a/lib/actions/device.coffee b/lib/actions/device.coffee index e737e68a..84102e94 100644 --- a/lib/actions/device.coffee +++ b/lib/actions/device.coffee @@ -1,19 +1,16 @@ Promise = require('bluebird') capitano = require('capitano') _ = require('lodash') -async = require('async') resin = require('resin-sdk') visuals = require('resin-cli-visuals') vcs = require('resin-vcs') -manager = require('resin-image-manager') -image = require('resin-image') -inject = require('resin-config-inject') -registerDevice = require('resin-register-device') -pine = require('resin-pine') -deviceConfig = require('resin-device-config') form = require('resin-cli-form') events = require('resin-cli-events') -htmlToText = require('html-to-text') +init = require('resin-device-init') +fs = Promise.promisifyAll(require('fs')) +rimraf = Promise.promisify(require('rimraf')) +umount = Promise.promisifyAll(require('umount')) +patterns = require('../utils/patterns') helpers = require('../utils/helpers') commandOptions = require('./command-options') @@ -107,7 +104,7 @@ exports.remove = options: [ commandOptions.yes ] permission: 'user' action: (params, options, done) -> - helpers.confirm(options.yes, 'Are you sure you want to delete the device?').then -> + patterns.confirm(options.yes, 'Are you sure you want to delete the device?').then -> resin.models.device.remove(params.uuid) .tap -> events.send('device.delete', device: params.uuid) @@ -156,186 +153,84 @@ exports.rename = events.send('device.rename', device: params.uuid) .nodeify(done) +stepHandler = (step) -> + + step.on('stdout', _.bind(process.stdout.write, process.stdout)) + step.on('stderr', _.bind(process.stderr.write, process.stderr)) + + step.on 'state', (state) -> + return if state.operation.command is 'burn' + console.log(helpers.stateToString(state)) + + bar = new visuals.Progress('Writing Device OS') + + step.on('burn', _.bind(bar.update, bar)) + + return new Promise (resolve, reject) -> + step.on('error', reject) + step.on('end', resolve) + exports.init = - signature: 'device init [device]' + signature: 'device init' description: 'initialise a device with resin os' help: ''' Use this command to download the OS image of a certain application and write it to an SD Card. - Note that this command requires admin privileges. - - If `device` is omitted, you will be prompted to select a device interactively. - - Notice this command asks for confirmation interactively. + Notice this command may ask for confirmation interactively. You can avoid this by passing the `--yes` boolean option. - You can quiet the progress bar and other logging information by passing the `--quiet` boolean option. - - You need to configure the network type and other settings: - - Ethernet: - You can setup the device OS to use ethernet by setting the `--network` option to "ethernet". - - Wifi: - You can setup the device OS to use wifi by setting the `--network` option to "wifi". - If you set "network" to "wifi", you will need to specify the `--ssid` and `--key` option as well. - - You can omit network related options to be asked about them interactively. - Examples: $ resin device init $ resin device init --application MyApp - $ resin device init --application MyApp --network ethernet - $ resin device init /dev/disk2 --application MyApp --network wifi --ssid MyNetwork --key secret ''' options: [ commandOptions.optionalApplication - commandOptions.network - commandOptions.wifiSsid - commandOptions.wifiKey + commandOptions.yes ] permission: 'user' root: true action: (params, options, done) -> + Promise.try -> + return options.application if options.application? + return vcs.getApplicationName(process.cwd()) + .then(resin.models.application.get) + .then (application) -> - networkOptions = - network: options.network - wifiSsid: options.ssid - wifiKey: options.key + console.info('Getting configuration options') + patterns.askDeviceOptions(application.device_type).tap (answers) -> + if answers.drive? + message = "This will erase #{answers.drive}. Are you sure?" + patterns.confirm(options.yes, message) + .return(answers.drive) + .then(umount.umountAsync) + .then (answers) -> + console.info('Getting device operating system') + patterns.download(application.device_type).then (temporalPath) -> + uuid = resin.models.device.generateUUID() + console.log("Registering to #{application.app_name}: #{uuid}") + resin.models.device.register(application.app_name, uuid).tap (device) -> + console.log('Configuring operating system') + init.configure(temporalPath, device.uuid, answers).then(stepHandler).then -> + console.log('Initializing device') + init.initialize(temporalPath, device.uuid, answers).then(stepHandler) + .tap -> + return if not answers.drive? + umount.umountAsync(answers.drive).tap -> + console.log("You can safely remove #{answers.drive} now") + .then (device) -> + console.log('Done') + return device.uuid - async.waterfall [ + .finally -> + fs.statAsync(temporalPath).then (stat) -> + return rimraf(temporalPath) if stat.isDirectory() + return fs.unlinkAsync(temporalPath) + .catch (error) -> - (callback) -> - return callback(null, options.application) if options.application? - vcs.getApplicationName(process.cwd()).nodeify(callback) + # Ignore errors if temporary file does not exist + return if error.code is 'ENOENT' - (applicationName, callback) -> - options.application = applicationName - resin.models.application.has(options.application).nodeify(callback) + throw error - (hasApplication, callback) -> - if not hasApplication - return callback(new Error("Invalid application: #{options.application}")) - - return callback(null, params.device) if params.device? - form.ask - type: 'drive' - message: 'Select a drive' - .nodeify(callback) - - (device, callback) -> - params.device = device - message = "This will completely erase #{params.device}. Are you sure you want to continue?" - if options.yes - return callback(null, true) - else - form.ask - message: message - type: 'confirm' - default: false - .nodeify(callback) - - (confirmed, callback) -> - return done() if not confirmed - return callback() if networkOptions.network? - form.run [ - message: 'Network Type' - name: 'network' - type: 'list' - choices: [ 'ethernet', 'wifi' ] - , - message: 'Wifi Ssid' - name: 'wifiSsid' - type: 'input' - when: - network: 'wifi' - , - message: 'Wifi Key' - name: 'wifiKey' - type: 'input' - when: - network: 'wifi' - ] - .then (parameters) -> - _.extend(networkOptions, parameters) - .nodeify(callback) - - (callback) -> - console.info("Checking application: #{options.application}") - resin.models.application.get(options.application).nodeify(callback) - - (application, callback) -> - async.parallel - - manifest: (callback) -> - console.info('Getting device manifest for the application') - resin.models.device.getManifestBySlug(application.device_type).nodeify(callback) - - config: (callback) -> - console.info('Fetching application configuration') - deviceConfig.get(options.application, networkOptions).nodeify(callback) - - , callback - - (results, callback) -> - params.manifest = results.manifest - console.info('Associating the device') - - registerDevice.register pine, results.config, (error, device) -> - return callback(error) if error? - - # Associate a device - results.config.deviceId = device.id - results.config.uuid = device.uuid - results.config.registered_at = Math.floor(Date.now() / 1000) - - params.uuid = results.config.uuid - - return callback(null, results) - - (results, callback) -> - console.info('Initializing device operating system image') - console.info('This may take a few minutes') - - if process.env.DEBUG - console.log(results.config) - - bar = new visuals.Progress('Downloading Device OS') - spinner = new visuals.Spinner('Downloading Device OS (size unknown)') - - manager.configure params.manifest, results.config, (error, imagePath, removeCallback) -> - spinner.stop() - return callback(error, imagePath, removeCallback) - , (state) -> - if state? - bar.update(state) - else - spinner.start() - - (configuredImagePath, removeCallback, callback) -> - console.info('The base image was cached to improve initialization time of similar devices') - - console.info('Attempting to write operating system image to drive') - - bar = new visuals.Progress('Writing Device OS') - image.write - device: params.device - image: configuredImagePath - progress: _.bind(bar.update, bar) - , (error) -> - return callback(error) if error? - return callback(null, configuredImagePath, removeCallback) - - (temporalImagePath, removeCallback, callback) -> - console.info('Image written successfully') - removeCallback(callback) - - (callback) -> - resin.models.device.get(params.uuid).nodeify(callback) - - (device, callback) -> - console.info("Device created: #{device.name}") - return callback(null, params.uuid) - - ], done + .nodeify(done) diff --git a/lib/actions/environment-variables.coffee b/lib/actions/environment-variables.coffee index 7d9a76ee..4e3d0d37 100644 --- a/lib/actions/environment-variables.coffee +++ b/lib/actions/environment-variables.coffee @@ -4,7 +4,7 @@ resin = require('resin-sdk') visuals = require('resin-cli-visuals') events = require('resin-cli-events') commandOptions = require('./command-options') -helpers = require('../utils/helpers') +patterns = require('../utils/patterns') exports.list = signature: 'envs' @@ -83,7 +83,7 @@ exports.remove = ] permission: 'user' action: (params, options, done) -> - helpers.confirm(options.yes, 'Are you sure you want to delete the environment variable?').then -> + patterns.confirm(options.yes, 'Are you sure you want to delete the environment variable?').then -> if options.device resin.models.environmentVariables.device.remove(params.id) events.send('deviceEnvironmentVariable.delete', id: params.id) diff --git a/lib/actions/keys.coffee b/lib/actions/keys.coffee index 0f4db5bd..d57d3afc 100644 --- a/lib/actions/keys.coffee +++ b/lib/actions/keys.coffee @@ -6,7 +6,7 @@ capitano = require('capitano') visuals = require('resin-cli-visuals') events = require('resin-cli-events') commandOptions = require('./command-options') -helpers = require('../utils/helpers') +patterns = require('../utils/patterns') exports.list = signature: 'keys' @@ -68,7 +68,7 @@ exports.remove = options: [ commandOptions.yes ] permission: 'user' action: (params, options, done) -> - helpers.confirm(options.yes, 'Are you sure you want to delete the key?').then -> + patterns.confirm(options.yes, 'Are you sure you want to delete the key?').then -> resin.models.key.remove(params.id) .tap -> events.send('publicKey.delete', id: params.id) diff --git a/lib/actions/wizard.coffee b/lib/actions/wizard.coffee index b88c730c..d10e4919 100644 --- a/lib/actions/wizard.coffee +++ b/lib/actions/wizard.coffee @@ -3,7 +3,7 @@ capitano = Promise.promisifyAll(require('capitano')) mkdirp = Promise.promisify(require('mkdirp')) resin = require('resin-sdk') form = require('resin-cli-form') -helpers = require('../utils/helpers') +patterns = require('../utils/patterns') exports.wizard = signature: 'quickstart [name]' @@ -28,18 +28,18 @@ exports.wizard = action: (params, options, done) -> Promise.try -> return if params.name? - helpers.selectApplication().tap (applicationName) -> + patterns.selectApplication().tap (applicationName) -> capitano.runAsync("app create #{applicationName}") .then (applicationName) -> params.name = applicationName .then -> return capitano.runAsync("device init --application #{params.name}") - .tap(helpers.awaitDevice) + .tap(patterns.awaitDevice) .then (uuid) -> return capitano.runAsync("device #{uuid}") .tap -> console.log('Your device is ready, lets start pushing some code!') - .then(helpers.selectProjectDirectory) + .then(patterns.selectProjectDirectory) .tap(mkdirp) .tap(process.chdir) .then -> diff --git a/lib/utils/helpers.coffee b/lib/utils/helpers.coffee index b5c1cdc3..6044184e 100644 --- a/lib/utils/helpers.coffee +++ b/lib/utils/helpers.coffee @@ -1,76 +1,23 @@ _ = require('lodash') -Promise = require('bluebird') -form = require('resin-cli-form') -visuals = require('resin-cli-visuals') -resin = require('resin-sdk') +_.str = require('underscore.string') +os = require('os') +chalk = require('chalk') -exports.selectDeviceType = -> - return form.ask - message: 'Device Type' - type: 'list' - choices: [ +exports.getOperatingSystem = -> + platform = os.platform() + platform = 'osx' if platform is 'darwin' + return platform - # Lock to specific devices until we support - # the rest with device specs. - 'Raspberry Pi' - 'Raspberry Pi 2' - 'BeagleBone Black' - ] +exports.stateToString = (state) -> + percentage = _.str.lpad(state.percentage, 3, '0') + '%' + result = "#{chalk.blue(percentage)} #{chalk.cyan(state.operation.command)}" -exports.confirm = (yesOption, message) -> - Promise.try -> - return true if yesOption - return form.ask - message: message - type: 'confirm' - default: false - .then (confirmed) -> - if not confirmed - throw new Error('Aborted') - -exports.selectApplication = -> - resin.models.application.hasAny().then (hasAnyApplications) -> - return if not hasAnyApplications - resin.models.application.getAll().then (applications) -> - applications = _.pluck(applications, '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' - -exports.selectProjectDirectory = -> - resin.settings.get('projectsDirectory').then (projectsDirectory) -> - return form.ask - message: 'Please choose a directory for your code' - type: 'input' - default: projectsDirectory - -exports.awaitDevice = (uuid) -> - spinner = new visuals.Spinner("Awaiting device: #{uuid}") - - poll = -> - resin.models.device.isOnline(uuid).then (isOnline) -> - if isOnline - spinner.stop() - console.info("Device became online: #{uuid}") - return - else - - # Spinner implementation is smart enough to - # not start again if it was already started - spinner.start() - - return Promise.delay(3000).then(poll) - - resin.models.device.getName(uuid).then (deviceName) -> - console.info("Waiting for #{deviceName} to connect to resin...") - poll().return(uuid) + 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}") diff --git a/lib/utils/patterns.coffee b/lib/utils/patterns.coffee new file mode 100644 index 00000000..6e804278 --- /dev/null +++ b/lib/utils/patterns.coffee @@ -0,0 +1,94 @@ +_ = require('lodash') +Promise = require('bluebird') +form = require('resin-cli-form') +visuals = require('resin-cli-visuals') +resin = require('resin-sdk') +manager = require('resin-image-manager') +helpers = require('./helpers') + +exports.selectDeviceType = -> + resin.models.device.getSupportedDeviceTypes().then (deviceTypes) -> + return form.ask + message: 'Device Type' + type: 'list' + choices: deviceTypes + +exports.confirm = (yesOption, message) -> + Promise.try -> + return true if yesOption + return form.ask + message: message + type: 'confirm' + default: false + .then (confirmed) -> + if not confirmed + throw new Error('Aborted') + +exports.selectApplication = -> + resin.models.application.hasAny().then (hasAnyApplications) -> + return if not hasAnyApplications + resin.models.application.getAll().then (applications) -> + applications = _.pluck(applications, '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' + +exports.selectProjectDirectory = -> + resin.settings.get('projectsDirectory').then (projectsDirectory) -> + return form.ask + message: 'Please choose a directory for your code' + type: 'input' + default: projectsDirectory + +exports.awaitDevice = (uuid) -> + spinner = new visuals.Spinner("Awaiting device: #{uuid}") + + poll = -> + resin.models.device.isOnline(uuid).then (isOnline) -> + if isOnline + spinner.stop() + console.info("Device became online: #{uuid}") + return + else + + # Spinner implementation is smart enough to + # not start again if it was already started + spinner.start() + + return Promise.delay(3000).then(poll) + + resin.models.device.getName(uuid).then (deviceName) -> + console.info("Waiting for #{deviceName} to connect to resin...") + poll().return(uuid) + +exports.askDeviceOptions = (deviceType) -> + resin.models.config.getDeviceOptions(deviceType).then(form.run) + .then (answers) -> + answers.os ?= helpers.getOperatingSystem() + return answers + +exports.download = (deviceType) -> + manager.get(deviceType).then (stream) -> + bar = new visuals.Progress('Downloading Device OS') + spinner = new visuals.Spinner('Downloading Device OS (size unknown)') + + stream.on 'progress', (state) -> + if state? + bar.update(state) + else + spinner.start() + + stream.on 'end', -> + spinner.stop() + + return manager.pipeTemporal(stream) diff --git a/package.json b/package.json index 187c8fba..eb3c9742 100644 --- a/package.json +++ b/package.json @@ -38,12 +38,11 @@ "sinon-chai": "^2.8.0" }, "dependencies": { - "async": "^1.3.0", "bluebird": "^2.9.34", "capitano": "~1.7.0", + "chalk": "^1.1.1", "coffee-script": "^1.9.3", "columnify": "^1.5.2", - "html-to-text": "^1.3.1", "is-root": "^1.0.0", "lodash": "^3.10.0", "mkdirp": "~0.5.0", @@ -52,16 +51,18 @@ "open": "0.0.5", "resin-cli-events": "^1.0.2", "resin-cli-form": "^1.2.1", - "resin-cli-visuals": "^1.1.0", + "resin-cli-visuals": "^1.2.2", "resin-config-inject": "^2.0.0", "resin-device-config": "^1.0.0", + "resin-device-init": "^1.0.4", "resin-image": "^1.1.4", - "resin-image-manager": "^2.0.0", + "resin-image-manager": "^3.2.2", "resin-pine": "^1.3.0", - "resin-register-device": "^1.0.1", "resin-sdk": "^2.7.2", + "resin-settings-client": "^3.1.0", "resin-vcs": "^2.0.0", - "selfupdate": "^1.1.0", + "rimraf": "^2.4.3", + "umount": "^1.1.1", "underscore.string": "^3.1.1", "update-notifier": "^0.5.0", "valid-email": "0.0.2"