···11-var assert = require('assert')
22-var path = require('path')
33-var fs = require('fs')
44-var pull = require('pull-stream')
55-var cat = require('pull-cat')
66-var many = require('pull-many')
77-var pushable = require('pull-pushable')
88-var toPull = require('stream-to-pull-stream')
99-var spawn = require('cross-spawn')
1010-var mkdirp = require('mkdirp')
1111-var osenv = require('osenv')
1212-var rimraf = require('rimraf')
1313-var mv = require('mv')
1414-var mdm = require('mdmanifest')
1515-var explain = require('explain-error')
1616-var valid = require('../lib/validators')
1717-1818-module.exports = {
1919- name: 'plugins',
2020- version: '1.0.0',
2121- manifest: mdm.manifest(fs.readFileSync(path.join(__dirname, 'plugins.md'), 'utf8')),
2222- permissions: {
2323- master: {allow: ['install', 'uninstall', 'enable', 'disable']}
2424- },
2525- init: function (server, config) {
2626- var installPath = config.path
2727- config.plugins = config.plugins || {}
2828- mkdirp.sync(path.join(installPath, 'node_modules'))
2929-3030- // helper to enable/disable plugins
3131- function configPluginEnabled (b) {
3232- return function (pluginName, cb) {
3333- checkInstalled(pluginName, function (err) {
3434- if (err) return cb(err)
3535-3636- config.plugins[pluginName] = b
3737- writePluginConfig(pluginName, b)
3838- if (b)
3939- cb(null, '\''+pluginName+'\' has been enabled. Restart ssb-server to use the plugin.')
4040- else
4141- cb(null, '\''+pluginName+'\' has been disabled. Restart ssb-server to stop using the plugin.')
4242- })
4343- }
4444- }
4545-4646- // helper to check if a plugin is installed
4747- function checkInstalled (pluginName, cb) {
4848- if (!pluginName || typeof pluginName !== 'string')
4949- return cb(new Error('plugin name is required'))
5050- var modulePath = path.join(installPath, 'node_modules', pluginName)
5151- fs.stat(modulePath, function (err) {
5252- if (err)
5353- cb(new Error('Plugin "'+pluginName+'" is not installed.'))
5454- else
5555- cb()
5656- })
5757- }
5858-5959- // write the plugin config to ~/.ssb/config
6060- function writePluginConfig (pluginName, value) {
6161- var cfgPath = path.join(config.path, 'config')
6262- // load ~/.ssb/config
6363- let existingConfig
6464- fs.readFile(cfgPath, 'utf-8', (err, data) => {
6565- if (err) {
6666- if (err.code === 'ENOENT') {
6767- // only catch "file not found"
6868- existingConfig = {}
6969- } else {
7070- throw err
7171- }
7272- } else {
7373- existingConfig = JSON.parse(data)
7474- }
7575-7676-7777- // update the plugins config
7878- existingConfig.plugins = existingConfig.plugins || {}
7979- existingConfig.plugins[pluginName] = value
8080-8181- // write to disc
8282- fs.writeFileSync(cfgPath, JSON.stringify(existingConfig, null, 2), 'utf-8')
8383- })
8484-8585- }
8686-8787- return {
8888- install: valid.source(function (pluginName, opts) {
8989- var p = pushable()
9090- var dryRun = opts && opts['dry-run']
9191- var from = opts && opts.from
9292-9393- if (!pluginName || typeof pluginName !== 'string')
9494- return pull.error(new Error('plugin name is required'))
9595-9696- // pull out the version, if given
9797- if (pluginName.indexOf('@') !== -1) {
9898- var pluginNameSplitted = pluginName.split('@')
9999- pluginName = pluginNameSplitted[0]
100100- var version = pluginNameSplitted[1]
101101-102102- if (version && !from)
103103- from = pluginName + '@' + version
104104- }
105105-106106- if (!validatePluginName(pluginName))
107107- return pull.error(new Error('invalid plugin name: "'+pluginName+'"'))
108108-109109- // create a tmp directory to install into
110110- var tmpInstallPath = path.join(osenv.tmpdir(), pluginName)
111111- rimraf.sync(tmpInstallPath); mkdirp.sync(tmpInstallPath)
112112-113113- // build args
114114- // --global-style: dont dedup at the top level, gives proper isolation between each plugin
115115- // --loglevel error: dont output warnings, because npm just whines about the lack of a package.json in ~/.ssb
116116- var args = ['install', from||pluginName, '--global-style', '--loglevel', 'error']
117117- if (dryRun)
118118- args.push('--dry-run')
119119-120120- // exec npm
121121- var child = spawn('npm', args, { cwd: tmpInstallPath })
122122- .on('close', function (code) {
123123- if (code == 0 && !dryRun) {
124124- var tmpInstallNMPath = path.join(tmpInstallPath, 'node_modules')
125125- var finalInstallNMPath = path.join(installPath, 'node_modules')
126126-127127- // delete plugin, if it's already there
128128- rimraf.sync(path.join(finalInstallNMPath, pluginName))
129129-130130- // move the plugin from the tmpdir into our install path
131131- // ...using our given plugin name
132132- var dirs = fs.readdirSync(tmpInstallNMPath)
133133- .filter(function (name) { return name.charAt(0) !== '.' }) // filter out dot dirs, like '.bin'
134134- mv(
135135- path.join(tmpInstallNMPath, dirs[0]),
136136- path.join(finalInstallNMPath, pluginName),
137137- function (err) {
138138- if (err)
139139- return p.end(explain(err, '"'+pluginName+'" failed to install. See log output above.'))
140140-141141- // enable the plugin
142142- // - use basename(), because plugins can be installed from the FS, in which case pluginName is a path
143143- var name = path.basename(pluginName)
144144- config.plugins[name] = true
145145- writePluginConfig(name, true)
146146- p.push(Buffer.from('"'+pluginName+'" has been installed. Restart ssb-server to enable the plugin.\n', 'utf-8'))
147147- p.end()
148148- }
149149- )
150150- } else
151151- p.end(new Error('"'+pluginName+'" failed to install. See log output above.'))
152152- })
153153- return cat([
154154- pull.values([Buffer.from('Installing "'+pluginName+'"...\n', 'utf-8')]),
155155- many([toPull(child.stdout), toPull(child.stderr)]),
156156- p
157157- ])
158158- }, 'string', 'object?'),
159159- uninstall: valid.source(function (pluginName, opts) {
160160- var p = pushable()
161161- if (!pluginName || typeof pluginName !== 'string')
162162- return pull.error(new Error('plugin name is required'))
163163-164164- var modulePath = path.join(installPath, 'node_modules', pluginName)
165165-166166- rimraf(modulePath, function (err) {
167167- if (!err) {
168168- writePluginConfig(pluginName, false)
169169- p.push(Buffer.from('"'+pluginName+'" has been uninstalled. Restart ssb-server to disable the plugin.\n', 'utf-8'))
170170- p.end()
171171- } else
172172- p.end(err)
173173- })
174174- return p
175175- }, 'string', 'object?'),
176176- enable: valid.async(configPluginEnabled(true), 'string'),
177177- disable: valid.async(configPluginEnabled(false), 'string')
178178- }
179179- }
180180-}
181181-182182-module.exports.loadUserPlugins = function (createSsbServer, config) {
183183- // iterate all modules
184184- var nodeModulesPath = path.join(config.path, 'node_modules')
185185- //instead of testing all plugins, only load things explicitly
186186- //enabled in the config
187187- for(var module_name in config.plugins) {
188188- if(config.plugins[module_name]) {
189189- var name = config.plugins[module_name]
190190- if(name === true)
191191- name = /^ssb-/.test(module_name) ? module_name.substring(4) : module_name
192192-193193- if (createSsbServer.plugins.some(plug => plug.name === name))
194194- throw new Error('already loaded plugin named:'+name)
195195- var plugin = require(path.join(nodeModulesPath, module_name))
196196- if(!plugin || plugin.name !== name)
197197- throw new Error('plugin at:'+module_name+' expected name:'+name+' but had:'+(plugin||{}).name)
198198- assertSsbServerPlugin(plugin)
199199- createSsbServer.use(plugin)
200200- }
201201- }
202202-}
203203-204204-// predictate to check if an object appears to be a ssbServer plugin
205205-function assertSsbServerPlugin (obj) {
206206- // function signature:
207207- if (typeof obj == 'function')
208208- return
209209-210210- // object signature:
211211- assert(obj && typeof obj == 'object', 'module.exports must be an object')
212212- assert(typeof obj.name == 'string', 'module.exports.name must be a string')
213213- assert(typeof obj.version == 'string', 'module.exports.version must be a string')
214214- assert(obj.manifest &&
215215- typeof obj.manifest == 'object', 'module.exports.manifest must be an object')
216216- assert(typeof obj.init == 'function', 'module.exports.init must be a function')
217217-}
218218-219219-function validatePluginName (name) {
220220- if (/^[._]/.test(name))
221221- return false
222222- // from npm-validate-package-name:
223223- if (encodeURIComponent(name) !== name)
224224- return false
225225- return true
226226-}
227227-228228-229229-
-68
plugins/plugins.md
···11-# ssb-server plugins plugin
22-33-Install and manage third-party plugins.
44-55-66-77-## install: source
88-99-Install a plugin to ssb-server.
1010-1111-```bash
1212-install {nodeModule} [--from path]
1313-```
1414-```js
1515-install(nodeModule, { from: })
1616-```
1717-1818-Calls out to npm to install a package into `~/.ssb/node_modules`.
1919-2020- - nodeModule (string): The name of the plugin to install. Uses npm's module package-name rules.
2121- - from (string): A location to install from (directory path, url, or any location that npm accepts for its install command).
2222-2323-2424-2525-## uninstall: source
2626-2727-Uninstall a plugin from ssb-server.
2828-2929-```bash
3030-uninstall {nodeModule}
3131-```
3232-```js
3333-uninstall(nodeModule)
3434-```
3535-3636-Calls out to npm to uninstall a package into `~/.ssb/node_modules`.
3737-3838- - nodeModule (string): The name of the plugin to uninstall.
3939-4040-4141-4242-## enable: async
4343-4444-Update the config to enable a plugin.
4545-4646-```bash
4747-enable {nodeModule}
4848-```
4949-```js
5050-enable(nodeModule, cb)
5151-```
5252-5353- - nodeModule (string): The name of the plugin to enable.
5454-5555-5656-5757-## disable: async
5858-5959-Update the config to disable a plugin.
6060-6161-```bash
6262-disable {nodeModule}
6363-```
6464-```js
6565-disable(nodeModule, cb)
6666-```
6767-6868- - nodeModule (string): The name of the plugin to disable.