diff --git a/build/actions/ssh.js b/build/actions/ssh.js index 2a53b296..ffa26ca0 100644 --- a/build/actions/ssh.js +++ b/build/actions/ssh.js @@ -35,9 +35,9 @@ limitations under the License. }; module.exports = { - signature: 'ssh [destination]', + signature: 'ssh [uuid]', description: '(beta) get a shell into the running app container of a device', - help: 'WARNING: If you\'re running Windows, this command only supports `cmd.exe`.\n\nUse this command to get a shell into the running application container of\nyour device.\n\nThe `destination` argument can be either a device uuid or an application name.\n\nExamples:\n\n $ resin ssh MyApp\n $ resin ssh 7cf02a6\n $ resin ssh 7cf02a6 --port 8080\n $ resin ssh 7cf02a6 -v', + help: 'WARNING: If you\'re running Windows, this command only supports `cmd.exe`.\n\nUse this command to get a shell into the running application container of\nyour device.\n\nExamples:\n\n $ resin ssh MyApp\n $ resin ssh 7cf02a6\n $ resin ssh 7cf02a6 --port 8080\n $ resin ssh 7cf02a6 -v', permission: 'user', primary: true, options: [ @@ -45,7 +45,7 @@ limitations under the License. signature: 'port', parameter: 'port', description: 'ssh gateway port', - alias: 't' + alias: 'p' }, { signature: 'verbose', boolean: true, @@ -64,11 +64,11 @@ limitations under the License. options.port = 22; } verbose = options.verbose ? '-vvv' : ''; - return resin.models.device.has(params.destination).then(function(isValidUUID) { + return resin.models.device.has(params.uuid).then(function(isValidUUID) { if (isValidUUID) { - return params.destination; + return params.uuid; } - return patterns.inferOrSelectDevice(params.destination); + return patterns.inferOrSelectDevice(); }).then(function(uuid) { console.info("Connecting with: " + uuid); return resin.models.device.get(uuid); @@ -88,7 +88,7 @@ limitations under the License. } return Promise["try"](function() { var command, spawn, subShellCommand; - command = "ssh " + verbose + " -t -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p " + options.port + " " + username + "@ssh." + (settings.get('proxyUrl')) + " enter " + uuid + " " + containerId; + command = "ssh " + verbose + " -t -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ControlMaster=no -p " + options.port + " " + username + "@ssh." + (settings.get('proxyUrl')) + " enter " + uuid + " " + containerId; subShellCommand = getSubShellCommand(command); return spawn = child_process.spawn(subShellCommand.program, subShellCommand.args, { stdio: 'inherit' diff --git a/build/actions/sync.js b/build/actions/sync.js index 2d8b8263..ff84ee6a 100644 --- a/build/actions/sync.js +++ b/build/actions/sync.js @@ -16,38 +16,79 @@ limitations under the License. */ (function() { + var loadConfig; + + loadConfig = function(source) { + var _, config, configPath, error, error1, fs, jsYaml, path, result; + fs = require('fs'); + path = require('path'); + _ = require('lodash'); + jsYaml = require('js-yaml'); + configPath = path.join(source, '.resin-sync.yml'); + try { + config = fs.readFileSync(configPath, { + encoding: 'utf8' + }); + result = jsYaml.safeLoad(config); + } catch (error1) { + error = error1; + if (error.code === 'ENOENT') { + return {}; + } + throw error; + } + if (!_.isPlainObject(result)) { + throw new Error("Invalid configuration file: " + configPath); + } + return result; + }; + module.exports = { - signature: 'sync [destination]', - description: '(beta) sync your changes with a device', - help: 'WARNING: If you\'re running Windows, this command only supports `cmd.exe`.\n\nUse this command to sync your local changes to a certain device on the fly.\n\nThe `destination` argument can be either a device uuid or an application name.\n\nYou can save all the options mentioned below in a `resin-sync.yml` file,\nby using the same option names as keys. For example:\n\n $ cat $PWD/resin-sync.yml\n source: src/\n before: \'echo Hello\'\n ignore:\n - .git\n - node_modules/\n progress: true\n verbose: false\n\nNotice that explicitly passed command options override the ones set in the configuration file.\n\nExamples:\n\n $ resin sync MyApp\n $ resin sync 7cf02a6\n $ resin sync 7cf02a6 --port 8080\n $ resin sync 7cf02a6 --ignore foo,bar\n $ resin sync 7cf02a6 -v', + signature: 'sync [uuid]', + description: '(beta) sync your changes to a device', + help: 'WARNING: If you\'re running Windows, this command only supports `cmd.exe`.\n\nUse this command to sync your local changes to a certain device on the fly.\n\nAfter every \'resin sync\' the updated settings will be saved in\n\'/.resin-sync.yml\' and will be used in later invocations. You can\nalso change any option by editing \'.resin-sync.yml\' directly.\n\nHere is an example \'.resin-sync.yml\' :\n\n $ cat $PWD/.resin-sync.yml\n uuid: 7cf02a6\n destination: \'/usr/src/app\'\n before: \'echo Hello\'\n after: \'echo Done\'\n ignore:\n - .git\n - node_modules/\n\nCommand line options have precedence over the ones saved in \'.resin-sync.yml\'.\n\nIf \'.gitignore\' is found in the source directory then all explicitly listed files will be\nexcluded from the syncing process. You can choose to change this default behavior with the\n\'--skip-gitignore\' option.\n\nExamples:\n\n $ resin sync 7cf02a6 --source . --destination /usr/src/app\n $ resin sync 7cf02a6 -s /home/user/myResinProject -d /usr/src/app --before \'echo Hello\' --after \'echo Done\'\n $ resin sync --ignore lib/\n $ resin sync --verbose false\n $ resin sync', permission: 'user', primary: true, options: [ { signature: 'source', parameter: 'path', - description: 'custom source path', + description: 'local directory path to synchronize to device', alias: 's' + }, { + signature: 'destination', + parameter: 'path', + description: 'destination path on device', + alias: 'd' }, { signature: 'ignore', parameter: 'paths', description: 'comma delimited paths to ignore when syncing', alias: 'i' + }, { + signature: 'skip-gitignore', + boolean: true, + description: 'do not parse excluded/included files from .gitignore' }, { signature: 'before', parameter: 'command', description: 'execute a command before syncing', alias: 'b' }, { - signature: 'progress', - boolean: true, - description: 'show progress', - alias: 'p' + signature: 'after', + parameter: 'command', + description: 'execute a command after syncing', + alias: 'a' }, { signature: 'port', parameter: 'port', description: 'ssh port', alias: 't' + }, { + signature: 'progress', + boolean: true, + description: 'show progress', + alias: 'p' }, { signature: 'verbose', boolean: true, @@ -56,20 +97,43 @@ limitations under the License. } ], action: function(params, options, done) { - var patterns, resin, resinSync; + var Promise, fs, path, patterns, resin, resinSync; + fs = require('fs'); + path = require('path'); resin = require('resin-sdk'); + Promise = require('bluebird'); resinSync = require('resin-sync'); patterns = require('../utils/patterns'); - if (options.ignore != null) { - options.ignore = options.ignore.split(','); - } - return resin.models.device.has(params.destination).then(function(isValidUUID) { - if (isValidUUID) { - return params.destination; + return Promise["try"](function() { + var error1; + try { + fs.accessSync(path.join(process.cwd(), '.resin-sync.yml')); + } catch (error1) { + if (options.source == null) { + throw new Error('No --source option passed and no \'.resin-sync.yml\' file found in current directory.'); + } } - return patterns.inferOrSelectDevice(params.destination); - }).then(function(uuid) { - return resinSync.sync(uuid, options); + if (options.source == null) { + options.source = process.cwd(); + } + if (options.ignore != null) { + options.ignore = options.ignore.split(','); + } + return Promise.resolve(params.uuid).then(function(uuid) { + var savedUuid; + if (uuid == null) { + savedUuid = loadConfig(options.source).uuid; + return patterns.inferOrSelectDevice(savedUuid); + } + return resin.models.device.has(uuid).then(function(hasDevice) { + if (!hasDevice) { + throw new Error("Device not found: " + uuid); + } + return uuid; + }); + }).then(function(uuid) { + return resinSync.sync(uuid, options); + }); }).nodeify(done); } }; diff --git a/build/utils/patterns.js b/build/utils/patterns.js index 33b60be6..0fb977f7 100644 --- a/build/utils/patterns.js +++ b/build/utils/patterns.js @@ -16,7 +16,8 @@ limitations under the License. */ (function() { - var Promise, _, chalk, form, messages, resin, validation, visuals; + var Promise, _, chalk, form, messages, resin, validation, visuals, + indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; _ = require('lodash'); @@ -191,25 +192,18 @@ limitations under the License. }); }; - exports.inferOrSelectDevice = function(applicationName) { - return Promise["try"](function() { - if (applicationName != null) { - return resin.models.device.getAllByApplication(applicationName); - } - return resin.models.device.getAll(); - }).filter(function(device) { + exports.inferOrSelectDevice = function(preferredUuid) { + return resin.models.device.getAll().filter(function(device) { return device.is_online; - }).then(function(devices) { - if (_.isEmpty(devices)) { - throw new Error('You don\'t have any devices'); - } - if (devices.length === 1) { - return _.first(devices).uuid; + }).then(function(onlineDevices) { + if (_.isEmpty(onlineDevices)) { + throw new Error('You don\'t have any devices online'); } return form.ask({ message: 'Select a device', type: 'list', - choices: _.map(devices, function(device) { + "default": indexOf.call(_.map(onlineDevices, 'uuid'), preferredUuid) >= 0 ? preferredUuid : onlineDevices[0].uuid, + choices: _.map(onlineDevices, function(device) { return { name: (device.name || 'Untitled') + " (" + (device.uuid.slice(0, 7)) + ")", value: device.uuid diff --git a/doc/cli.markdown b/doc/cli.markdown index 10056a1b..c5dfd98b 100644 --- a/doc/cli.markdown +++ b/doc/cli.markdown @@ -69,11 +69,11 @@ Now you have access to all the commands referenced below. - Sync - - [sync [source]](#sync-source-) + - [sync [uuid]](#sync-uuid-) - SSH - - [ssh <uuid>](#ssh-60-uuid-62-) + - [ssh [uuid]](#ssh-uuid-) - Notes @@ -582,65 +582,82 @@ continuously stream output # Sync -## sync [source] +## sync [uuid] WARNING: If you're running Windows, this command only supports `cmd.exe`. Use this command to sync your local changes to a certain device on the fly. -The `source` argument can be either a device uuid or an application name. +After every 'resin sync' the updated settings will be saved in +'/.resin-sync.yml' and will be used in later invocations. You can +also change any option by editing '.resin-sync.yml' directly. -You can save all the options mentioned below in a `resin-sync.yml` file, -by using the same option names as keys. For example: +Here is an example '.resin-sync.yml' : - $ cat $PWD/resin-sync.yml - source: src/ + $ cat $PWD/.resin-sync.yml + uuid: 7cf02a6 + destination: '/usr/src/app' before: 'echo Hello' + after: 'echo Done' ignore: - .git - node_modules/ - progress: true - verbose: false -Notice that explicitly passed command options override the ones set in the configuration file. +Command line options have precedence over the ones saved in '.resin-sync.yml'. + +If '.gitignore' is found in the source directory then all explicitly listed files will be +excluded from the syncing process. You can choose to change this default behavior with the +'--skip-gitignore' option. Examples: - $ resin sync MyApp - $ resin sync 7cf02a6 - $ resin sync 7cf02a6 --port 8080 - $ resin sync 7cf02a6 --ignore foo,bar - $ resin sync 7cf02a6 -v + $ resin sync 7cf02a6 --source . --destination /usr/src/app + $ resin sync 7cf02a6 -s /home/user/myResinProject -d /usr/src/app --before 'echo Hello' --after 'echo Done' + $ resin sync --ignore lib/ + $ resin sync --verbose false + $ resin sync ### Options #### --source, -s <path> -custom source path +local directory path to synchronize to device + +#### --destination, -d <path> + +destination path on device #### --ignore, -i <paths> comma delimited paths to ignore when syncing +#### --skip-gitignore + +do not parse excluded/included files from .gitignore + #### --before, -b <command> execute a command before syncing -#### --progress, -p +#### --after, -a <command> -show progress +execute a command after syncing #### --port, -t <port> ssh port +#### --progress, -p + +show progress + #### --verbose, -v increase verbosity # SSH -## ssh <uuid> +## ssh [uuid] WARNING: If you're running Windows, this command only supports `cmd.exe`. @@ -649,13 +666,14 @@ your device. Examples: + $ resin ssh MyApp $ resin ssh 7cf02a6 $ resin ssh 7cf02a6 --port 8080 $ resin ssh 7cf02a6 -v ### Options -#### --port, -t <port> +#### --port, -p <port> ssh gateway port diff --git a/lib/actions/ssh.coffee b/lib/actions/ssh.coffee index 6a46e659..e3f9f358 100644 --- a/lib/actions/ssh.coffee +++ b/lib/actions/ssh.coffee @@ -36,7 +36,7 @@ getSubShellCommand = (command) -> } module.exports = - signature: 'ssh [destination]' + signature: 'ssh [uuid]' description: '(beta) get a shell into the running app container of a device' help: ''' WARNING: If you're running Windows, this command only supports `cmd.exe`. @@ -44,8 +44,6 @@ module.exports = Use this command to get a shell into the running application container of your device. - The `destination` argument can be either a device uuid or an application name. - Examples: $ resin ssh MyApp @@ -59,7 +57,7 @@ module.exports = signature: 'port' parameter: 'port' description: 'ssh gateway port' - alias: 't' + alias: 'p' , signature: 'verbose' boolean: true @@ -78,11 +76,11 @@ module.exports = verbose = if options.verbose then '-vvv' else '' - resin.models.device.has(params.destination).then (isValidUUID) -> + resin.models.device.has(params.uuid).then (isValidUUID) -> if isValidUUID - return params.destination + return params.uuid - return patterns.inferOrSelectDevice(params.destination) + return patterns.inferOrSelectDevice() .then (uuid) -> console.info("Connecting with: #{uuid}") resin.models.device.get(uuid) @@ -96,7 +94,11 @@ module.exports = .then ({ username, uuid, containerId }) -> throw new Error('Did not find running application container') if not containerId? Promise.try -> - command = "ssh #{verbose} -t -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ + command = "ssh #{verbose} -t \ + -o LogLevel=ERROR \ + -o StrictHostKeyChecking=no \ + -o UserKnownHostsFile=/dev/null \ + -o ControlMaster=no \ -p #{options.port} #{username}@ssh.#{settings.get('proxyUrl')} enter #{uuid} #{containerId}" subShellCommand = getSubShellCommand(command) diff --git a/lib/actions/sync.coffee b/lib/actions/sync.coffee index a6912c8f..63cf165a 100644 --- a/lib/actions/sync.coffee +++ b/lib/actions/sync.coffee @@ -14,65 +14,110 @@ See the License for the specific language governing permissions and limitations under the License. ### +# Loads '.resin-sync.yml' configuration from 'source' directory. +# Returns the configuration object on success +# +# TODO: Use 'config.load()' method from `resin sync` when resin sync gets +# integrated into resin CLI +loadConfig = (source) -> + fs = require('fs') + path = require('path') + _ = require('lodash') + jsYaml = require('js-yaml') + + configPath = path.join(source, '.resin-sync.yml') + try + config = fs.readFileSync(configPath, encoding: 'utf8') + result = jsYaml.safeLoad(config) + catch error + # return empty object if '.resin-sync.yml' is missing + if error.code is 'ENOENT' + return {} + throw error + + if not _.isPlainObject(result) + throw new Error("Invalid configuration file: #{configPath}") + + return result + module.exports = - signature: 'sync [destination]' - description: '(beta) sync your changes with a device' + signature: 'sync [uuid]' + description: '(beta) sync your changes to a device' help: ''' WARNING: If you're running Windows, this command only supports `cmd.exe`. Use this command to sync your local changes to a certain device on the fly. - The `destination` argument can be either a device uuid or an application name. + After every 'resin sync' the updated settings will be saved in + '/.resin-sync.yml' and will be used in later invocations. You can + also change any option by editing '.resin-sync.yml' directly. - You can save all the options mentioned below in a `resin-sync.yml` file, - by using the same option names as keys. For example: + Here is an example '.resin-sync.yml' : - $ cat $PWD/resin-sync.yml - source: src/ + $ cat $PWD/.resin-sync.yml + uuid: 7cf02a6 + destination: '/usr/src/app' before: 'echo Hello' + after: 'echo Done' ignore: - .git - node_modules/ - progress: true - verbose: false - Notice that explicitly passed command options override the ones set in the configuration file. + Command line options have precedence over the ones saved in '.resin-sync.yml'. + + If '.gitignore' is found in the source directory then all explicitly listed files will be + excluded from the syncing process. You can choose to change this default behavior with the + '--skip-gitignore' option. Examples: - $ resin sync MyApp - $ resin sync 7cf02a6 - $ resin sync 7cf02a6 --port 8080 - $ resin sync 7cf02a6 --ignore foo,bar - $ resin sync 7cf02a6 -v + $ resin sync 7cf02a6 --source . --destination /usr/src/app + $ resin sync 7cf02a6 -s /home/user/myResinProject -d /usr/src/app --before 'echo Hello' --after 'echo Done' + $ resin sync --ignore lib/ + $ resin sync --verbose false + $ resin sync ''' permission: 'user' primary: true options: [ signature: 'source' parameter: 'path' - description: 'custom source path' + description: 'local directory path to synchronize to device' alias: 's' + , + signature: 'destination' + parameter: 'path' + description: 'destination path on device' + alias: 'd' , signature: 'ignore' parameter: 'paths' description: 'comma delimited paths to ignore when syncing' alias: 'i' + , + signature: 'skip-gitignore' + boolean: true + description: 'do not parse excluded/included files from .gitignore' , signature: 'before' parameter: 'command' description: 'execute a command before syncing' alias: 'b' , - signature: 'progress' - boolean: true - description: 'show progress' - alias: 'p' + signature: 'after' + parameter: 'command' + description: 'execute a command after syncing' + alias: 'a' , signature: 'port' parameter: 'port' description: 'ssh port' alias: 't' + , + signature: 'progress' + boolean: true + description: 'show progress' + alias: 'p' , signature: 'verbose' boolean: true @@ -81,19 +126,37 @@ module.exports = , ] action: (params, options, done) -> + fs = require('fs') + path = require('path') resin = require('resin-sdk') + Promise = require('bluebird') resinSync = require('resin-sync') patterns = require('../utils/patterns') - # TODO: Add comma separated options to Capitano - if options.ignore? - options.ignore = options.ignore.split(',') + Promise.try -> + try + fs.accessSync(path.join(process.cwd(), '.resin-sync.yml')) + catch + if not options.source? + throw new Error('No --source option passed and no \'.resin-sync.yml\' file found in current directory.') - resin.models.device.has(params.destination).then (isValidUUID) -> - if isValidUUID - return params.destination + options.source ?= process.cwd() - return patterns.inferOrSelectDevice(params.destination) - .then (uuid) -> - resinSync.sync(uuid, options) + # TODO: Add comma separated options to Capitano + if options.ignore? + options.ignore = options.ignore.split(',') + + Promise.resolve(params.uuid) + .then (uuid) -> + if not uuid? + savedUuid = loadConfig(options.source).uuid + return patterns.inferOrSelectDevice(savedUuid) + + resin.models.device.has(uuid) + .then (hasDevice) -> + if not hasDevice + throw new Error("Device not found: #{uuid}") + return uuid + .then (uuid) -> + resinSync.sync(uuid, options) .nodeify(done) diff --git a/lib/utils/patterns.coffee b/lib/utils/patterns.coffee index 254598ec..c1ab8a4c 100644 --- a/lib/utils/patterns.coffee +++ b/lib/utils/patterns.coffee @@ -150,24 +150,19 @@ exports.awaitDevice = (uuid) -> console.info("Waiting for #{deviceName} to connect to resin...") poll().return(uuid) -exports.inferOrSelectDevice = (applicationName) -> - Promise.try -> - if applicationName? - return resin.models.device.getAllByApplication(applicationName) - return resin.models.device.getAll() +exports.inferOrSelectDevice = (preferredUuid) -> + resin.models.device.getAll() .filter (device) -> device.is_online - .then (devices) -> - if _.isEmpty(devices) - throw new Error('You don\'t have any devices') - - if devices.length is 1 - return _.first(devices).uuid + .then (onlineDevices) -> + if _.isEmpty(onlineDevices) + throw new Error('You don\'t have any devices online') return form.ask message: 'Select a device' type: 'list' - choices: _.map devices, (device) -> + 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 diff --git a/package.json b/package.json index 17bfb05f..52a4a060 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "resin-pine": "^1.3.0", "resin-sdk": "^5.3.5", "resin-settings-client": "^3.5.0", - "resin-sync": "^2.0.2", + "resin-sync": "^3.0.0", "resin-vcs": "^2.0.0", "rimraf": "^2.4.3", "rindle": "^1.0.0",