git ssb

4+

Dominic / scuttlebot



Tree: 66ea5fb7f20bcb120716849b8b81dd19854a758b

Files: 66ea5fb7f20bcb120716849b8b81dd19854a758b / plugins / plugins.js

8030 bytesRaw
1var assert = require('assert')
2var path = require('path')
3var fs = require('fs')
4var pull = require('pull-stream')
5var cat = require('pull-cat')
6var many = require('pull-many')
7var pushable = require('pull-pushable')
8var toPull = require('stream-to-pull-stream')
9var spawn = require('cross-spawn')
10var mkdirp = require('mkdirp')
11var osenv = require('osenv')
12var rimraf = require('rimraf')
13var mv = require('mv')
14var mdm = require('mdmanifest')
15var explain = require('explain-error')
16var valid = require('../lib/validators')
17var apidoc = require('../lib/apidocs').plugins
18
19module.exports = {
20 name: 'plugins',
21 version: '1.0.0',
22 manifest: mdm.manifest(apidoc),
23 permissions: {
24 master: {allow: ['install', 'uninstall', 'enable', 'disable']}
25 },
26 init: function (server, config) {
27 var installPath = config.path
28 config.plugins = config.plugins || {}
29 mkdirp.sync(path.join(installPath, 'node_modules'))
30
31 // helper to enable/disable plugins
32 function configPluginEnabled (b) {
33 return function (pluginName, cb) {
34 checkInstalled(pluginName, function (err) {
35 if (err) return cb(err)
36
37 config.plugins[pluginName] = b
38 writePluginConfig(pluginName, b)
39 if (b)
40 cb(null, '\''+pluginName+'\' has been enabled. Restart Scuttlebot server to use the plugin.')
41 else
42 cb(null, '\''+pluginName+'\' has been disabled. Restart Scuttlebot server to stop using the plugin.')
43 })
44 }
45 }
46
47 // helper to check if a plugin is installed
48 function checkInstalled (pluginName, cb) {
49 if (!pluginName || typeof pluginName !== 'string')
50 return cb(new Error('plugin name is required'))
51 var modulePath = path.join(installPath, 'node_modules', pluginName)
52 fs.stat(modulePath, function (err) {
53 if (err)
54 cb(new Error('Plugin "'+pluginName+'" is not installed.'))
55 else
56 cb()
57 })
58 }
59
60 // write the plugin config to ~/.ssb/config
61 function writePluginConfig (pluginName, value) {
62 var cfgPath = path.join(config.path, 'config')
63 var existingConfig = {}
64
65 // load ~/.ssb/config
66 try { existingConfig = JSON.parse(fs.readFileSync(cfgPath, 'utf-8')) }
67 catch (e) {}
68
69 // update the plugins config
70 existingConfig.plugins = existingConfig.plugins || {}
71 existingConfig.plugins[pluginName] = value
72
73 // write to disc
74 fs.writeFileSync(cfgPath, JSON.stringify(existingConfig, null, 2), 'utf-8')
75 }
76
77 return {
78 install: valid.source(function (pluginName, opts) {
79 var p = pushable()
80 var dryRun = opts && opts['dry-run']
81 var from = opts && opts.from
82
83 if (!pluginName || typeof pluginName !== 'string')
84 return pull.error(new Error('plugin name is required'))
85
86 // pull out the version, if given
87 if (pluginName.indexOf('@') !== -1) {
88 var pluginNameSplitted = pluginName.split('@')
89 pluginName = pluginNameSplitted[0]
90 var version = pluginNameSplitted[1]
91
92 if (version && !from)
93 from = pluginName + '@' + version
94 }
95
96 if (!validatePluginName(pluginName))
97 return pull.error(new Error('invalid plugin name: "'+pluginName+'"'))
98
99 // create a tmp directory to install into
100 var tmpInstallPath = path.join(osenv.tmpdir(), pluginName)
101 rimraf.sync(tmpInstallPath); mkdirp.sync(tmpInstallPath)
102
103 // build args
104 // --global-style: dont dedup at the top level, gives proper isolation between each plugin
105 // --loglevel error: dont output warnings, because npm just whines about the lack of a package.json in ~/.ssb
106 var args = ['install', from||pluginName, '--global-style', '--loglevel', 'error']
107 if (dryRun)
108 args.push('--dry-run')
109
110 // exec npm
111 var child = spawn('npm', args, { cwd: tmpInstallPath })
112 .on('close', function (code) {
113 if (code == 0 && !dryRun) {
114 var tmpInstallNMPath = path.join(tmpInstallPath, 'node_modules')
115 var finalInstallNMPath = path.join(installPath, 'node_modules')
116
117 // delete plugin, if it's already there
118 rimraf.sync(path.join(finalInstallNMPath, pluginName))
119
120 // move the plugin from the tmpdir into our install path
121 // ...using our given plugin name
122 var dirs = fs.readdirSync(tmpInstallNMPath)
123 .filter(function (name) { return name.charAt(0) !== '.' }) // filter out dot dirs, like '.bin'
124 mv(
125 path.join(tmpInstallNMPath, dirs[0]),
126 path.join(finalInstallNMPath, pluginName),
127 function (err) {
128 if (err)
129 return p.end(explain(err, '"'+pluginName+'" failed to install. See log output above.'))
130
131 // enable the plugin
132 // - use basename(), because plugins can be installed from the FS, in which case pluginName is a path
133 var name = path.basename(pluginName)
134 config.plugins[name] = true
135 writePluginConfig(name, true)
136 p.push(new Buffer('"'+pluginName+'" has been installed. Restart Scuttlebot server to enable the plugin.\n', 'utf-8'))
137 p.end()
138 }
139 )
140 } else
141 p.end(new Error('"'+pluginName+'" failed to install. See log output above.'))
142 })
143 return cat([
144 pull.values([new Buffer('Installing "'+pluginName+'"...\n', 'utf-8')]),
145 many([toPull(child.stdout), toPull(child.stderr)]),
146 p
147 ])
148 }, 'string', 'object?'),
149 uninstall: valid.source(function (pluginName, opts) {
150 var p = pushable()
151 if (!pluginName || typeof pluginName !== 'string')
152 return pull.error(new Error('plugin name is required'))
153
154 var modulePath = path.join(installPath, 'node_modules', pluginName)
155
156 rimraf(modulePath, function (err) {
157 if (!err) {
158 writePluginConfig(pluginName, false)
159 p.push(new Buffer('"'+pluginName+'" has been uninstalled. Restart Scuttlebot server to disable the plugin.\n', 'utf-8'))
160 p.end()
161 } else
162 p.end(err)
163 })
164 return p
165 }, 'string', 'object?'),
166 enable: valid.async(configPluginEnabled(true), 'string'),
167 disable: valid.async(configPluginEnabled(false), 'string')
168 }
169 }
170}
171
172module.exports.loadUserPlugins = function (createSbot, config) {
173 // iterate all modules
174 var nodeModulesPath = path.join(config.path, 'node_modules')
175 //instead of testing all plugins, only load things explicitly
176 //enabled in the config
177 for(var k in config.plugins) {
178 if(config.plugins[k]) {
179 try {
180 var plugin = require(path.join(nodeModulesPath, k))
181 assertSbotPlugin(plugin)
182 if (createSbot.plugins.some(plug => plug.name === plugin.name))
183 throw new Error('already loaded')
184 createSbot.use(plugin)
185 } catch (e) {
186 console.error('Error loading plugin "'+k+'":', e.message)
187 }
188 }
189 }
190}
191
192// predictate to check if an object appears to be a sbot plugin
193function assertSbotPlugin (obj) {
194 // function signature:
195 if (typeof obj == 'function')
196 return
197
198 // object signature:
199 assert(obj && typeof obj == 'object', 'module.exports must be an object')
200 assert(typeof obj.name == 'string', 'module.exports.name must be a string')
201 assert(typeof obj.version == 'string', 'module.exports.version must be a string')
202 assert(obj.manifest &&
203 typeof obj.manifest == 'object', 'module.exports.manifest must be an object')
204 assert(typeof obj.init == 'function', 'module.exports.init must be a function')
205}
206
207function validatePluginName (name) {
208 if (/^[._]/.test(name))
209 return false
210 // from npm-validate-package-name:
211 if (encodeURIComponent(name) !== name)
212 return false
213 return true
214}
215
216

Built with git-ssb-web