{"action":"created","comment":{"url":"https://api.github.com/repos/cozy-labs/cozy-light/pulls/comments/23007772","id":23007772,"diff_hunk":"@@ -1,1296 +1,93 @@\n-var _ = require('underscore');\n-var http = require('http');\n-var https = require('https');\n-var fs = require('fs');\n-var fsExtra = require('fs-extra');\n-var pathExtra = require('path-extra');\n-var npm = require('npm');\n-var request = require('request-json-light');\n-var express = require('express');\n-var bodyParser = require('body-parser');\n-var morgan = require('morgan');\n-var async = require('async');\n var printit = require('printit');\n+var pathExtra = require('path-extra');\n var Pouchdb = require('pouchdb');\n-var httpProxy = require('http-proxy');\n-var symbols = require('./symbols');\n-var distrib = require('./distrib');\n-var configWatcher = require('./config-watcher');\n-var nodeHelpers = require('./node-helpers');\n-\n-// Constants\n-const LOGGER = printit({ prefix: 'Cozy Light' });\n-const DEFAULT_PORT = 19104;\n-\n-// 'Global' variables\n-\n-var program = {};\n-var initialWd = process.cwd();\n-var home = '';\n-var configPath = '';\n-var routes = {};\n-var loadedApps = {};\n-var loadedPlugins = {};\n-var port = 18001;\n-var defaultAppsPort = port;\n-var proxy = null;\n-var config = null;\n-var server = null;\n-var db = null;\n-var expressLog = false;\n-\n-\n-// Helpers\n-\n-var configHelpers = {\n-\n-  /**\n-   * Return installation path module.\n-   *\n-   * @param {String} npmModule The name of the cozy-light module\n-   */\n-  modulePath: function (npmModule) {\n-    return pathExtra.join(home, 'node_modules', npmModule);\n-  },\n-\n-  /**\n-   * Load config file\n-   *\n-   * @return {Object} config\n-   */\n-  loadConfigFile: function () {\n-    config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));\n-    return config;\n-  },\n-\n-  /**\n-   * Save config file to disk.\n-   */\n-  saveConfig: function () {\n-    var configString = JSON.stringify(config, null, 2);\n-    fs.writeFileSync(configPath, configString);\n-  },\n-\n-  /**\n-  * Add a new application to the configuration file. The configuration file is\n-  * written in JSON. It adds an entry to the apps field. Name, display name,\n-  * version and description are required.\n-  *\n-  * @param {String} app The app name as it's typed by the user (user/repo).\n-  * @param {Object} manifest Manifest containing application fields.\n-  */\n-  addApp: function (app, manifest) {\n-    if (manifest.type === undefined) {\n-      manifest.type = 'classic';\n-    }\n-    config.apps[app] = {\n-      name: manifest.name,\n-      displayName: manifest.displayName,\n-      version: manifest.version,\n-      description: manifest.description,\n-      type: manifest.type\n-    };\n-    configHelpers.saveConfig();\n-  },\n-\n-  /**\n-  * Remove given application from the configuration file. The configuration file\n-  * is written in JSON.\n-  *\n-  * @param {String} app The app name as it's typed by the user (user/repo).\n-  */\n-  removeApp: function (app) {\n-    var ret = false;\n-    if (config.apps[app] !== undefined) {\n-      delete config.apps[app];\n-      configHelpers.saveConfig();\n-      ret = true;\n-    }\n-    return ret;\n-  },\n-\n-  /**\n-  * Add a new plugin to the configuration file. The configuration file is\n-  * written in JSON. It adds an entry to the plugins field. Name, display name,\n-  * version and description are required.\n-  *\n-  * @param {String} plugin The plugin name as it's typed by the user.\n-   *                        It s in the form user/repo.\n-  * @param {Object} manifest Manifest containing plugin fields.\n-  */\n-  addPlugin: function (plugin, manifest) {\n-    if (config.plugins === undefined) {\n-      config.plugins = {};\n-    }\n-\n-    config.plugins[plugin] = {\n-      name: manifest.name,\n-      displayName: manifest.displayName,\n-      version: manifest.version,\n-      description: manifest.description\n-    };\n-    configHelpers.saveConfig();\n-  },\n-\n-  /**\n-  * Remove given plugin from the configuration file.\n-  *\n-  * @param {String} plugin The plugin name as it's typed by the user\n-  * (user/repo).\n-  */\n-  removePlugin: function (plugin) {\n-    var ret = false;\n-    if (config.plugins[plugin] !== undefined) {\n-      delete config.plugins[plugin];\n-      configHelpers.saveConfig();\n-      ret = true;\n-    }\n-    return ret;\n-  },\n-\n-\n-  /**\n-   * Remove from given app or plugin the disabled field.\n-   *\n-   * @param {String} apporplugin The plugin/app name as it's typed by the user.\n-   */\n-  enable: function (apporplugin) {\n-    if (config.plugins[apporplugin] !== undefined) {\n-      config.plugins[apporplugin].disabled = undefined;\n-      configHelpers.saveConfig();\n-    } else if (config.apps[apporplugin] !== undefined) {\n-      config.apps[apporplugin].disabled = undefined;\n-      configHelpers.saveConfig();\n-    } else {\n-      throw new Error(\n-          'Cannot disable, given app or plugin is not configured.');\n-    }\n-\n-  },\n-\n-  /**\n-   * Add to given app or plugin a disabled field.\n-   *\n-   * @param {String} apporplugin The plugin/app name as it's typed by the user.\n-   */\n-  disable: function (apporplugin) {\n-    if (config.plugins[apporplugin] !== undefined) {\n-      config.plugins[apporplugin].disabled = true;\n-      configHelpers.saveConfig();\n-    } else if (config.apps[apporplugin] !== undefined) {\n-      config.apps[apporplugin].disabled = true;\n-      configHelpers.saveConfig();\n-    } else {\n-      throw new Error(\n-          'Cannot disable, given app or plugin is not configured.');\n-    }\n-  },\n-\n-  /**\n-   * Create config file if it doesn't exist\n-   */\n-  createConfigFile: function () {\n-    var exists = fs.existsSync(configPath);\n-    if (!exists) {\n-      config = { apps: {} };\n-      configHelpers.saveConfig();\n-    } else {\n-      config = configHelpers.loadConfigFile();\n-    }\n-  },\n-\n-  /**\n-   * Copy given dependency to app folder to avoid apps to fetch and rebuild it\n-   * every time it's required as dependency.\n-   * Most dependencies are common and can be reused.\n-   */\n-  copyDependency: function (name) {\n-    var destPath = configHelpers.modulePath(name);\n-    var sourcePath = pathExtra.join(__dirname, '..', 'node_modules', name);\n-\n-    if (!fs.existsSync(destPath)) {\n-      fsExtra.copySync(sourcePath, destPath);\n-    }\n-  },\n-\n-  /**\n-   * Init happens in several steps:\n-   * - Set configHelpers HOME and CONFIG_PATH\n-   * - Create the home folder\n-   * - Change current working directory to home folder\n-   * - Initialize a default config file if none exists\n-   * - Manage config file watcher subscriptions\n-   * - Load and return config file.\n-   *\n-   * default home dir is ~/.cozy-light\n-   * default config path is ~/.cozy-light/package.json\n-   *\n-   * @param {String} customHome The home path where .cozy-light folder belongs\n-   *\n-   * @return {Object} config\n-   */\n-  init: function (customHome, options) {\n-    home = pathExtra.join(customHome, '.cozy-light');\n-    program = options || program;\n-    configPath = pathExtra.join(home, 'config.json');\n-\n-    fsExtra.mkdirsSync(home);\n-    process.chdir(home);\n-    db = new Pouchdb('cozy');\n-\n-    configHelpers.createConfigFile();\n-    configHelpers.copyDependency('pouchdb');\n-    config = configHelpers.loadConfigFile();\n-    Object.keys(config.plugins || {}).forEach(function (name) {\n-      pluginHelpers.loadPlugin(name);\n-    });\n-\n-    return config;\n-  },\n-\n-\n-  /**\n-   * Subscribe an handler\n-   * to config file updates\n-   *\n-   * @return {Object} config\n-   */\n-  watchConfig: function (newWatcher) {\n-    configWatcher.on(newWatcher);\n-  },\n-\n-  /**\n-   * unSubscribe an handler\n-   * to config file updates\n-   *\n-   * @return {Object} config\n-   */\n-  unwatchConfig: function (newWatcher) {\n-    configWatcher.off(newWatcher);\n-  },\n-\n-  /**\n-   * Prepare plugins to be sent as json file to the web UI.\n-   *\n-   * @returns {Array}\n-   */\n-  exportPlugins: function () {\n-    var plugins = {};\n-    Object.keys(config.plugins || {}).forEach(function(name){\n-      var template = '';\n-      if (loadedPlugins[name] && loadedPlugins[name].getTemplate) {\n-        template = loadedPlugins[name].getTemplate(config);\n-      }\n-      plugins[name] = {\n-        name: config.plugins[name].name,\n-        displayName: config.plugins[name].displayName,\n-        version: config.plugins[name].version,\n-        template: template\n-      };\n-    });\n-    return plugins;\n-  },\n-\n-  /**\n-   * Prepare apps to be sent as json file to the web UI.\n-   *\n-   * @returns {Array}\n-   */\n-  exportApps: function () {\n-    var apps = {};\n-    var baseUrl = configHelpers.getServerUrl();\n-    Object.keys(config.apps || {}).forEach(function(name){\n-      apps[name] = {\n-        name: config.apps[name].name,\n-        displayName: config.apps[name].displayName,\n-        version: config.apps[name].version,\n-        url: baseUrl + '/apps/' + config.apps[name].name + '/'\n-      };\n-    });\n-    return apps;\n-  },\n-\n-  /**\n-   * @return {String} Application server host.\n-   */\n-  getHost: function () {\n-    return 'localhost';\n-  },\n-\n-  /**\n-   * Get application server port.\n-   * Take port from command line args, or config,\n-   * fallback to default one if none is set.\n-   *\n-   * @return {int} Application server port.\n-   */\n-  getServerPort: function (options) {\n-    var mainPort = DEFAULT_PORT;\n-    var infos = options || program;\n-    if (infos.port !== undefined) {\n-      mainPort = infos.port;\n-    } else if (config.port !== undefined) {\n-      mainPort = config.port;\n-    }\n-\n-    return mainPort;\n-  },\n-\n-  /**\n-   * @return {String} Application server url.\n-   * TODO: refactor with url module.\n-   */\n-  getServerUrl: function (options) {\n-    var resolvedHost = configHelpers.getHost(options);\n-    var resolvedPort = configHelpers.getServerPort(options);\n-    var serverUrl = '://' + resolvedHost + ':' + resolvedPort;\n-    if (config.ssl !== undefined) {\n-      serverUrl = 'https' + serverUrl;\n-    } else  {\n-      serverUrl = 'http' + serverUrl;\n-    }\n-\n-    return serverUrl;\n-  }\n-};\n-\n-var npmHelpers = {\n-\n-  /**\n-   * Fetch given app source and dependencies from NPM registry.\n-   *\n-   * Config file is ~/.cozy-light/.config\n-   *\n-   * @param {String} app App to fetch from NPM.\n-   * @param {Function} callback Callback to run once work is done.\n-   */\n-  install: function (app, callback) {\n-    npm.load({}, function () {\n-      npm.commands.install(home, [app], callback);\n-    });\n-  },\n-\n-  /**\n-   * Link given app source and dependencies from local file system.\n-   *\n-   * @param {String} app Path to the module to link.\n-   * @param {Function} callback Callback to run once work is done.\n-   */\n-  link: function (app, callback) {\n-    npm.load({ local: true}, function () {\n-      app = pathExtra.resolve(initialWd, app);\n-      npm.commands.link([app], callback);\n-    });\n-  },\n-\n-  /**\n-   * Remove application source and dependencies using NPM lib.\n-   *\n-   * @param {String} app App to fetch from NPM.\n-   * @param {Function} callback Callback to run once work is done.\n-   */\n-  uninstall: function (app, callback) {\n-    npm.load({}, function () {\n-      npm.commands.uninstall([app], callback);\n-    });\n-  },\n-\n-  /**\n-   * Fetch application or plugin manifest from an url or a path\n-   *\n-   * @param {String} app App or Plugin name to fetch from url or path.\n-   * @param {Function} callback Termination.\n-   */\n-  fetchManifest: function (app, callback) {\n-    var appPath = pathExtra.resolve(initialWd, app);\n-\n-    if (fs.existsSync(appPath)\n-        && fs.existsSync(pathExtra.join(appPath,'package.json'))) {\n-      var manifestPath = pathExtra.join(appPath,'package.json');\n-      fs.readFile(manifestPath, function checkError (err, manifest) {\n-        if (err) {\n-          LOGGER.error(err);\n-          callback(err);\n-        } else {\n-          callback(err, JSON.parse(manifest), 'file');\n-        }\n-      });\n-    } else {\n-      var client = request.newClient( 'https://raw.githubusercontent.com/');\n-      var manifestUrl = app + '/master/package.json';\n-\n-      client.get(manifestUrl, function (err, res, manifest) {\n-        if (res.statusCode !== 200) {\n-          LOGGER.error(err);\n-          callback(err);\n-        } else if (err) {\n-          LOGGER.error(err);\n-          callback(err);\n-        } else {\n-          callback(err, manifest, 'url');\n-        }\n-      });\n-    }\n-  },\n-\n-  /**\n-   * Fetch and install application or plugin from an url or a path\n-   *\n-   * @param {String} app App or Plugin name to fetch from url or path.\n-   * @param {Function} callback Termination.\n-   * TODO rename this function\n-   */\n-  fetchInstall: function (app, callback) {\n-    npmHelpers.fetchManifest(app, function(err, manifest, type){\n-      if (err) { return callback(err); }\n-      var cb = function (err) {\n-        callback(err, manifest, type);\n-      };\n-      if (type === 'file') {\n-        npmHelpers.link(app, cb);\n-      } else {\n-        npmHelpers.install(app, cb);\n-      }\n-    });\n-  }\n-};\n-\n-var pluginHelpers = {\n-\n-  loadPlugin: function (pluginName) {\n-      var pluginConfig = config.plugins[pluginName];\n-      if (pluginConfig.disabled !== true) {\n-        var pluginPath = configHelpers.modulePath(pluginConfig.name);\n-        try {\n-          var pluginModule = require(pluginPath);\n-        } catch (err) {\n-          LOGGER.error(\"Can't load \" + pluginName);\n-          return null;\n-        }\n-\n-        var options = {\n-          name: pluginModule.name,\n-          displayName: pluginConfig.displayName,\n-          version: pluginConfig.version,\n-          description: pluginConfig.description,\n-          configPath: configPath,\n-          /*eslint-disable */\n-          config_path: configPath, // for backward compatibility\n-          /*eslint-enable */\n-          home: home,\n-          npmHelpers: npmHelpers,\n-          cozyLight: cozyLight,\n-          proxy: proxy\n-        };\n-\n-        if (pluginModule.configure){\n-          pluginModule.configure(options, config, program);\n-        }\n-\n-        loadedPlugins[pluginName] = pluginModule;\n-\n-        return pluginModule;\n-    } else {\n-      return null;\n-    }\n-  },\n-\n-  /**\n-   * Start given plugin (require it, run configuration and apply change to\n-   * cozy light server).\n-   * NB: Do not load given plugin if it is disabled.\n-   *\n-   * @param pluginName The plugin name to start.\n-   * @param applicationServer The application server to connect the plugin on.\n-   * @param callback Termination.\n-   */\n-  start: function (pluginName, applicationServer, callback) {\n-    if (config.plugins[pluginName] === undefined) {\n-      LOGGER.error('Plugin ' + pluginName + ' not installed !');\n-      callback();\n-    } else {\n-      try {\n-        if (loadedPlugins[pluginName] === undefined) {\n-          pluginHelpers.loadPlugin(pluginName);\n-        }\n-        var plugin = loadedPlugins[pluginName];\n-\n-        if (plugin !== null) {\n-          if (plugin.configureAppServer !== undefined) {\n-            LOGGER.info('Configuring plugin ' + pluginName + '...');\n-            var logResult = function () {\n-              LOGGER.info('Plugin ' + pluginName + ' configured.');\n-              callback();\n-            };\n-            return plugin.configureAppServer(applicationServer, config, routes,\n-              logResult);\n-          }\n-        } else {\n-          callback();\n-        }\n-\n-      } catch(err) {\n-        LOGGER.raw(err);\n-        LOGGER.error('Plugin ' + pluginName + ' loading failed.');\n-        return callback(err);\n-      }\n-    }\n-  },\n-\n-  /**\n-   * Stop given plugin (unload it form cache and run its exit handleR).\n-   *\n-   * @param pluginName The plugin name to start.\n-   * @param callback Termination.\n-   */\n-  stop: function (pluginName, callback) {\n-    if (config.plugins[pluginName] === undefined) {\n-      LOGGER.error('Plugin ' + pluginName + ' not installed!');\n-      callback();\n-    } else if (loadedPlugins[pluginName] === undefined) {\n-      LOGGER.error('Plugin ' + pluginName + ' not started!');\n-      callback();\n-    } else {\n-      var options = config.plugins[pluginName];\n-      delete loadedPlugins[pluginName];\n-      try {\n-        var plugin = require(configHelpers.modulePath(options.name));\n-        nodeHelpers.clearRequireCache(configHelpers.modulePath(options.name));\n-        if (plugin.onExit !== undefined) {\n-          plugin.onExit(options, config, function (err) {\n-            if (err) {\n-              LOGGER.raw(err);\n-              LOGGER.info('\\t' + symbols.err + '\\t' + options.displayName + '');\n-            } else {\n-              LOGGER.info('\\t' + symbols.ok + '\\t' + options.displayName + '');\n-            }\n-            callback(err);\n-          });\n-        } else {\n-          LOGGER.info('\\t' + symbols.ok + '\\t' + options.displayName + '');\n-          return callback();\n-        }\n-      } catch(err) {\n-        LOGGER.raw(err);\n-        LOGGER.error('Plugin ' + pluginName + ' failed for termination.');\n-        callback(err);\n-      }\n-    }\n-  },\n-\n-  /**\n-   * Start all plugins.\n-   *\n-   * @param app Express app.\n-   * @param callback Termination.\n-   */\n-  startAll: function (app, callback) {\n-    var attachPlugin = function (pluginName, cb) {\n-      pluginHelpers.start(pluginName, app, cb);\n-    };\n-    async.eachSeries(Object.keys(config.plugins || {}),\n-      attachPlugin,\n-      callback);\n-  },\n-\n-  /**\n-   * Stop all plugins.\n-   *\n-   * @param callback Termination.\n-   */\n-  stopAll: function (callback) {\n-    var plugins = Object.keys(config.plugins || {});\n-    async.eachSeries(plugins, pluginHelpers.stop, callback);\n-  }\n-};\n-\n-\n-var applicationHelpers = {\n-\n-  /**\n-   * Start given classic application as a new listening server. List of\n-   * function to run on when configuration file is changed. List of function to\n-   * run on when configuration file is changed.\n-   * NB: Do not start given app if it is disabled.\n-   *\n-   * @param {Object} application The application to start.\n-   * @param {Object} db The database to give as parameter to the application\n-   *                      (they all share the same datastore).\n-   * @param {Function} callback Termination.\n-   */\n-  start: function (application, db, callback) {\n-\n-    if ((application.type === undefined || application.type === 'classic')\n-        && application.disabled !== true) {\n-\n-      var name = application.name;\n-\n-      var appModule;\n-      var modulePath = configHelpers.modulePath(name);\n-      try {\n-        var cozyFile = pathExtra.join(modulePath, 'cozy.js');\n-        appModule = require(cozyFile);\n-      } catch (err) {\n-        if(!err.toString().match(new RegExp(\"Cannot find module\"))) {\n-          LOGGER.raw(err);\n-        }\n-        try {\n-          appModule = require(configHelpers.modulePath(name));\n-        } catch (err_2) {\n-          LOGGER.raw(err_2);\n-        }\n-      }\n-\n-      if (appModule === undefined) {\n-        LOGGER.error('Can\\'t load application ' + name + '.');\n-        callback();\n-      } else if (appModule.start === undefined) {\n-        LOGGER.error('Can\\'t start application ' + name + '.');\n-        callback();\n-      } else {\n-\n-        loadedApps[name] = {\n-          appModule: appModule,\n-          server: null,\n-          sockets: [],\n-          watchers: []\n-        };\n-\n-        // use getPort from hosted application\n-        // to grab more than one port,\n-        // still ensure no app port conflict\n-        var options = {\n-          db: db,\n-          port: port,\n-          getPort: function(){\n-            return port++;\n-          },\n-          silent: true\n-        };\n-\n-        appModule.start(options, function (err, app, server) {\n-          if (err) { LOGGER.error(err); }\n-\n-          if (server !== undefined ){\n-            // to properly close the server,\n-            // and release all its resources\n-            nodeHelpers.clearCloseServer(server);\n-            loadedApps[name].server = server;\n-          }\n-          routes[name] = options.port;\n-          LOGGER.info(\n-            'Application ' + name + ' is now running ' +\n-            'on port ' + options.port + '...');\n-          port = port + 1;\n-          callback(err, app, server);\n-        });\n-      }\n-    } else {\n-      callback();\n-    }\n-  },\n-\n-  /**\n-   * Stop given classic application.\n-   *\n-   * @param application The application to stop.\n-   * @param {Function} callback Termination.\n-   */\n-  stop: function (application, callback) {\n-    var name = application.name;\n-\n-    if (loadedApps[name] !== undefined) {\n-      var appModule = loadedApps[name].appModule;\n-\n-      var closeServer = function () {\n-        try {\n-          if (loadedApps[name].server !== undefined ){\n-            loadedApps[name].server.close(function logInfo (err) {\n-              var appInfos = loadedApps[name];\n-              if (err) {\n-                LOGGER.raw(err);\n-                LOGGER.info(\n-                  '\\t' + symbols.err + '\\t' + application.displayName + '');\n-              } else {\n-                LOGGER.info(\n-                  '\\t' + symbols.ok + '\\t' + application.displayName + '');\n-              }\n-              nodeHelpers.clearRequireCache(configHelpers.modulePath(name));\n-              delete loadedApps[name];\n-              callback();\n-            });\n-          } else {\n-            nodeHelpers.clearRequireCache(configHelpers.modulePath(name));\n-            delete loadedApps[name];\n-            callback();\n-          }\n-        } catch (err) {\n-          LOGGER.raw(err);\n-          LOGGER.warn('An error occurred while stopping ' + name);\n-          nodeHelpers.clearRequireCache(configHelpers.modulePath(name));\n-          delete loadedApps[name];\n-          callback();\n-        }\n-      };\n-\n-      loadedApps[name].watchers.forEach(function(watch){\n-        configHelpers.unwatchConfig(watch);\n-      });\n-\n-      if (appModule.stop === undefined) {\n-        closeServer();\n-      } else {\n-        appModule.stop(closeServer);\n-      }\n-    } else {\n-      callback();\n-    }\n-  },\n-\n-  /**\n-   * Stop all running apps,\n-   *\n-   * @param {Function} callback Termination.\n-   */\n-  stopAll: function (callback) {\n-    function stopApp (app, cb) {\n-      var application = config.apps[app];\n-      applicationHelpers.stop(application, cb);\n-    }\n-    async.eachSeries(Object.keys(config.apps), stopApp, callback);\n-  },\n-\n-  /**\n-   * Start all running apps,\n-   *\n-   * @param db The datastore.\n-   * @param {Function} callback Termination.\n-   */\n-  startAll: function (db, callback) {\n-    function startApp (app, cb) {\n-      var application = config.apps[app];\n-      applicationHelpers.start(application, db, cb);\n-    }\n-    async.eachSeries(Object.keys(config.apps), startApp, callback);\n-  }\n-};\n-\n-var controllers = {\n-\n-  /**\n-   * Proxy requests targeting apps.\n-   */\n-  proxyPrivate: function (req, res) {\n-    var appName = req.params.name;\n-    var appPort = routes[appName];\n-    req.url = req.url.substring(('/apps/' + appName).length);\n-    if (port !== null) {\n-      proxy.web(req, res, { target: 'http://localhost:' + appPort });\n-    } else {\n-      res.send(404);\n-    }\n-  },\n-\n-  /**\n-   * Proxy requests targeting apps public path.\n-   */\n-  proxyPublic: function (req, res) {\n-    var appName = req.params.name;\n-    var appPort = routes[appName];\n-    req.url = '/public' + req.url.substring(('/public/' + appName).length);\n-    if (port !== null) {\n-      proxy.web(req, res, { target: 'http://localhost:' + appPort });\n-    } else {\n-      res.send(404);\n-    }\n-  }\n-\n-};\n-\n-\n-var mainAppHelper = {\n-\n-  /**\n-   * Configure properly proxy: handle errors and websockets.\n-   *\n-   * @param {Object} server Express server.\n-   */\n-  initializeProxy: function (server) {\n-\n-    proxy = httpProxy.createProxyServer(/*{agent: new http.Agent()}*/);\n-\n-    proxy.on('error', function onProxyError(err, req, res) {\n-      LOGGER.raw(err);\n-      res.status(500).send(err);\n-    });\n-\n-    server.on('upgrade', function onProxyUpgrade(req, socket, head) {\n-\n-      function proxyWS(port) {\n-        proxy.ws(req, socket, head, {\n-          target: 'ws://localhost:' + port,\n-          ws: true\n-        });\n-      }\n-\n-      req.originalUrl = req.url;\n-\n-      var publicOrPrivate = '';\n-      var slug = '';\n-\n-      var urlParts = req.url.split('/');\n-      if (urlParts.length >= 3) {\n-        publicOrPrivate = urlParts[1];\n-        slug = urlParts[2];\n-      }\n-\n-      if (publicOrPrivate === 'public') {\n-        req.url = req.url.replace('/public/' + slug, '/public');\n-        proxyWS(routes[slug]);\n-\n-      } else if (publicOrPrivate === 'apps') {\n-        req.url = req.url.replace('/apps/' + slug, '');\n-        proxyWS(routes[slug]);\n-\n-      } else {\n-        proxyWS(process.env.DEFAULT_REDIRECT_PORT);\n-      }\n-    });\n-\n-    return proxy;\n-  },\n-\n-  /**\n-   * Configure main application server.\n-   *\n-   * @param program\n-   * @param app\n-   * @returns {*}\n-   */\n-  start: function (program, app) {\n-    // Set SSL configuration if certificates path are properly set.\n-    var options = {};\n-    if (config.ssl !== undefined) {\n-      options.key = fs.readFileSync(config.ssl.key, 'utf8');\n-      options.cert = fs.readFileSync(config.ssl.cert, 'utf8');\n-      server = https.createServer(options, app);\n-    } else  {\n-      server = http.createServer(app);\n-    }\n-\n-    var mainPort = configHelpers.getServerPort(program);\n-    server.listen(mainPort);\n-    mainAppHelper.initializeProxy(server);\n-\n-    LOGGER.info(\n-      'Cozy Light server is listening on port ' + mainPort + '...'\n-    );\n-\n-    app.all('/apps/:name/*', controllers.proxyPrivate);\n-    app.all('/apps/:name*', controllers.proxyPrivate);\n-\n-    app.all('/public/:name/*', controllers.proxyPublic);\n-    app.all('/public/:name*', controllers.proxyPublic);\n-\n-    return server;\n-  },\n-\n-  /**\n-   * Stop main application server.\n-   */\n-  stop: function (done) {\n-    var list = [];\n-    if (server) {\n-      list.push(function (callback) {\n-        try {\n-          server.close(callback);\n-        } catch (err) {\n-          callback();\n-        }\n-      });\n-    }\n-    if (proxy) {\n-      list.push(function (callback) {\n-        if (proxy.close !== undefined) {\n-          proxy.close(); // does not work well, or has no callback\n-        }\n-        callback();\n-      });\n-    }\n-    async.series(list, done);\n-  }\n-};\n-\n \n-// Actions\n+// 'Global' variable\n \n-var actions = {\n-\n-  /**\n-   * Apply plugin customatisation on server. Then get all installed app\n-   * modules. Expect that a start function is available, then run the app\n-   * server on given port. Ports are expected to be available.\n-   *\n-   * @param {Object} program The command line options.\n-   * @param {Function} callback Termination.\n-   */\n-  start: function (program, callback) {\n-\n-    var app = express();\n-    var morgand = morgan('combined');\n-    if (expressLog) {\n-      app.use(morgand);\n-    }\n-\n-    config.pouchdb = Pouchdb;\n-\n-    pluginHelpers.startAll(app, function(){\n-      mainAppHelper.start(program, app);\n-      applicationHelpers.startAll(db, function() {\n-\n-        // always connect it after the proxy\n-        // https://github.com/nodejitsu/node-http-proxy/issues/180\n-        var jsonParser = bodyParser.json();\n-        var urlencodedParser = bodyParser.urlencoded({ extended: false });\n-        app.use(urlencodedParser);\n-        app.use(jsonParser);\n-\n-        if(configHelpers.mainWatcher === undefined) {\n-          var watchOptions = {persistent: false, interval: 1000};\n-          var onConfigChanged = function () {\n-            LOGGER.info('Config changed...');\n-            configWatcher.trigger();\n-          };\n-          configHelpers.mainWatcher = fs.watchFile(\n-            configPath,\n-            watchOptions,\n-            onConfigChanged\n-          );\n-        }\n-\n-        // Reload apps when file configuration is modified\n-        configHelpers.watchConfig(actions.restart);\n-        if (callback !== undefined && typeof(callback) === 'function') {\n-          callback(null, app, server);\n-        }\n-      });\n-    });\n-  },\n-\n-  /**\n-   * Remove disabled mark from config file for given app or plugin.\n-   */\n-  enable: function (apporplugin) {\n-    try {\n-      configHelpers.enable(apporplugin);\n-      LOGGER.info(apporplugin + ' enabled');\n-    } catch (err) {\n-      LOGGER.error(\n-        'Cannot enable given app or plugin, ' +\n-        'cannot find it in the config file.');\n-    }\n-  },\n-\n-  /**\n-   * Mark given app or plugin as disabled. It won't be activated or started\n-   * in that case.\n-   */\n-  disable: function (appormodule) {\n-    try {\n-      configHelpers.disable(appormodule);\n-      LOGGER.info(appormodule + ' disabled');\n-    } catch (err) {\n-      LOGGER.error(\n-        'Cannot disable given app or plugin, ' +\n-        'cannot find it in the config file.');\n-      LOGGER.raw(err);\n-    }\n-  },\n-\n-  /**\n-   * Stop all running applications.\n-   *\n-   * @param {Function} callback Termination.\n-   */\n-  stop: function (callback) {\n-    LOGGER.info('Stopping apps...');\n-    applicationHelpers.stopAll(function (err) {\n-      if (err) {\n-        LOGGER.error('An error occured while stopping applications');\n-        LOGGER.raw(err);\n+var cozyLight = {\n+  status: 'stopped',\n+  init: function( program ){\n+    cozyLight.logger.info('initializing..');\n+    cozyLight.npmHelpers.initialWd = process.cwd();\n+    var defaultHome = pathExtra.join(pathExtra.homedir(), '.cozy-light');\n+    cozyLight.configHelpers.init(program.home || defaultHome);\n+    cozyLight.configHelpers.setMainAppPort(program.port || 19104);\n+    process.chdir( cozyLight.configHelpers.getHomePath() );\n+    cozyLight.logger.info('Home is ' + cozyLight.configHelpers.getHomePath());\n+    cozyLight.db = new Pouchdb('cozy');\n+    cozyLight.pluginHelpers.loadAll(program);\n+    // Reload apps when file configuration is modified\n+    cozyLight.configHelpers.watcher.on(function configChanged () {\n+      cozyLight.logger.info('Config changed...');\n+      if ( cozyLight.status === 'started' ) {\n+        cozyLight.logger.info('...Restarting...');\n+        cozyLight.actions.restart(program);\n       }\n-\n-      LOGGER.info('Stopping plugins...');\n-      pluginHelpers.stopAll(function (err) {\n-        if (err) {\n-          LOGGER.error('An error occured while stopping plugins');\n-          LOGGER.raw(err);\n-        }\n-\n-        LOGGER.info('Stopping server...');\n-        mainAppHelper.stop(function(){\n-          port = defaultAppsPort;\n-          LOGGER.info('\\t' + symbols.ok + '\\tmain server');\n-          if (callback) { callback(); }\n-        });\n-      });\n     });\n   },\n-\n-  /**\n-   * Stops application server, its apps, and its plugins,\n-   * reload everyone, then restart everyone\n-   */\n-  restart: function (callback) {\n-    LOGGER.info('Restarting...');\n-    actions.stop(function(){\n-      configHelpers.loadConfigFile();\n-      loadedPlugins = {};\n-      loadedApps = {};\n-      routes = {};\n-      LOGGER.info('Starting all apps...');\n-      actions.start(program, function(){\n-        LOGGER.info('...Cozy Light was properly restarted.');\n-        if (callback) { callback(); }\n-      });\n-    });\n-  },\n-\n   /**\n    * Manage properly exit of the process when SIGINT signal is triggered.\n    * It asks to every plugin to end properly.\n    */\n-  exit: function (callback) {\n-    actions.stop(function endProcess (err) {\n+  stop: function( callback ){\n+    cozyLight.actions.stop(function endProcess (err) {\n       if (err) {\n-        LOGGER.error('Cozy Light was not properly terminated.');\n+        cozyLight.logger.error('Cozy Light was not properly terminated.');\n       } else {\n-        LOGGER.info('Cozy Light was properly terminated.');\n+        cozyLight.logger.info('Cozy Light was properly terminated.');\n       }\n \n-      if (db) {\n+      var doneCallback = function(){\n+        cozyLight.configHelpers.watcher.release();\n+        cozyLight.nodeHelpers.invoke(callback);\n+      };\n+\n+      if (cozyLight.db) {\n         try {\n-          if(!db._closed) {\n-            db.close();\n+          /*eslint-disable */\n+          if(!cozyLight.db._closed) {\n+            /*eslint-enable */\n+            return cozyLight.db.close(doneCallback);\n           }","path":"lib/cozy-light.js","position":1109,"original_position":1109,"commit_id":"4531a50886ee30626aa4d534a7d2b7c95a5867b1","original_commit_id":"4531a50886ee30626aa4d534a7d2b7c95a5867b1","user":{"login":"maboiteaspam","id":530732,"avatar_url":"https://avatars.githubusercontent.com/u/530732?v=3","gravatar_id":"","url":"https://api.github.com/users/maboiteaspam","html_url":"https://github.com/maboiteaspam","followers_url":"https://api.github.com/users/maboiteaspam/followers","following_url":"https://api.github.com/users/maboiteaspam/following{/other_user}","gists_url":"https://api.github.com/users/maboiteaspam/gists{/gist_id}","starred_url":"https://api.github.com/users/maboiteaspam/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/maboiteaspam/subscriptions","organizations_url":"https://api.github.com/users/maboiteaspam/orgs","repos_url":"https://api.github.com/users/maboiteaspam/repos","events_url":"https://api.github.com/users/maboiteaspam/events{/privacy}","received_events_url":"https://api.github.com/users/maboiteaspam/received_events","type":"User","site_admin":false},"body":"In the case you mention the execution continues unitl L57. It is intended to avoid else and prefer return callback(); syntax. It helps to reduce braces, thus, complexity during the reading phase. But yet, it is less expressive because more natural.","created_at":"2015-01-15T13:02:38Z","updated_at":"2015-01-15T13:02:38Z","html_url":"https://github.com/cozy-labs/cozy-light/pull/63#discussion_r23007772","pull_request_url":"https://api.github.com/repos/cozy-labs/cozy-light/pulls/63","_links":{"self":{"href":"https://api.github.com/repos/cozy-labs/cozy-light/pulls/comments/23007772"},"html":{"href":"https://github.com/cozy-labs/cozy-light/pull/63#discussion_r23007772"},"pull_request":{"href":"https://api.github.com/repos/cozy-labs/cozy-light/pulls/63"}}},"pull_request":{"url":"https://api.github.com/repos/cozy-labs/cozy-light/pulls/63","id":27185932,"html_url":"https://github.com/cozy-labs/cozy-light/pull/63","diff_url":"https://github.com/cozy-labs/cozy-light/pull/63.diff","patch_url":"https://github.com/cozy-labs/cozy-light/pull/63.patch","issue_url":"https://api.github.com/repos/cozy-labs/cozy-light/issues/63","number":63,"state":"open","locked":false,"title":"refactoring","user":{"login":"maboiteaspam","id":530732,"avatar_url":"https://avatars.githubusercontent.com/u/530732?v=3","gravatar_id":"","url":"https://api.github.com/users/maboiteaspam","html_url":"https://github.com/maboiteaspam","followers_url":"https://api.github.com/users/maboiteaspam/followers","following_url":"https://api.github.com/users/maboiteaspam/following{/other_user}","gists_url":"https://api.github.com/users/maboiteaspam/gists{/gist_id}","starred_url":"https://api.github.com/users/maboiteaspam/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/maboiteaspam/subscriptions","organizations_url":"https://api.github.com/users/maboiteaspam/orgs","repos_url":"https://api.github.com/users/maboiteaspam/repos","events_url":"https://api.github.com/users/maboiteaspam/events{/privacy}","received_events_url":"https://api.github.com/users/maboiteaspam/received_events","type":"User","site_admin":false},"body":"hi,\r\n\r\nhere is the refactoring applied. While general compatibility was kept in order to insure that no breaking changes was introduced, here is a list of interesting changes,\r\n- lib/cozy-light now is a kind of cozy kernel, holding reference to necessary sub module, manage proper init / exit process of a cozy instance\r\n- all sub modules are now in a independant files, tests also reflect that\r\n- a new cozy.status is introduced to know if the app is actively running the front gui\r\n- cozyLight.actions exposes a new event 'restarted' to listen this event\r\n- install / uninstall app and plugins are updated to not let twice app being installed together\r\n- uninstall command now requires the name app instead of install source reference\r\n- mocha integration is updated, the options are deported to a specific files when possible\r\n- should.js is used whenever it is possible in the test scenarios to improve tests results understanding\r\n- tests are now delegated to specific sh files for process isolations\r\n- package file was cleaned of several useless dependency, at least not explicitly required.\r\n- application helpers and plugin helpers exposes cozyLight instance as last arguments\r\n- README was updated with fedora specifics install steps\r\n- also added tip to faster test run\r\n\r\nHTH","created_at":"2015-01-12T10:49:38Z","updated_at":"2015-01-15T13:02:38Z","closed_at":null,"merged_at":null,"merge_commit_sha":"4aa69d05d7fb6374559ddf1745103189a1beaed1","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/cozy-labs/cozy-light/pulls/63/commits","review_comments_url":"https://api.github.com/repos/cozy-labs/cozy-light/pulls/63/comments","review_comment_url":"https://api.github.com/repos/cozy-labs/cozy-light/pulls/comments/{number}","comments_url":"https://api.github.com/repos/cozy-labs/cozy-light/issues/63/comments","statuses_url":"https://api.github.com/repos/cozy-labs/cozy-light/statuses/4531a50886ee30626aa4d534a7d2b7c95a5867b1","head":{"label":"maboiteaspam:refactoring","ref":"refactoring","sha":"4531a50886ee30626aa4d534a7d2b7c95a5867b1","user":{"login":"maboiteaspam","id":530732,"avatar_url":"https://avatars.githubusercontent.com/u/530732?v=3","gravatar_id":"","url":"https://api.github.com/users/maboiteaspam","html_url":"https://github.com/maboiteaspam","followers_url":"https://api.github.com/users/maboiteaspam/followers","following_url":"https://api.github.com/users/maboiteaspam/following{/other_user}","gists_url":"https://api.github.com/users/maboiteaspam/gists{/gist_id}","starred_url":"https://api.github.com/users/maboiteaspam/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/maboiteaspam/subscriptions","organizations_url":"https://api.github.com/users/maboiteaspam/orgs","repos_url":"https://api.github.com/users/maboiteaspam/repos","events_url":"https://api.github.com/users/maboiteaspam/events{/privacy}","received_events_url":"https://api.github.com/users/maboiteaspam/received_events","type":"User","site_admin":false},"repo":{"id":26041336,"name":"cozy-light","full_name":"maboiteaspam/cozy-light","owner":{"login":"maboiteaspam","id":530732,"avatar_url":"https://avatars.githubusercontent.com/u/530732?v=3","gravatar_id":"","url":"https://api.github.com/users/maboiteaspam","html_url":"https://github.com/maboiteaspam","followers_url":"https://api.github.com/users/maboiteaspam/followers","following_url":"https://api.github.com/users/maboiteaspam/following{/other_user}","gists_url":"https://api.github.com/users/maboiteaspam/gists{/gist_id}","starred_url":"https://api.github.com/users/maboiteaspam/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/maboiteaspam/subscriptions","organizations_url":"https://api.github.com/users/maboiteaspam/orgs","repos_url":"https://api.github.com/users/maboiteaspam/repos","events_url":"https://api.github.com/users/maboiteaspam/events{/privacy}","received_events_url":"https://api.github.com/users/maboiteaspam/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/maboiteaspam/cozy-light","description":"Micro PaaS That Makes Self-hosting Easy","fork":true,"url":"https://api.github.com/repos/maboiteaspam/cozy-light","forks_url":"https://api.github.com/repos/maboiteaspam/cozy-light/forks","keys_url":"https://api.github.com/repos/maboiteaspam/cozy-light/keys{/key_id}","collaborators_url":"https://api.github.com/repos/maboiteaspam/cozy-light/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/maboiteaspam/cozy-light/teams","hooks_url":"https://api.github.com/repos/maboiteaspam/cozy-light/hooks","issue_events_url":"https://api.github.com/repos/maboiteaspam/cozy-light/issues/events{/number}","events_url":"https://api.github.com/repos/maboiteaspam/cozy-light/events","assignees_url":"https://api.github.com/repos/maboiteaspam/cozy-light/assignees{/user}","branches_url":"https://api.github.com/repos/maboiteaspam/cozy-light/branches{/branch}","tags_url":"https://api.github.com/repos/maboiteaspam/cozy-light/tags","blobs_url":"https://api.github.com/repos/maboiteaspam/cozy-light/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/maboiteaspam/cozy-light/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/maboiteaspam/cozy-light/git/refs{/sha}","trees_url":"https://api.github.com/repos/maboiteaspam/cozy-light/git/trees{/sha}","statuses_url":"https://api.github.com/repos/maboiteaspam/cozy-light/statuses/{sha}","languages_url":"https://api.github.com/repos/maboiteaspam/cozy-light/languages","stargazers_url":"https://api.github.com/repos/maboiteaspam/cozy-light/stargazers","contributors_url":"https://api.github.com/repos/maboiteaspam/cozy-light/contributors","subscribers_url":"https://api.github.com/repos/maboiteaspam/cozy-light/subscribers","subscription_url":"https://api.github.com/repos/maboiteaspam/cozy-light/subscription","commits_url":"https://api.github.com/repos/maboiteaspam/cozy-light/commits{/sha}","git_commits_url":"https://api.github.com/repos/maboiteaspam/cozy-light/git/commits{/sha}","comments_url":"https://api.github.com/repos/maboiteaspam/cozy-light/comments{/number}","issue_comment_url":"https://api.github.com/repos/maboiteaspam/cozy-light/issues/comments/{number}","contents_url":"https://api.github.com/repos/maboiteaspam/cozy-light/contents/{+path}","compare_url":"https://api.github.com/repos/maboiteaspam/cozy-light/compare/{base}...{head}","merges_url":"https://api.github.com/repos/maboiteaspam/cozy-light/merges","archive_url":"https://api.github.com/repos/maboiteaspam/cozy-light/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/maboiteaspam/cozy-light/downloads","issues_url":"https://api.github.com/repos/maboiteaspam/cozy-light/issues{/number}","pulls_url":"https://api.github.com/repos/maboiteaspam/cozy-light/pulls{/number}","milestones_url":"https://api.github.com/repos/maboiteaspam/cozy-light/milestones{/number}","notifications_url":"https://api.github.com/repos/maboiteaspam/cozy-light/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/maboiteaspam/cozy-light/labels{/name}","releases_url":"https://api.github.com/repos/maboiteaspam/cozy-light/releases{/id}","created_at":"2014-11-01T04:05:08Z","updated_at":"2015-01-13T14:10:49Z","pushed_at":"2015-01-13T14:10:49Z","git_url":"git://github.com/maboiteaspam/cozy-light.git","ssh_url":"git@github.com:maboiteaspam/cozy-light.git","clone_url":"https://github.com/maboiteaspam/cozy-light.git","svn_url":"https://github.com/maboiteaspam/cozy-light","homepage":"","size":1331,"stargazers_count":0,"watchers_count":0,"language":"JavaScript","has_issues":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"refactoring"}},"base":{"label":"cozy-labs:master","ref":"master","sha":"13f3ebb68e7a40687141fc541af4a2c72e6ce01b","user":{"login":"cozy-labs","id":8415593,"avatar_url":"https://avatars.githubusercontent.com/u/8415593?v=3","gravatar_id":"","url":"https://api.github.com/users/cozy-labs","html_url":"https://github.com/cozy-labs","followers_url":"https://api.github.com/users/cozy-labs/followers","following_url":"https://api.github.com/users/cozy-labs/following{/other_user}","gists_url":"https://api.github.com/users/cozy-labs/gists{/gist_id}","starred_url":"https://api.github.com/users/cozy-labs/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cozy-labs/subscriptions","organizations_url":"https://api.github.com/users/cozy-labs/orgs","repos_url":"https://api.github.com/users/cozy-labs/repos","events_url":"https://api.github.com/users/cozy-labs/events{/privacy}","received_events_url":"https://api.github.com/users/cozy-labs/received_events","type":"Organization","site_admin":false},"repo":{"id":22798256,"name":"cozy-light","full_name":"cozy-labs/cozy-light","owner":{"login":"cozy-labs","id":8415593,"avatar_url":"https://avatars.githubusercontent.com/u/8415593?v=3","gravatar_id":"","url":"https://api.github.com/users/cozy-labs","html_url":"https://github.com/cozy-labs","followers_url":"https://api.github.com/users/cozy-labs/followers","following_url":"https://api.github.com/users/cozy-labs/following{/other_user}","gists_url":"https://api.github.com/users/cozy-labs/gists{/gist_id}","starred_url":"https://api.github.com/users/cozy-labs/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cozy-labs/subscriptions","organizations_url":"https://api.github.com/users/cozy-labs/orgs","repos_url":"https://api.github.com/users/cozy-labs/repos","events_url":"https://api.github.com/users/cozy-labs/events{/privacy}","received_events_url":"https://api.github.com/users/cozy-labs/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/cozy-labs/cozy-light","description":"Personal Server Made Easy","fork":false,"url":"https://api.github.com/repos/cozy-labs/cozy-light","forks_url":"https://api.github.com/repos/cozy-labs/cozy-light/forks","keys_url":"https://api.github.com/repos/cozy-labs/cozy-light/keys{/key_id}","collaborators_url":"https://api.github.com/repos/cozy-labs/cozy-light/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/cozy-labs/cozy-light/teams","hooks_url":"https://api.github.com/repos/cozy-labs/cozy-light/hooks","issue_events_url":"https://api.github.com/repos/cozy-labs/cozy-light/issues/events{/number}","events_url":"https://api.github.com/repos/cozy-labs/cozy-light/events","assignees_url":"https://api.github.com/repos/cozy-labs/cozy-light/assignees{/user}","branches_url":"https://api.github.com/repos/cozy-labs/cozy-light/branches{/branch}","tags_url":"https://api.github.com/repos/cozy-labs/cozy-light/tags","blobs_url":"https://api.github.com/repos/cozy-labs/cozy-light/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/cozy-labs/cozy-light/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/cozy-labs/cozy-light/git/refs{/sha}","trees_url":"https://api.github.com/repos/cozy-labs/cozy-light/git/trees{/sha}","statuses_url":"https://api.github.com/repos/cozy-labs/cozy-light/statuses/{sha}","languages_url":"https://api.github.com/repos/cozy-labs/cozy-light/languages","stargazers_url":"https://api.github.com/repos/cozy-labs/cozy-light/stargazers","contributors_url":"https://api.github.com/repos/cozy-labs/cozy-light/contributors","subscribers_url":"https://api.github.com/repos/cozy-labs/cozy-light/subscribers","subscription_url":"https://api.github.com/repos/cozy-labs/cozy-light/subscription","commits_url":"https://api.github.com/repos/cozy-labs/cozy-light/commits{/sha}","git_commits_url":"https://api.github.com/repos/cozy-labs/cozy-light/git/commits{/sha}","comments_url":"https://api.github.com/repos/cozy-labs/cozy-light/comments{/number}","issue_comment_url":"https://api.github.com/repos/cozy-labs/cozy-light/issues/comments/{number}","contents_url":"https://api.github.com/repos/cozy-labs/cozy-light/contents/{+path}","compare_url":"https://api.github.com/repos/cozy-labs/cozy-light/compare/{base}...{head}","merges_url":"https://api.github.com/repos/cozy-labs/cozy-light/merges","archive_url":"https://api.github.com/repos/cozy-labs/cozy-light/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/cozy-labs/cozy-light/downloads","issues_url":"https://api.github.com/repos/cozy-labs/cozy-light/issues{/number}","pulls_url":"https://api.github.com/repos/cozy-labs/cozy-light/pulls{/number}","milestones_url":"https://api.github.com/repos/cozy-labs/cozy-light/milestones{/number}","notifications_url":"https://api.github.com/repos/cozy-labs/cozy-light/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/cozy-labs/cozy-light/labels{/name}","releases_url":"https://api.github.com/repos/cozy-labs/cozy-light/releases{/id}","created_at":"2014-08-10T00:37:34Z","updated_at":"2015-01-13T10:55:09Z","pushed_at":"2015-01-08T01:33:55Z","git_url":"git://github.com/cozy-labs/cozy-light.git","ssh_url":"git@github.com:cozy-labs/cozy-light.git","clone_url":"https://github.com/cozy-labs/cozy-light.git","svn_url":"https://github.com/cozy-labs/cozy-light","homepage":"http://cozy-labs.github.io/cozy-light/","size":2532,"stargazers_count":56,"watchers_count":56,"language":"JavaScript","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":true,"forks_count":5,"mirror_url":null,"open_issues_count":13,"forks":5,"open_issues":13,"watchers":56,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/cozy-labs/cozy-light/pulls/63"},"html":{"href":"https://github.com/cozy-labs/cozy-light/pull/63"},"issue":{"href":"https://api.github.com/repos/cozy-labs/cozy-light/issues/63"},"comments":{"href":"https://api.github.com/repos/cozy-labs/cozy-light/issues/63/comments"},"review_comments":{"href":"https://api.github.com/repos/cozy-labs/cozy-light/pulls/63/comments"},"review_comment":{"href":"https://api.github.com/repos/cozy-labs/cozy-light/pulls/comments/{number}"},"commits":{"href":"https://api.github.com/repos/cozy-labs/cozy-light/pulls/63/commits"},"statuses":{"href":"https://api.github.com/repos/cozy-labs/cozy-light/statuses/4531a50886ee30626aa4d534a7d2b7c95a5867b1"}}}}