From 356d2ef6b2718e90a3595a61ec7e25e227e474fd Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Fri, 12 Feb 2016 14:34:16 -0400 Subject: [PATCH] Inject analytics in Capitano --- build/actions/app.js | 25 +++---------- build/actions/auth.js | 18 +++------- build/actions/device.js | 24 +++---------- build/actions/environment-variables.js | 45 +++++------------------- build/actions/keys.js | 14 ++------ build/app.js | 8 +++-- build/events.js | 37 +++++++++++++++++++ lib/actions/app.coffee | 8 ----- lib/actions/auth.coffee | 12 +------ lib/actions/device.coffee | 9 ----- lib/actions/environment-variables.coffee | 18 +++------- lib/actions/keys.coffee | 6 ---- lib/app.coffee | 6 +++- lib/events.coffee | 24 +++++++++++++ package.json | 2 +- 15 files changed, 105 insertions(+), 151 deletions(-) create mode 100644 build/events.js create mode 100644 lib/events.coffee diff --git a/build/actions/app.js b/build/actions/app.js index 44bf8f35..64dbda66 100644 --- a/build/actions/app.js +++ b/build/actions/app.js @@ -35,9 +35,8 @@ limitations under the License. permission: 'user', primary: true, action: function(params, options, done) { - var events, patterns, resin; + var patterns, resin; resin = require('resin-sdk'); - events = require('resin-cli-events'); patterns = require('../utils/patterns'); return resin.models.application.has(params.name).then(function(hasApplication) { if (hasApplication) { @@ -48,10 +47,7 @@ limitations under the License. }).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 + ")"); - return events.send('application.create', { - application: application.id - }); + return console.info("Application created: " + application.app_name + " (" + application.device_type + ", id " + application.id + ")"); }).nodeify(done); } }; @@ -79,15 +75,11 @@ limitations under the License. permission: 'user', primary: true, action: function(params, options, done) { - var events, resin, visuals; + var resin, visuals; resin = require('resin-sdk'); visuals = require('resin-cli-visuals'); - events = require('resin-cli-events'); return resin.models.application.get(params.name).then(function(application) { - console.log(visuals.table.vertical(application, ["$" + application.app_name + "$", 'id', 'device_type', 'git_repository', 'commit'])); - return events.send('application.open', { - application: application.id - }); + return console.log(visuals.table.vertical(application, ["$" + application.app_name + "$", 'id', 'device_type', 'git_repository', 'commit'])); }).nodeify(done); } }; @@ -111,18 +103,11 @@ limitations under the License. options: [commandOptions.yes], permission: 'user', action: function(params, options, done) { - var events, patterns, resin; + var patterns, resin; resin = require('resin-sdk'); - events = require('resin-cli-events'); patterns = require('../utils/patterns'); 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) { - return events.send('application.delete', { - application: application.id - }); - }); }).nodeify(done); } }; diff --git a/build/actions/auth.js b/build/actions/auth.js index 85c64bfd..1413f9a8 100644 --- a/build/actions/auth.js +++ b/build/actions/auth.js @@ -50,12 +50,11 @@ limitations under the License. ], primary: true, action: function(params, options, done) { - var Promise, _, auth, capitano, events, form, login, messages, patterns, resin; + var Promise, _, auth, capitano, form, login, messages, patterns, resin; _ = require('lodash'); Promise = require('bluebird'); capitano = Promise.promisifyAll(require('capitano')); resin = require('resin-sdk'); - events = require('resin-cli-events'); auth = require('resin-cli-auth'); form = require('resin-cli-form'); patterns = require('../utils/patterns'); @@ -91,7 +90,6 @@ limitations under the License. console.log("\nLogging in to " + resinUrl); return login(options); }).then(resin.auth.whoami).tap(function(username) { - events.send('user.login'); console.info("Successfully logged in as: " + username); return console.info("\nNow what?\n\n" + messages.gettingStarted + "\n\nFind out about more super powers by running:\n\n $ resin help\n\n" + messages.reachingOut); }).nodeify(done); @@ -104,12 +102,9 @@ limitations under the License. help: 'Use this command to logout from your resin.io account.o\n\nExamples:\n\n $ resin logout', permission: 'user', action: function(params, options, done) { - var events, resin; + var resin; resin = require('resin-sdk'); - events = require('resin-cli-events'); - return resin.auth.logout().then(function() { - return events.send('user.logout'); - }).nodeify(done); + return resin.auth.logout().nodeify(done); } }; @@ -118,10 +113,9 @@ limitations under the License. description: 'signup to resin.io', help: 'Use this command to signup for a resin.io account.\n\nIf signup is successful, you\'ll be logged in to your new user automatically.\n\nExamples:\n\n $ resin signup\n Email: me@mycompany.com\n Username: johndoe\n Password: ***********\n\n $ resin whoami\n johndoe', action: function(params, options, done) { - var events, form, resin, validation; + var form, resin, validation; resin = require('resin-sdk'); form = require('resin-cli-form'); - events = require('resin-cli-events'); validation = require('../utils/validation'); return resin.settings.get('resinUrl').then(function(resinUrl) { console.log("\nRegistering to " + resinUrl); @@ -142,9 +136,7 @@ limitations under the License. validate: validation.validatePassword } ]); - }).then(resin.auth.register).then(resin.auth.loginWithToken).tap(function() { - return events.send('user.signup'); - }).nodeify(done); + }).then(resin.auth.register).then(resin.auth.loginWithToken).nodeify(done); } }; diff --git a/build/actions/device.js b/build/actions/device.js index 7fa6f129..615af231 100644 --- a/build/actions/device.js +++ b/build/actions/device.js @@ -55,17 +55,13 @@ limitations under the License. permission: 'user', primary: true, action: function(params, options, done) { - var events, resin, visuals; + var resin, visuals; resin = require('resin-sdk'); visuals = require('resin-cli-visuals'); - events = require('resin-cli-events'); return resin.models.device.get(params.uuid).then(function(device) { return resin.models.device.getStatus(device).then(function(status) { device.status = status; - console.log(visuals.table.vertical(device, ["$" + device.name + "$", 'id', 'device_type', 'status', 'is_online', 'ip_address', 'application_name', 'last_seen', 'uuid', 'commit', 'supervisor_version', 'is_web_accessible', 'note'])); - return events.send('device.open', { - device: device.uuid - }); + return console.log(visuals.table.vertical(device, ["$" + device.name + "$", 'id', 'device_type', 'status', 'is_online', 'ip_address', 'application_name', 'last_seen', 'uuid', 'commit', 'supervisor_version', 'is_web_accessible', 'note'])); }); }).nodeify(done); } @@ -106,16 +102,11 @@ limitations under the License. options: [commandOptions.yes], permission: 'user', action: function(params, options, done) { - var events, patterns, resin; + var patterns, resin; resin = require('resin-sdk'); - events = require('resin-cli-events'); patterns = require('../utils/patterns'); 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', { - device: params.uuid - }); }).nodeify(done); } }; @@ -138,11 +129,10 @@ limitations under the License. help: 'Use this command to rename a device.\n\nIf you omit the name, you\'ll get asked for it interactively.\n\nExamples:\n\n $ resin device rename 7cf02a6\n $ resin device rename 7cf02a6 MyPi', permission: 'user', action: function(params, options, done) { - var Promise, _, events, form, resin; + var Promise, _, form, resin; Promise = require('bluebird'); _ = require('lodash'); resin = require('resin-sdk'); - events = require('resin-cli-events'); form = require('resin-cli-form'); return Promise["try"](function() { if (!_.isEmpty(params.newName)) { @@ -152,11 +142,7 @@ limitations under the License. message: 'How do you want to name this device?', type: 'input' }); - }).then(_.partial(resin.models.device.rename, params.uuid)).tap(function() { - return events.send('device.rename', { - device: params.uuid - }); - }).nodeify(done); + }).then(_.partial(resin.models.device.rename, params.uuid)).nodeify(done); } }; diff --git a/build/actions/environment-variables.js b/build/actions/environment-variables.js index d46c7654..ca9badaf 100644 --- a/build/actions/environment-variables.js +++ b/build/actions/environment-variables.js @@ -68,21 +68,14 @@ limitations under the License. options: [commandOptions.yes, commandOptions.booleanDevice], permission: 'user', action: function(params, options, done) { - var events, patterns, resin; + var patterns, resin; resin = require('resin-sdk'); - events = require('resin-cli-events'); patterns = require('../utils/patterns'); 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', { - id: params.id - }); + return resin.models.environmentVariables.device.remove(params.id); } else { - resin.models.environmentVariables.remove(params.id); - return events.send('environmentVariable.delete', { - id: params.id - }); + return resin.models.environmentVariables.remove(params.id); } }).nodeify(done); } @@ -95,10 +88,9 @@ limitations under the License. options: [commandOptions.optionalApplication, commandOptions.optionalDevice], permission: 'user', action: function(params, options, done) { - var Promise, events, resin; + var Promise, resin; Promise = require('bluebird'); resin = require('resin-sdk'); - events = require('resin-cli-events'); return Promise["try"](function() { if (params.value == null) { params.value = process.env[params.key]; @@ -109,19 +101,9 @@ limitations under the License. } } if (options.application != null) { - return resin.models.environmentVariables.create(options.application, params.key, params.value).then(function() { - return resin.models.application.get(options.application).then(function(application) { - return events.send('environmentVariable.create', { - application: application.id - }); - }); - }); + return resin.models.environmentVariables.create(options.application, params.key, params.value); } else if (options.device != null) { - return resin.models.environmentVariables.device.create(options.device, params.key, params.value).then(function() { - return events.send('deviceEnvironmentVariable.create', { - device: options.device - }); - }); + return resin.models.environmentVariables.device.create(options.device, params.key, params.value); } else { throw new Error('You must specify an application or device'); } @@ -136,23 +118,14 @@ limitations under the License. permission: 'user', options: [commandOptions.booleanDevice], action: function(params, options, done) { - var Promise, events, resin; + var Promise, resin; Promise = require('bluebird'); resin = require('resin-sdk'); - events = require('resin-cli-events'); return Promise["try"](function() { if (options.device) { - return resin.models.environmentVariables.device.update(params.id, params.value).then(function() { - return events.send('deviceEnvironmentVariable.edit', { - id: params.id - }); - }); + return resin.models.environmentVariables.device.update(params.id, params.value); } else { - return resin.models.environmentVariables.update(params.id, params.value).then(function() { - return events.send('environmentVariable.edit', { - id: params.id - }); - }); + return resin.models.environmentVariables.update(params.id, params.value); } }).nodeify(done); } diff --git a/build/actions/keys.js b/build/actions/keys.js index daf785bc..d44fc3d2 100644 --- a/build/actions/keys.js +++ b/build/actions/keys.js @@ -58,16 +58,11 @@ limitations under the License. options: [commandOptions.yes], permission: 'user', action: function(params, options, done) { - var events, patterns, resin; + var patterns, resin; resin = require('resin-sdk'); - events = require('resin-cli-events'); patterns = require('../utils/patterns'); 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', { - id: params.id - }); }).nodeify(done); } }; @@ -78,13 +73,12 @@ limitations under the License. help: 'Use this command to associate a new SSH key with your account.\n\nIf `path` is omitted, the command will attempt\nto read the SSH key from stdin.\n\nExamples:\n\n $ resin key add Main ~/.ssh/id_rsa.pub\n $ cat ~/.ssh/id_rsa.pub | resin key add Main', permission: 'user', action: function(params, options, done) { - var Promise, _, capitano, events, fs, resin; + var Promise, _, capitano, fs, resin; _ = require('lodash'); Promise = require('bluebird'); fs = Promise.promisifyAll(require('fs')); capitano = require('capitano'); resin = require('resin-sdk'); - events = require('resin-cli-events'); return Promise["try"](function() { if (params.path != null) { return fs.readFileAsync(params.path, { @@ -96,9 +90,7 @@ limitations under the License. return callback(null, data); }); }); - }).then(_.partial(resin.models.key.create, params.name)).tap(function() { - return events.send('publicKey.create'); - }).nodeify(done); + }).then(_.partial(resin.models.key.create, params.name)).nodeify(done); } }; diff --git a/build/app.js b/build/app.js index 47cb7f65..c9b7ff46 100644 --- a/build/app.js +++ b/build/app.js @@ -16,7 +16,7 @@ limitations under the License. */ (function() { - var Promise, _, actions, capitano, errors, plugins, resin, update; + var Promise, _, actions, capitano, errors, events, plugins, resin, update; _ = require('lodash'); @@ -30,6 +30,8 @@ limitations under the License. errors = require('./errors'); + events = require('./events'); + plugins = require('./utils/plugins'); update = require('./utils/update'); @@ -130,7 +132,9 @@ limitations under the License. plugins.register(/^resin-plugin-(.+)$/).then(function() { var cli; cli = capitano.parse(process.argv); - return capitano.executeAsync(cli); + return events.trackCommand(cli).then(function() { + return capitano.executeAsync(cli); + }); })["catch"](errors.handle); }).call(this); diff --git a/build/events.js b/build/events.js new file mode 100644 index 00000000..84a24a5c --- /dev/null +++ b/build/events.js @@ -0,0 +1,37 @@ +(function() { + var Mixpanel, Promise, _, packageJSON, resin; + + _ = require('lodash'); + + Mixpanel = require('mixpanel'); + + Promise = require('bluebird'); + + resin = require('resin-sdk'); + + packageJSON = require('../package.json'); + + exports.getLoggerInstance = _.memoize(function() { + return resin.models.config.getMixpanelToken().then(Mixpanel.init); + }); + + exports.trackCommand = function(capitanoCommand) { + return Promise.props({ + resinUrl: resin.settings.get('resinUrl'), + username: resin.auth.whoami(), + mixpanel: exports.getLoggerInstance() + }).then(function(data) { + return data.mixpanel.track("[CLI] " + capitanoCommand.command, { + distinct_id: data.username, + argv: process.argv.join(' '), + version: packageJSON.version, + node: process.version, + arch: process.arch, + resinUrl: data.resinUrl, + platform: process.platform, + command: capitanoCommand + }); + }); + }; + +}).call(this); diff --git a/lib/actions/app.coffee b/lib/actions/app.coffee index 00754d8c..a94aa5a7 100644 --- a/lib/actions/app.coffee +++ b/lib/actions/app.coffee @@ -46,7 +46,6 @@ exports.create = primary: true action: (params, options, done) -> resin = require('resin-sdk') - events = require('resin-cli-events') patterns = require('../utils/patterns') # Validate the the application name is available @@ -62,7 +61,6 @@ exports.create = return resin.models.application.create(params.name, deviceType) .then (application) -> console.info("Application created: #{application.app_name} (#{application.device_type}, id #{application.id})") - events.send('application.create', application: application.id) .nodeify(done) exports.list = @@ -109,7 +107,6 @@ exports.info = action: (params, options, done) -> resin = require('resin-sdk') visuals = require('resin-cli-visuals') - events = require('resin-cli-events') resin.models.application.get(params.name).then (application) -> console.log visuals.table.vertical application, [ @@ -119,7 +116,6 @@ exports.info = 'git_repository' 'commit' ] - events.send('application.open', application: application.id) .nodeify(done) exports.restart = @@ -155,12 +151,8 @@ exports.remove = permission: 'user' action: (params, options, done) -> resin = require('resin-sdk') - events = require('resin-cli-events') patterns = require('../utils/patterns') 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) -> - events.send('application.delete', application: application.id) .nodeify(done) diff --git a/lib/actions/auth.coffee b/lib/actions/auth.coffee index 0f8bfbc8..c78790e6 100644 --- a/lib/actions/auth.coffee +++ b/lib/actions/auth.coffee @@ -75,7 +75,6 @@ exports.login = Promise = require('bluebird') capitano = Promise.promisifyAll(require('capitano')) resin = require('resin-sdk') - events = require('resin-cli-events') auth = require('resin-cli-auth') form = require('resin-cli-form') patterns = require('../utils/patterns') @@ -110,8 +109,6 @@ exports.login = return login(options) .then(resin.auth.whoami) .tap (username) -> - events.send('user.login') - console.info("Successfully logged in as: #{username}") console.info """ @@ -140,11 +137,7 @@ exports.logout = permission: 'user' action: (params, options, done) -> resin = require('resin-sdk') - events = require('resin-cli-events') - - resin.auth.logout().then -> - events.send('user.logout') - .nodeify(done) + resin.auth.logout().nodeify(done) exports.signup = signature: 'signup' @@ -167,7 +160,6 @@ exports.signup = action: (params, options, done) -> resin = require('resin-sdk') form = require('resin-cli-form') - events = require('resin-cli-events') validation = require('../utils/validation') resin.settings.get('resinUrl').then (resinUrl) -> @@ -191,8 +183,6 @@ exports.signup = .then(resin.auth.register) .then(resin.auth.loginWithToken) - .tap -> - events.send('user.signup') .nodeify(done) exports.whoami = diff --git a/lib/actions/device.coffee b/lib/actions/device.coffee index 9bcb30ce..67e3d32b 100644 --- a/lib/actions/device.coffee +++ b/lib/actions/device.coffee @@ -76,7 +76,6 @@ exports.info = action: (params, options, done) -> resin = require('resin-sdk') visuals = require('resin-cli-visuals') - events = require('resin-cli-events') resin.models.device.get(params.uuid).then (device) -> @@ -98,8 +97,6 @@ exports.info = 'is_web_accessible' 'note' ] - - events.send('device.open', device: device.uuid) .nodeify(done) exports.register = @@ -151,13 +148,10 @@ exports.remove = permission: 'user' action: (params, options, done) -> resin = require('resin-sdk') - events = require('resin-cli-events') patterns = require('../utils/patterns') 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) .nodeify(done) exports.identify = @@ -195,7 +189,6 @@ exports.rename = Promise = require('bluebird') _ = require('lodash') resin = require('resin-sdk') - events = require('resin-cli-events') form = require('resin-cli-form') Promise.try -> @@ -206,8 +199,6 @@ exports.rename = type: 'input' .then(_.partial(resin.models.device.rename, params.uuid)) - .tap -> - events.send('device.rename', device: params.uuid) .nodeify(done) exports.move = diff --git a/lib/actions/environment-variables.coffee b/lib/actions/environment-variables.coffee index e483b3b7..3a195b4b 100644 --- a/lib/actions/environment-variables.coffee +++ b/lib/actions/environment-variables.coffee @@ -99,16 +99,13 @@ exports.remove = permission: 'user' action: (params, options, done) -> resin = require('resin-sdk') - events = require('resin-cli-events') patterns = require('../utils/patterns') 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) else resin.models.environmentVariables.remove(params.id) - events.send('environmentVariable.delete', id: params.id) .nodeify(done) exports.add = @@ -140,7 +137,6 @@ exports.add = action: (params, options, done) -> Promise = require('bluebird') resin = require('resin-sdk') - events = require('resin-cli-events') Promise.try -> if not params.value? @@ -152,12 +148,9 @@ exports.add = console.info("Warning: using #{params.key}=#{params.value} from host environment") if options.application? - resin.models.environmentVariables.create(options.application, params.key, params.value).then -> - resin.models.application.get(options.application).then (application) -> - events.send('environmentVariable.create', application: application.id) + resin.models.environmentVariables.create(options.application, params.key, params.value) else if options.device? - resin.models.environmentVariables.device.create(options.device, params.key, params.value).then -> - events.send('deviceEnvironmentVariable.create', device: options.device) + resin.models.environmentVariables.device.create(options.device, params.key, params.value) else throw new Error('You must specify an application or device') .nodeify(done) @@ -180,13 +173,10 @@ exports.rename = action: (params, options, done) -> Promise = require('bluebird') resin = require('resin-sdk') - events = require('resin-cli-events') Promise.try -> if options.device - resin.models.environmentVariables.device.update(params.id, params.value).then -> - events.send('deviceEnvironmentVariable.edit', id: params.id) + resin.models.environmentVariables.device.update(params.id, params.value) else - resin.models.environmentVariables.update(params.id, params.value).then -> - events.send('environmentVariable.edit', id: params.id) + resin.models.environmentVariables.update(params.id, params.value) .nodeify(done) diff --git a/lib/actions/keys.coffee b/lib/actions/keys.coffee index f24e7421..f813e2fc 100644 --- a/lib/actions/keys.coffee +++ b/lib/actions/keys.coffee @@ -83,13 +83,10 @@ exports.remove = permission: 'user' action: (params, options, done) -> resin = require('resin-sdk') - events = require('resin-cli-events') patterns = require('../utils/patterns') 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) .nodeify(done) exports.add = @@ -113,7 +110,6 @@ exports.add = fs = Promise.promisifyAll(require('fs')) capitano = require('capitano') resin = require('resin-sdk') - events = require('resin-cli-events') Promise.try -> return fs.readFileAsync(params.path, encoding: 'utf8') if params.path? @@ -123,6 +119,4 @@ exports.add = return callback(null, data) .then(_.partial(resin.models.key.create, params.name)) - .tap -> - events.send('publicKey.create') .nodeify(done) diff --git a/lib/app.coffee b/lib/app.coffee index 9321cdab..a401442c 100644 --- a/lib/app.coffee +++ b/lib/app.coffee @@ -20,6 +20,7 @@ capitano = Promise.promisifyAll(require('capitano')) resin = require('resin-sdk') actions = require('./actions') errors = require('./errors') +events = require('./events') plugins = require('./utils/plugins') update = require('./utils/update') @@ -107,5 +108,8 @@ update.notify() plugins.register(/^resin-plugin-(.+)$/).then -> cli = capitano.parse(process.argv) - capitano.executeAsync(cli) + + events.trackCommand(cli).then -> + capitano.executeAsync(cli) + .catch(errors.handle) diff --git a/lib/events.coffee b/lib/events.coffee new file mode 100644 index 00000000..91010c78 --- /dev/null +++ b/lib/events.coffee @@ -0,0 +1,24 @@ +_ = require('lodash') +Mixpanel = require('mixpanel') +Promise = require('bluebird') +resin = require('resin-sdk') +packageJSON = require('../package.json') + +exports.getLoggerInstance = _.memoize -> + return resin.models.config.getMixpanelToken().then(Mixpanel.init) + +exports.trackCommand = (capitanoCommand) -> + return Promise.props + resinUrl: resin.settings.get('resinUrl') + username: resin.auth.whoami() + mixpanel: exports.getLoggerInstance() + .then (data) -> + data.mixpanel.track "[CLI] #{capitanoCommand.command}", + distinct_id: data.username + argv: process.argv.join(' ') + version: packageJSON.version + node: process.version + arch: process.arch + resinUrl: data.resinUrl + platform: process.platform + command: capitanoCommand diff --git a/package.json b/package.json index 0817c473..1a8b1a66 100644 --- a/package.json +++ b/package.json @@ -38,13 +38,13 @@ "columnify": "^1.5.2", "is-root": "^1.0.0", "lodash": "^3.10.0", + "mixpanel": "^0.4.0", "moment": "^2.10.6", "nplugm": "^3.0.0", "president": "^2.0.1", "prettyjson": "^1.1.3", "resin-cli-auth": "^1.0.0", "resin-cli-errors": "^1.0.0", - "resin-cli-events": "^1.0.2", "resin-cli-form": "^1.4.0", "resin-cli-visuals": "^1.2.2", "resin-config-json": "^1.0.0",