diff options
| -rw-r--r-- | README.md | 12 | ||||
| -rw-r--r-- | app.js | 16 | ||||
| -rw-r--r-- | config.json.example | 3 | ||||
| -rw-r--r-- | lib/config/default.js | 1 | ||||
| -rw-r--r-- | lib/config/environment.js | 2 | ||||
| -rw-r--r-- | lib/config/index.js | 6 | ||||
| -rw-r--r-- | lib/history.js | 15 | ||||
| -rw-r--r-- | lib/migrations/20150702001020-update-to-0_3_1.js | 6 | ||||
| -rw-r--r-- | lib/migrations/20160112220142-note-add-lastchange.js | 6 | ||||
| -rw-r--r-- | lib/migrations/20160420180355-note-add-alias.js | 6 | ||||
| -rw-r--r-- | lib/migrations/20160515114000-user-add-tokens.js | 6 | ||||
| -rw-r--r-- | lib/migrations/20160607060246-support-revision.js | 6 | ||||
| -rw-r--r-- | lib/migrations/20160703062241-support-authorship.js | 6 | ||||
| -rw-r--r-- | lib/migrations/20161009040430-support-delete-note.js | 8 | ||||
| -rw-r--r-- | lib/migrations/20161201050312-support-email-signin.js | 14 | ||||
| -rw-r--r-- | lib/models/note.js | 6 | ||||
| -rw-r--r-- | lib/response.js | 4 | ||||
| -rw-r--r-- | package.json | 2 | ||||
| -rw-r--r-- | public/js/index.js | 17 | ||||
| -rw-r--r-- | public/views/codimd/body.ejs | 2 | ||||
| -rw-r--r-- | public/views/index/body.ejs | 2 | ||||
| -rw-r--r-- | yarn.lock | 6 | 
22 files changed, 131 insertions, 21 deletions
@@ -29,6 +29,7 @@ Thanks for using! :smile:    - [Heroku Deployment](#heroku-deployment)    - [Kubernetes](#kubernetes)    - [CodiMD by docker container](#codimd-by-docker-container) +  - [Cloudron](#cloudron)  - [Upgrade](#upgrade)    - [Native setup](#native-setup)  - [Configuration](#configuration) @@ -121,6 +122,12 @@ docker-compose up  ```  Read more about it in the [docker repository…](https://github.com/hackmdio/docker-hackmd) +## Cloudron + +Install CodiMD on [Cloudron](https://cloudron.io): + +[](https://cloudron.io/button.html?app=io.hackmd.cloudronapp) +  # Upgrade  ## Native setup @@ -170,7 +177,9 @@ There are some config settings you need to change in the files below.  | `DEBUG` | `true` or `false` | set debug mode; show more logs |  | `CMD_DOMAIN` | `codimd.org` | domain name |  | `CMD_URL_PATH` | `codimd` | sub URL path, like `www.example.com/<URL_PATH>` | +| `CMD_HOST` | `localhost` | host to listen on |  | `CMD_PORT` | `80` | web app port | +| `CMD_PATH` | `/var/run/codimd.sock` | path to UNIX domain socket to listen on (if specified, `CMD_HOST` and `CMD_PORT` are ignored) |  | `CMD_ALLOW_ORIGIN` | `localhost, codimd.org` | domain name whitelist (use comma to separate) |  | `CMD_PROTOCOL_USESSL` | `true` or `false` | set to use SSL protocol for resources path (only applied when domain is set) |  | `CMD_URL_ADDPORT` | `true` or `false` | set to add port on callback URL (ports `80` or `443` won't be applied) (only applied when domain is set) | @@ -192,6 +201,7 @@ There are some config settings you need to change in the files below.  | `CMD_GITLAB_BASEURL` | no example | GitLab authentication endpoint, set to use other endpoint than GitLab.com (optional) |  | `CMD_GITLAB_CLIENTID` | no example | GitLab API client id |  | `CMD_GITLAB_CLIENTSECRET` | no example | GitLab API client secret | +| `CMD_GITLAB_VERSION` | no example | GitLab API version (v3 or v4) |  | `CMD_MATTERMOST_BASEURL` | no example | Mattermost authentication endpoint |  | `CMD_MATTERMOST_CLIENTID` | no example | Mattermost API client id |  | `CMD_MATTERMOST_CLIENTSECRET` | no example | Mattermost API client secret | @@ -252,7 +262,9 @@ There are some config settings you need to change in the files below.  | `debug` | `true` or `false` | set debug mode, show more logs |  | `domain` | `localhost` | domain name |  | `urlPath` | `codimd` | sub URL path, like `www.example.com/<urlpath>` | +| `host` | `localhost` | host to listen on |  | `port` | `80` | web app port | +| `path` | `/var/run/codimd.sock` | path to UNIX domain socket to listen on (if specified, `host` and `port` are ignored) |  | `allowOrigin` | `['localhost']` | domain name whitelist |  | `useSSL` | `true` or `false` | set to use SSL server (if `true`, will auto turn on `protocolUseSSL`) |  | `hsts` | `{"enable": true, "maxAgeSeconds": 31536000, "includeSubdomains": true, "preload": true}` | [HSTS](https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security) options to use with HTTPS (default is the example value, max age is a year) | @@ -205,11 +205,21 @@ io.sockets.on('connection', realtime.connection)  // listen  function startListen () { -  server.listen(config.port, function () { +  var address +  var listenCallback = function () {      var schema = config.useSSL ? 'HTTPS' : 'HTTP' -    logger.info('%s Server listening at port %d', schema, config.port) +    logger.info('%s Server listening at %s', schema, address)      realtime.maintenance = false -  }) +  } + +  // use unix domain socket if 'path' is specified +  if (config.path) { +    address = config.path +    server.listen(config.path, listenCallback) +  } else { +    address = config.host + ':' + config.port +    server.listen(config.port, config.host, listenCallback) +  }  }  // sync db then start listen diff --git a/config.json.example b/config.json.example index 1f2ec3d5..16c95509 100644 --- a/config.json.example +++ b/config.json.example @@ -55,7 +55,8 @@              "baseURL": "change this",              "clientID": "change this",              "clientSecret": "change this", -            "scope": "use 'read_user' scope for auth user only or remove this property if you need gitlab snippet import/export support (will result to be default scope 'api')" +            "scope": "use 'read_user' scope for auth user only or remove this property if you need gitlab snippet import/export support (will result to be default scope 'api')", +            "version": "use 'v4' if gitlab version > 11, 'v3' otherwise. Default to 'v4'"          },          "mattermost": {              "baseURL": "change this", diff --git a/lib/config/default.js b/lib/config/default.js index 5c39a4da..6096bce4 100644 --- a/lib/config/default.js +++ b/lib/config/default.js @@ -3,6 +3,7 @@  module.exports = {    domain: '',    urlPath: '', +  host: '0.0.0.0',    port: 3000,    urlAddPort: false,    allowOrigin: ['localhost'], diff --git a/lib/config/environment.js b/lib/config/environment.js index d850ac9d..6c4ce92f 100644 --- a/lib/config/environment.js +++ b/lib/config/environment.js @@ -5,7 +5,9 @@ const {toBooleanConfig, toArrayConfig, toIntegerConfig} = require('./utils')  module.exports = {    domain: process.env.CMD_DOMAIN,    urlPath: process.env.CMD_URL_PATH, +  host: process.env.CMD_HOST,    port: toIntegerConfig(process.env.CMD_PORT), +  path: process.env.CMD_PATH,    urlAddPort: toBooleanConfig(process.env.CMD_URL_ADDPORT),    useSSL: toBooleanConfig(process.env.CMD_USESSL),    hsts: { diff --git a/lib/config/index.js b/lib/config/index.js index ac03fcd4..f96684ea 100644 --- a/lib/config/index.js +++ b/lib/config/index.js @@ -103,6 +103,12 @@ config.isSAMLEnable = config.saml.idpSsoUrl  config.isOAuth2Enable = config.oauth2.clientID && config.oauth2.clientSecret  config.isPDFExportEnable = config.allowPDFExport +// Check gitlab api version +if (config.gitlab.version !== 'v4' && config.gitlab.version !== 'v3') { +  logger.warn('config.js contains wrong version (' + config.gitlab.version + ') for gitlab api; it should be \'v3\' or \'v4\'. Defaulting to v4') +  config.gitlab.version = 'v4' +} +  // Only update i18n files in development setups  config.updateI18nFiles = (env === Environment.development) diff --git a/lib/history.js b/lib/history.js index c7d2472c..9c389bfa 100644 --- a/lib/history.js +++ b/lib/history.js @@ -31,6 +31,15 @@ function getHistory (userid, callback) {        history = JSON.parse(user.history)        // migrate LZString encoded note id to base64url encoded note id        for (let i = 0, l = history.length; i < l; i++) { +        // Calculate minimal string length for an UUID that is encoded +        // base64 encoded and optimize comparsion by using -1 +        // this should make a lot of LZ-String parsing errors obsolete +        // as we can assume that a nodeId that is 48 chars or longer is a +        // noteID. +        const base64UuidLength = ((4 * 36) / 3) - 1 +        if (!(history[i].id.length > base64UuidLength)) { +          continue +        }          try {            let id = LZString.decompressFromBase64(history[i].id)            if (id && models.Note.checkNoteIdValid(id)) { @@ -38,7 +47,11 @@ function getHistory (userid, callback) {            }          } catch (err) {            // most error here comes from LZString, ignore -          logger.error(err) +          if (err.message === 'Cannot read property \'charAt\' of undefined') { +            logger.warning('Looks like we can not decode "' + history[i].id + '" with LZString. Can be ignored.') +          } else { +            logger.error(err) +          }          }        }        history = parseHistoryToObject(history) diff --git a/lib/migrations/20150702001020-update-to-0_3_1.js b/lib/migrations/20150702001020-update-to-0_3_1.js index 40d9c97a..e661a343 100644 --- a/lib/migrations/20150702001020-update-to-0_3_1.js +++ b/lib/migrations/20150702001020-update-to-0_3_1.js @@ -20,6 +20,12 @@ module.exports = {          type: Sequelize.INTEGER,          defaultValue: 0        }) +    }).catch(function (error) { +      if (error.message === "ER_DUP_FIELDNAME: Duplicate column name 'shortid'") { +        console.log('Migration has already run… ignoring.') +      } else { +        throw error +      }      })    }, diff --git a/lib/migrations/20160112220142-note-add-lastchange.js b/lib/migrations/20160112220142-note-add-lastchange.js index b4e111b3..d0030d6b 100644 --- a/lib/migrations/20160112220142-note-add-lastchange.js +++ b/lib/migrations/20160112220142-note-add-lastchange.js @@ -7,6 +7,12 @@ module.exports = {        return queryInterface.addColumn('Notes', 'lastchangeAt', {          type: Sequelize.DATE        }) +    }).catch(function (error) { +      if (error.message === "ER_DUP_FIELDNAME: Duplicate column name 'lastchangeuserId'") { +        console.log('Migration has already run… ignoring.') +      } else { +        throw error +      }      })    }, diff --git a/lib/migrations/20160420180355-note-add-alias.js b/lib/migrations/20160420180355-note-add-alias.js index a043cd5c..4bad29ca 100644 --- a/lib/migrations/20160420180355-note-add-alias.js +++ b/lib/migrations/20160420180355-note-add-alias.js @@ -7,6 +7,12 @@ module.exports = {        return queryInterface.addIndex('Notes', ['alias'], {          indicesType: 'UNIQUE'        }) +    }).catch(function (error) { +      if (error.message === "ER_DUP_FIELDNAME: Duplicate column name 'alias'") { +        console.log('Migration has already run… ignoring.') +      } else { +        throw error +      }      })    }, diff --git a/lib/migrations/20160515114000-user-add-tokens.js b/lib/migrations/20160515114000-user-add-tokens.js index 4d5818be..4245f1ad 100644 --- a/lib/migrations/20160515114000-user-add-tokens.js +++ b/lib/migrations/20160515114000-user-add-tokens.js @@ -3,6 +3,12 @@ module.exports = {    up: function (queryInterface, Sequelize) {      return queryInterface.addColumn('Users', 'accessToken', Sequelize.STRING).then(function () {        return queryInterface.addColumn('Users', 'refreshToken', Sequelize.STRING) +    }).catch(function (error) { +      if (error.message === "ER_DUP_FIELDNAME: Duplicate column name 'accessToken'") { +        console.log('Migration has already run… ignoring.') +      } else { +        throw error +      }      })    }, diff --git a/lib/migrations/20160607060246-support-revision.js b/lib/migrations/20160607060246-support-revision.js index bcab97e3..10f288b0 100644 --- a/lib/migrations/20160607060246-support-revision.js +++ b/lib/migrations/20160607060246-support-revision.js @@ -15,6 +15,12 @@ module.exports = {          createdAt: Sequelize.DATE,          updatedAt: Sequelize.DATE        }) +    }).catch(function (error) { +      if (error.message === "ER_DUP_FIELDNAME: Duplicate column name 'savedAt'") { +        console.log('Migration has already run… ignoring.') +      } else { +        throw error +      }      })    }, diff --git a/lib/migrations/20160703062241-support-authorship.js b/lib/migrations/20160703062241-support-authorship.js index d73923b0..b3ced8c4 100644 --- a/lib/migrations/20160703062241-support-authorship.js +++ b/lib/migrations/20160703062241-support-authorship.js @@ -16,6 +16,12 @@ module.exports = {          createdAt: Sequelize.DATE,          updatedAt: Sequelize.DATE        }) +    }).catch(function (error) { +      if (error.message === "ER_DUP_FIELDNAME: Duplicate column name 'authorship'") { +        console.log('Migration has already run… ignoring.') +      } else { +        throw error +      }      })    }, diff --git a/lib/migrations/20161009040430-support-delete-note.js b/lib/migrations/20161009040430-support-delete-note.js index a39d1086..4df7a81c 100644 --- a/lib/migrations/20161009040430-support-delete-note.js +++ b/lib/migrations/20161009040430-support-delete-note.js @@ -1,7 +1,13 @@  'use strict'  module.exports = {    up: function (queryInterface, Sequelize) { -    return queryInterface.addColumn('Notes', 'deletedAt', Sequelize.DATE) +    return queryInterface.addColumn('Notes', 'deletedAt', Sequelize.DATE).catch(function (error) { +      if (error.message === "ER_DUP_FIELDNAME: Duplicate column name 'deletedAt'") { +        console.log('Migration has already run… ignoring.') +      } else { +        throw error +      } +    })    },    down: function (queryInterface, Sequelize) { diff --git a/lib/migrations/20161201050312-support-email-signin.js b/lib/migrations/20161201050312-support-email-signin.js index d33225f1..4653e67a 100644 --- a/lib/migrations/20161201050312-support-email-signin.js +++ b/lib/migrations/20161201050312-support-email-signin.js @@ -2,7 +2,19 @@  module.exports = {    up: function (queryInterface, Sequelize) {      return queryInterface.addColumn('Users', 'email', Sequelize.TEXT).then(function () { -      return queryInterface.addColumn('Users', 'password', Sequelize.TEXT) +      return queryInterface.addColumn('Users', 'password', Sequelize.TEXT).catch(function (error) { +        if (error.message === "ER_DUP_FIELDNAME: Duplicate column name 'password'") { +          console.log('Migration has already run… ignoring.') +        } else { +          throw error +        } +      }) +    }).catch(function (error) { +      if (error.message === "ER_DUP_FIELDNAME: Duplicate column name 'email'") { +        console.log('Migration has already run… ignoring.') +      } else { +        throw error +      }      })    }, diff --git a/lib/models/note.js b/lib/models/note.js index ec7e2b13..0e8dd4dd 100644 --- a/lib/models/note.js +++ b/lib/models/note.js @@ -227,7 +227,11 @@ module.exports = function (sequelize, DataTypes) {                var id = LZString.decompressFromBase64(noteId)                if (id && Note.checkNoteIdValid(id)) { return callback(null, id) } else { return _callback(null, null) }              } catch (err) { -              logger.error(err) +              if (err.message === 'Cannot read property \'charAt\' of undefined') { +                logger.warning('Looks like we can not decode "' + noteId + '" with LZString. Can be ignored.') +              } else { +                logger.error(err) +              }                return _callback(null, null)              }            }, diff --git a/lib/response.js b/lib/response.js index 3a31c511..37211998 100644 --- a/lib/response.js +++ b/lib/response.js @@ -573,11 +573,11 @@ function gitlabActionProjects (req, res, note) {        }      }).then(function (user) {        if (!user) { return response.errorNotFound(res) } -      var ret = { baseURL: config.gitlab.baseURL } +      var ret = { baseURL: config.gitlab.baseURL, version: config.gitlab.version }        ret.accesstoken = user.accessToken        ret.profileid = user.profileid        request( -                config.gitlab.baseURL + '/api/v3/projects?access_token=' + user.accessToken, +                config.gitlab.baseURL + '/api/' + config.gitlab.version + '/projects?access_token=' + user.accessToken,                  function (error, httpResponse, body) {                    if (!error && httpResponse.statusCode === 200) {                      ret.projects = JSON.parse(body) diff --git a/package.json b/package.json index 88bef3e0..1740500c 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@      "markdown-it-regexp": "^0.4.0",      "markdown-it-sub": "^1.0.0",      "markdown-it-sup": "^1.0.0", -    "markdown-pdf": "^8.0.0", +    "markdown-pdf": "^9.0.0",      "mathjax": "~2.7.0",      "mermaid": "~7.1.0",      "mattermost": "^3.4.0", diff --git a/public/js/index.js b/public/js/index.js index 6e13fe9c..1330deac 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -970,6 +970,7 @@ ui.toolbar.export.snippet.click(function () {          .done(function (data) {            $('#snippetExportModalAccessToken').val(data.accesstoken)            $('#snippetExportModalBaseURL').val(data.baseURL) +          $('#snippetExportModalVersion').val(data.version)            $('#snippetExportModalLoading').hide()            $('#snippetExportModal').modal('toggle')            $('#snippetExportModalProjects').find('option').remove().end().append('<option value="init" selected="selected" disabled="disabled">Select From Available Projects</option>') @@ -1021,6 +1022,7 @@ ui.toolbar.import.snippet.click(function () {          .done(function (data) {            $('#snippetImportModalAccessToken').val(data.accesstoken)            $('#snippetImportModalBaseURL').val(data.baseURL) +          $('#snippetImportModalVersion').val(data.version)            $('#snippetImportModalContent').prop('disabled', false)            $('#snippetImportModalConfirm').prop('disabled', false)            $('#snippetImportModalLoading').hide() @@ -1243,10 +1245,10 @@ ui.modal.snippetImportProjects.change(function () {    var accesstoken = $('#snippetImportModalAccessToken').val()    var baseURL = $('#snippetImportModalBaseURL').val()    var project = $('#snippetImportModalProjects').val() - +  var version = $('#snippetImportModalVersion').val()    $('#snippetImportModalLoading').show()    $('#snippetImportModalContent').val('/projects/' + project) -  $.get(baseURL + '/api/v3/projects/' + project + '/snippets?access_token=' + accesstoken) +  $.get(baseURL + '/api/' + version + '/projects/' + project + '/snippets?access_token=' + accesstoken)          .done(function (data) {            $('#snippetImportModalSnippets').find('option').remove().end().append('<option value="init" selected="selected" disabled="disabled">Select From Available Snippets</option>')            data.forEach(function (snippet) { @@ -1433,7 +1435,7 @@ $('#snippetImportModalConfirm').click(function () {    } else {      ui.spinner.show()      var accessToken = '?access_token=' + $('#snippetImportModalAccessToken').val() -    var fullURL = $('#snippetImportModalBaseURL').val() + '/api/v3' + snippeturl +    var fullURL = $('#snippetImportModalBaseURL').val() + '/api/' + $('#snippetImportModalVersion').val() + snippeturl      $.get(fullURL + accessToken)              .done(function (data) {                var content = '# ' + (data.title || 'Snippet Import') @@ -1470,15 +1472,19 @@ $('#snippetImportModalConfirm').click(function () {  $('#snippetExportModalConfirm').click(function () {    var accesstoken = $('#snippetExportModalAccessToken').val()    var baseURL = $('#snippetExportModalBaseURL').val() +  var version = $('#snippetExportModalVersion').val() +    var data = {      title: $('#snippetExportModalTitle').val(),      file_name: $('#snippetExportModalFileName').val(),      code: editor.getValue(), -    visibility_level: $('#snippetExportModalVisibility').val() +    visibility_level: $('#snippetExportModalVisibility').val(), +    visibility: $('#snippetExportModalVisibility').val() === 0 ? 'private' : ($('#snippetExportModalVisibility').val() === 10 ? 'internal' : '')    } +    if (!data.title || !data.file_name || !data.code || !data.visibility_level || !$('#snippetExportModalProjects').val()) return    $('#snippetExportModalLoading').show() -  var fullURL = baseURL + '/api/v3/projects/' + $('#snippetExportModalProjects').val() + '/snippets?access_token=' + accesstoken +  var fullURL = baseURL + '/api/' + version + '/projects/' + $('#snippetExportModalProjects').val() + '/snippets?access_token=' + accesstoken    $.post(fullURL          , data          , function (ret) { @@ -1487,7 +1493,6 @@ $('#snippetExportModalConfirm').click(function () {            var redirect = baseURL + '/' + $("#snippetExportModalProjects option[value='" + $('#snippetExportModalProjects').val() + "']").text() + '/snippets/' + ret.id            showMessageModal('<i class="fa fa-gitlab"></i> Export to Snippet', 'Export Successful!', redirect, 'View Snippet Here', true)          } -        , 'json'      )  }) diff --git a/public/views/codimd/body.ejs b/public/views/codimd/body.ejs index b5932a61..d4f27a93 100644 --- a/public/views/codimd/body.ejs +++ b/public/views/codimd/body.ejs @@ -153,6 +153,7 @@              <div class="modal-body">                  <input type="hidden" id="snippetImportModalAccessToken" />                  <input type="hidden" id="snippetImportModalBaseURL" /> +                <input type="hidden" id="snippetImportModalVersion" />                  <div class="ui-field-contain" style="display:table;margin-bottom:10px;width:100%;">                      <div style="display:table-row;margin-bottom:5px;">                          <label style="display:table-cell;">Project:</label> @@ -191,6 +192,7 @@              <div class="modal-body">                  <input type="hidden" id="snippetExportModalAccessToken" />                  <input type="hidden" id="snippetExportModalBaseURL" /> +                <input type="hidden" id="snippetExportModalVersion" />                  <div class="ui-field-contain" style="display:table;margin-bottom:10px;width:100%;">                      <div style="display:table-row;margin-bottom:5px;">                          <label style="display:table-cell;">Title:</label> diff --git a/public/views/index/body.ejs b/public/views/index/body.ejs index 29fa3181..53dbf2a2 100644 --- a/public/views/index/body.ejs +++ b/public/views/index/body.ejs @@ -152,7 +152,7 @@                          © 2018 <a href="https://hackmd.io">CodiMD</a> | <a href="<%- url %>/s/release-notes" target="_blank"><%= __('Releases') %></a><% if(privacyStatement) { %> | <a href="<%- url %>/s/privacy" target="_blank"><%= __('Privacy') %></a><% } %><% if(termsOfUse) { %> | <a href="<%- url %>/s/terms-of-use" target="_blank"><%= __('Terms of Use') %></a><% } %>                      </p>                      <h6 class="social-foot"> -                        <%- __('Follow us on %s and %s.', '<a href="https://github.com/hackmdio/CodiMD" target="_blank"><i class="fa fa-github"></i> GitHub</a>, <a href="https://twitter.com/hackmdio" target="_blank"><i class="fa fa-twitter"></i> Twitter</a>', '<a href="https://www.facebook.com/hackmdio" target="_blank"><i class="fa fa-facebook-square"></i> Facebook</a>') %> +                        <%- __('Follow us on %s and %s.', '<a href="https://github.com/hackmdio/CodiMD" target="_blank"><i class="fa fa-github"></i> GitHub</a>, <a href="https://riot.im/app/#/room/#codimd:matrix.org" target="_blank"><i class="fa fa-comments"></i> Riot</a>') %>                      </h6>                  </div>              </div> @@ -4698,9 +4698,9 @@ markdown-it@^8.2.2:      mdurl "^1.0.1"      uc.micro "^1.0.3" -markdown-pdf@^8.0.0: -  version "8.1.1" -  resolved "https://registry.yarnpkg.com/markdown-pdf/-/markdown-pdf-8.1.1.tgz#25c025d4f4f91869ac0f3f6fd7f7d32237669438" +markdown-pdf@^9.0.0: +  version "9.0.0" +  resolved "https://registry.yarnpkg.com/markdown-pdf/-/markdown-pdf-9.0.0.tgz#d699f29c3b6c41da4b9a2ec7d09ea8895daef146"    dependencies:      commander "^2.2.0"      duplexer "^0.1.1"  | 
