git ssb

3+

cel / ssb-npm-registry



Commit 45e34d5ff0c939447f0f24c04303dfc1663e3421

Add bootstrap script

cel committed on 9/25/2017, 11:34:34 PM
Parent: 4c7d541a176f2e064fa573b2a960014b4bc088d5

Files changed

README.mdchanged
index.jschanged
bootstrap.jsadded
README.mdView
@@ -63,8 +63,25 @@
6363 ### `Registry.publishPkgMentions(sbot, mentions, cb(err, msgs))`
6464
6565 Publish the given npm package mentions as one or more messages
6666
67+## Bootstrapping
68+
69+This plugin includes a script for securely bootstrapping an npm-installation of scuttlebot from your machine to a peer's machine.
70+
71+Steps for bootstrapping:
72+
73+- Find your local IP.
74+- In your web browser, go to your IP, port 8043 (or other port, if you set
75+ `config.npm.port`).
76+- Click the "bootstrap" link.
77+- Send that URL to your peer.
78+- Check that your peer sees the same hash on the bootstrap page as you do.
79+- While you are still online, have your peer run the script on that page.
80+- When your peer's sbot is running, verify that they have the same hash at
81+ their `http://localhost:8043/bootstrap` page.
82+- Proceed with gossip/pub onboarding.
83+
6784 ## License
6885
6986 Copyright (C) 2017 Secure Scuttlebutt Consortium
7087
index.jsView
@@ -1,8 +1,10 @@
11 var http = require('http')
22 var os = require('os')
33 var path = require('path')
44 var fs = require('fs')
5+var crypto = require('crypto')
6+var pkg = require('./package')
57
68 function pullOnce(data) {
79 var ended
810 return function (abort, cb) {
@@ -11,44 +13,120 @@
1113 cb(null, data)
1214 }
1315 }
1416
17+function escapeHTML(str) {
18+ return String(str)
19+ .replace(/</g, '&lt;')
20+ .replace(/>/g, '&gt;')
21+}
22+
23+function onceify(fn, self) {
24+ var cbs = [], err, data
25+ return function (cb) {
26+ if (fn) {
27+ cbs.push(cb)
28+ fn.call(self, function (_err, _data) {
29+ err = _err, data = _data
30+ var _cbs = cbs
31+ cbs = null
32+ while (_cbs.length) _cbs.shift()(err, data)
33+ })
34+ fn = null
35+ } else if (cbs) {
36+ cbs.push(cb)
37+ } else {
38+ cb(err, data)
39+ }
40+ }
41+}
42+
43+function pkgLockToRegistryPkgs(pkgLock, wsPort) {
44+ // convert a package-lock.json file into data for serving as an npm registry
45+ var hasNonBlobUrl = false
46+ var blobUrlRegex = new RegExp('^http://localhost:' + wsPort + '/blobs/get/&')
47+ var pkgs = {}
48+ var queue = [pkgLock, pkgLock.name]
49+ while (queue.length) {
50+ var dep = queue.shift(), name = queue.shift()
51+ if (name) {
52+ var pkg = pkgs[name] || (pkgs[name] = {
53+ _id: name,
54+ name: name,
55+ versions: {}
56+ })
57+ if (dep.version && dep.integrity && dep.resolved) {
58+ if (!hasNonBlobUrl && !blobUrlRegex.test(dep.resolved)) hasNonBlobUrl = true
59+ pkg.versions[dep.version] = {
60+ dist: {
61+ integrity: dep.integrity,
62+ tarball: dep.resolved
63+ }
64+ }
65+ }
66+ }
67+ if (dep.dependencies) for (var depName in dep.dependencies) {
68+ queue.push(dep.dependencies[depName], depName)
69+ }
70+ }
71+ pkgs._hasNonBlobUrl = hasNonBlobUrl
72+ return pkgs
73+}
74+
75+function npmLogin(registryAddress, cb) {
76+ var tokenLine = registryAddress.replace(/^http:/, '') + ':_authToken=1'
77+ var filename = path.join(os.homedir(), '.npmrc')
78+ fs.readFile(filename, 'utf8', function (err, data) {
79+ if (err && err.code === 'ENOENT') data = ''
80+ else if (err) return cb(new Error(err.stack))
81+ var lines = data ? data.split('\n') : []
82+ if (lines.indexOf(tokenLine) > -1) return cb()
83+ var trailingNewline = (lines.length === 0 || lines[lines.length-1] === '')
84+ var line = trailingNewline ? tokenLine + '\n' : '\n' + tokenLine
85+ fs.appendFile(filename, line, cb)
86+ })
87+}
88+
89+function formatHost(host) {
90+ return /^[^\[]:.*:.*:/.test(host) ? '[' + host + ']' : host
91+}
92+
1593 exports.name = 'npm-registry'
1694 exports.version = '1.0.0'
1795 exports.manifest = {
1896 getAddress: 'async'
1997 }
2098 exports.init = function (sbot, config) {
2199 var port = config.npm ? config.npm.port : 8043
22- var host = config.npm && config.npm.host || 'localhost'
23- var registryAddress
100+ var host = config.npm && config.npm.host || null
101+ var autoAuth = config.npm && config.npm.autoAuth !== false
24102 var getAddressCbs = []
25103
26104 var server = http.createServer(exports.respond(sbot, config))
27- server.listen(port, host, function () {
28- registryAddress = 'http://' + host + ':' + this.address().port + '/'
29- if (config.npm && config.npm.autoAuth !== false) login()
30- console.log('[npm-registry] Listening on ' + registryAddress)
31- while (getAddressCbs.length) getAddressCbs.shift()(null, registryAddress)
105+ var getAddress = onceify(function (cb) {
106+ server.on('error', cb)
107+ server.listen(port, host, function () {
108+ server.removeListener('error', cb)
109+ var regHost = formatHost(host || 'localhost')
110+ var regUrl = 'http://' + regHost + ':' + this.address().port + '/'
111+ if (autoAuth) npmLogin(regUrl, next)
112+ else next()
113+ function next(err) {
114+ cb(err, regUrl)
115+ }
116+ })
117+ sbot.on('close', function () {
118+ server.close()
119+ })
32120 })
33- sbot.on('close', function () {
34- server.close()
121+
122+ getAddress(function (err, addr) {
123+ if (err) return console.error(err)
124+ console.log('[npm-registry] Listening on ' + addr)
35125 })
36126
37- function login() {
38- var filename = path.join(os.homedir(), '.npmrc')
39- var tokenLine = registryAddress.replace(/^http:/, '') + ':_authToken=1'
40- var lines = fs.readFileSync(filename, 'utf8').split('\n')
41- if (lines.indexOf(tokenLine) === -1) {
42- fs.appendFileSync(filename, (lines.pop() ? '' : '\n') + tokenLine)
43- }
44- }
45-
46127 return {
47- getAddress: function (cb) {
48- if (registryAddress) cb(null, registryAddress)
49- else getAddressCbs.push(cb)
50- }
128+ getAddress: getAddress
51129 }
52130 }
53131
54132 exports.respond = function (sbot, config) {
@@ -107,10 +185,12 @@
107185 this.sbot = sbot
108186 this.config = config
109187 this.links2 = sbot.links2
110188 if (!this.links2) throw new Error('missing ssb-links2 scuttlebot plugin')
189+ this.wsPort = config.ws && Number(config.ws.port) || '8989'
111190 this.blobsPrefix = 'http://' + (config.host || 'localhost') + ':'
112- + (config.ws && config.ws.port || '8989') + '/blobs/get/'
191+ + this.wsPort + '/blobs/get/'
192+ this.getBootstrapInfo = onceify(this.getBootstrapInfo, this)
113193 }
114194
115195 SsbNpmRegistryServer.prototype = Object.create(http.Server.prototype)
116196 SsbNpmRegistryServer.prototype.constructor = SsbNpmRegistryServer
@@ -126,10 +206,15 @@
126206 })
127207 })(0)
128208 }
129209
130-SsbNpmRegistryServer.prototype.blobUrl = function (id) {
131- return this.blobsPrefix + id
210+SsbNpmRegistryServer.prototype.blobDist = function (id) {
211+ var m = /^&([^.]+)\.([a-z0-9]+)$/.exec(id)
212+ if (!m) throw new Error('bad blob id: ' + id)
213+ return {
214+ integrity: m[2] + '-' + m[1],
215+ tarball: 'http://localhost:' + this.wsPort + '/blobs/get/' + id
216+ }
132217 }
133218
134219 SsbNpmRegistryServer.prototype.getMentions = function (name) {
135220 return this.links2.read({
@@ -146,18 +231,80 @@
146231 ]
147232 })
148233 }
149234
235+SsbNpmRegistryServer.prototype.getBootstrapInfo = function (cb) {
236+ var self = this
237+ if (!self.sbot.bootstrap) return cb(new Error('missing sbot bootstrap plugin'))
238+
239+ self.sbot.bootstrap.getPackageLock(function (err, sbotPkgLock) {
240+ if (err) return cb(new Error(err.stack || err))
241+ var pkgs = pkgLockToRegistryPkgs(sbotPkgLock, self.wsPort)
242+ if (pkgs._hasNonBlobUrl) {
243+ console.error('[npm-registry] Warning: package-lock.json has non-blob URLs. Bootstrap installation may not be fully peer-to-peer.')
244+ }
245+
246+ if (!sbotPkgLock.name) console.trace('missing pkg lock name')
247+ if (!sbotPkgLock.version) console.trace('missing pkg lock version')
248+
249+ self.sbot.blobs.add(function (err, id) {
250+ if (err) return cb(new Error(err.stack || err))
251+ var pkg = pkgs[sbotPkgLock.name] || (pkgs[sbotPkgLock.name] = {})
252+ var versions = pkg.versions || (pkg.versions = {})
253+ pkg.versions[sbotPkgLock.version] = {
254+ dist: self.blobDist(id)
255+ }
256+ var distTags = pkg['dist-tags'] || (pkg['dist-tags'] = {})
257+ distTags.latest = sbotPkgLock.version
258+ next()
259+ })(self.sbot.bootstrap.pack())
260+
261+ function next() {
262+ fs.readFile(path.join(__dirname, 'bootstrap.js'), {
263+ encoding: 'utf8'
264+ }, function (err, bootstrapScript) {
265+ if (err) return cb(err)
266+ var script = bootstrapScript + '\n' +
267+ 'exports.pkgs = ' + JSON.stringify(pkgs, 0, 2)
268+
269+ self.sbot.blobs.add(function (err, id) {
270+ if (err) return cb(new Error(err.stack || err))
271+ var m = /^&([^.]+)\.([a-z0-9]+)$/.exec(id)
272+ if (!m) return cb(new Error('bad blob id: ' + id))
273+ cb(null, {
274+ name: sbotPkgLock.name,
275+ blob: id,
276+ hashType: m[2],
277+ hashBuf: Buffer.from(m[1], 'base64'),
278+ })
279+ })(pullOnce(script))
280+ })
281+ }
282+ })
283+}
284+
285+SsbNpmRegistryServer.prototype.getBootstrapScriptHash = function (cb) {
286+ var hasher = crypto.createHash('sha256')
287+ hasher.update(data)
288+ var hash = hasher.digest()
289+ getBootstrapScriptHash = function (cb) {
290+ return cb(null, hash)
291+ }
292+ getBootstrapScriptHash(cb)
293+}
294+
150295 function Req(server, req, res) {
151296 this.server = server
152297 this.req = req
153298 this.res = res
154299 this.blobsToPush = []
155300 }
156301
157302 Req.prototype.serve = function () {
158- // console.log(this.req.method, this.req.url)
303+ console.log(this.req.method, this.req.url, this.req.socket.remoteAddress)
159304 var pathname = this.req.url.replace(/\?.*/, '')
305+ if (pathname === '/') return this.serveHome()
306+ if (pathname === '/bootstrap') return this.serveBootstrap()
160307 if (pathname === '/-/whoami') return this.serveWhoami()
161308 if (pathname === '/-/ping') return this.respond(200, true)
162309 if (pathname === '/-/user/org.couchdb.user:1') return this.serveUser1()
163310 if (!/^\/-\//.test(pathname)) return this.servePkg(pathname.substr(1))
@@ -172,8 +319,46 @@
172319 Req.prototype.respondError = function (status, message) {
173320 this.respond(status, {error: message})
174321 }
175322
323+var bootstrapName = 'ssb-npm-bootstrap'
324+
325+Req.prototype.serveHome = function () {
326+ var self = this
327+ self.res.writeHead(200, {'content-type': 'text/html'})
328+ var port = 8044
329+ self.res.end('<!doctype html><html><head><meta charset=utf-8>' +
330+ '<title>' + escapeHTML(pkg.name) + '</title></head><body>' +
331+ '<h1>' + escapeHTML(pkg.name) + '</h1>\n' +
332+ '<p><a href="/bootstrap">Bootstrap</a></p>\n' +
333+ '</body></html>')
334+}
335+
336+Req.prototype.serveBootstrap = function () {
337+ var self = this
338+ self.server.getBootstrapInfo(function (err, info) {
339+ if (err) return this.respondError(err.stack || err)
340+ var pkgNameText = info.name
341+ var pkgTmpText = '/tmp/' + bootstrapName + '.js'
342+ var host = String(self.req.headers.host).replace(/:[0-9]*$/, '') || self.req.socket.localAddress
343+ var httpHost = /^[^\[]:.*:.*:/.test(host) ? '[' + host + ']' : host
344+ var blobsHostname = httpHost + ':' + self.server.wsPort
345+ var tarballLink = 'http://' + blobsHostname + '/blobs/get/' + info.blob
346+ var pkgHashText = info.hashBuf.toString('hex')
347+ var hashCmd = info.hashType + 'sum'
348+
349+ var script =
350+ 'wget \'' + tarballLink + '\' -O ' + pkgTmpText + ' &&\n' +
351+ 'echo ' + pkgHashText + ' ' + pkgTmpText + ' | ' + hashCmd + ' -c &&\n' +
352+ 'node ' + pkgTmpText + ' --blobs-remote ' + blobsHostname + ' -- ' +
353+ 'npm install -g ' + info.name + ' &&\n' +
354+ 'sbot server'
355+
356+ self.res.writeHead(200, {'content-type': 'text/plain'})
357+ self.res.end(script)
358+ })
359+}
360+
176361 Req.prototype.serveWhoami = function () {
177362 var self = this
178363 self.server.sbot.whoami(function (err, feed) {
179364 if (err) return self.respondError(err.stack || err)
@@ -213,27 +398,33 @@
213398 if (err === true) return self.respond(200, obj)
214399 if (err) return self.respondError(500, err.stack || err)
215400 var data = decodeName(mention.name)
216401 if (!data.version) return
217- var m = /^&([^.]+)\.([a-z0-9]+)$/.exec(mention.link)
218- if (!m) return
219402 if (data.distTag) obj['dist-tags'][data.distTag] = data.version
220403 obj.versions[data.version] = {
221404 author: {
222405 url: mention.author
223406 },
224- dist: {
225- integrity: m[2] + '-' + m[1],
226- tarball: self.server.blobUrl(mention.link)
227- }
407+ dist: self.server.blobDist(mention.link)
228408 }
229409 getMention(null, next)
230410 })
231411 }
232412
413+var localhosts = {
414+ '::1': true,
415+ '127.0.0.1': true,
416+ '::ffff:127.0.0.1': true,
417+}
418+
233419 Req.prototype.publishPkg = function (pkgName) {
420+ var self = this
421+ var remoteAddress = self.req.socket.remoteAddress
422+ if (!(remoteAddress in localhosts)) {
423+ return self.respondError(403, 'You may not publish as this user.')
424+ }
425+
234426 var chunks = []
235- var self = this
236427 self.req.on('data', function (data) {
237428 chunks.push(data)
238429 })
239430 self.req.on('end', function () {
@@ -248,9 +439,9 @@
248439 }
249440
250441 Req.prototype.publishPkg2 = function (name, data) {
251442 var self = this
252- if (data.users) console.trace('[npm-registry users property is not supported')
443+ if (data.users) console.trace('[npm-registry] users property is not supported')
253444 var attachments = data._attachments || {}
254445 var links = {/* <name>-<version>.tgz: {link: <BlobId>, size: number} */}
255446 var waiting = 0
256447 Object.keys(attachments).forEach(function (filename) {
bootstrap.jsView
@@ -1,0 +1,237 @@
1+var http = require('http')
2+var os = require('os')
3+var fs = require('fs')
4+var path = require('path')
5+var URL = require('url')
6+var http = require('http')
7+var https = require('https')
8+var crypto = require('crypto')
9+var Transform = require('stream').Transform
10+var proc = require('child_process')
11+
12+function usage(code) {
13+ console.error('Usage: ' + process.argv[0] + ' ' + process.argv[1] + '\n' +
14+ ' [--help] [--verbose]\n' +
15+ ' [--blobs-host <host>] [--blobs-port <port>]\n' +
16+ ' [--registry-host <host>] [--registry-port <port>]\n' +
17+ ' [--blobs-remote <remote_address>]\n' +
18+ ' [--] [<cmd> <args...>]')
19+ process.exit(code)
20+}
21+
22+var ssbAppname = process.env.ssb_appname || 'ssb'
23+var ssbPath = process.env.ssb_path || path.join(os.homedir(), '.' + ssbAppname)
24+var blobsPath = path.join(ssbPath, 'blobs')
25+var blobsTmpPath = path.join(blobsPath, 'tmp')
26+
27+var numTmpBlobs = 0
28+var remotes = []
29+
30+function blobFilename(buf) {
31+ var str = buf.toString('hex')
32+ return path.join(blobsPath, 'sha256', str.slice(0, 2), str.slice(2))
33+}
34+
35+function formatHttpAddr(addr) {
36+ return 'http://' + (typeof addr === 'string' ? 'unix:' + addr
37+ : addr.family === 'IPv6' ? '[' + addr.address + ']:' + addr.port
38+ : addr.address + ':' + addr.port)
39+}
40+
41+function serveBlobs(opts, req, res) {
42+ var p = URL.parse(req.url)
43+ if (p.pathname === '/') return serveStatus(res, 204)
44+ var m = /^\/blobs\/get\/(&([A-Za-z0-9\/+]{43}=)\.sha256)$/.exec(p.pathname)
45+ if (m) return serveBlobsGet(req, res, opts.remote, m[1], new Buffer(m[2], 'base64'))
46+ return serveStatus(res, 404, 'Not Found')
47+}
48+
49+function serveRegistry(opts, req, res) {
50+ var p = URL.parse(req.url)
51+ var pathname = req.url.replace(/\?.*/, '')
52+ if (pathname === '/') return serveStatus(res, 204)
53+ if (pathname === '/-/ping') return serveStatus(res, 200, null, '"pong"')
54+ if (/^\/-\//.test(pathname)) return serveStatus(404)
55+ return servePkg(opts.pkgs, req, res, pathname.substr(1))
56+}
57+
58+function serveStatus(res, code, message, body) {
59+ res.writeHead(code, message)
60+ res.end(body || message)
61+}
62+
63+function serveStop(opts, req, res) {
64+ var addr = req.socket.remoteAddress
65+ if (addr !== '::1' && addr !== '127.0.0.1' && addr !== '::ffff:127.0.0.1') {
66+ return serveStatus(res, 403)
67+ }
68+ serveStatus(res, 200)
69+ opts.registryServer.close()
70+ opts.blobsServer.close()
71+}
72+
73+function serveBlobsGet(req, res, remote, id, hash) {
74+ var filename = blobFilename(hash)
75+ getAddBlob(remote, id, hash, filename, function (err, stream) {
76+ if (err) return serveStatus(res, 500, null, err.message)
77+ if (!stream) return serveStatus(res, 404, 'Blob Not Found')
78+ res.writeHead(200)
79+ stream.pipe(res)
80+ })
81+}
82+
83+function getAddBlob(remote, id, hash, filename, cb) {
84+ fs.access(filename, fs.constants.R_OK, function (err) {
85+ if (err && err.code === 'ENOENT') {
86+ return fetchAddBlob(remote, id, hash, filename, cb)
87+ }
88+ if (err) return cb(err)
89+ cb(null, fs.createReadStream(filename))
90+ })
91+}
92+
93+function getRemoteBlob(remote, id, cb) {
94+ if (/^https:\/\//.test(remote)) return https.get(remote + id, cb)
95+ if (/^http:\/\//.test(remote)) return https.get(remote + id, cb)
96+ return http.get('http://' + remote + '/blobs/get/' + id, cb)
97+}
98+
99+function mkdirp(dir, cb) {
100+ fs.stat(dir, function (err, stats) {
101+ if (!err) return cb()
102+ fs.mkdir(dir, function (err) {
103+ if (!err) return cb()
104+ mkdirp(path.dirname(dir), function (err) {
105+ if (err) return cb(err)
106+ fs.mkdir(dir, cb)
107+ })
108+ })
109+ })
110+}
111+
112+function rename(src, dest, cb) {
113+ mkdirp(path.dirname(dest), function (err) {
114+ if (err) return cb(err)
115+ fs.rename(src, dest, cb)
116+ })
117+}
118+
119+function fetchAddBlob(remote, id, hash, filename, cb) {
120+ var req = getRemoteBlob(remote, id, function (res) {
121+ req.removeListener('error', cb)
122+ if (res.statusCode !== 200) return cb(new Error(res.statusMessage))
123+ mkdirp(blobsTmpPath, function (err) {
124+ if (err) return res.destroy(), cb(err)
125+ var blobTmpPath = path.join(blobsTmpPath, Date.now() + '-' + numTmpBlobs++)
126+ fs.open(blobTmpPath, 'w+', function (err, fd) {
127+ if (err) return res.destroy(), cb(err)
128+ var writeStream = fs.createWriteStream(null, {
129+ fd: fd, flags: 'w+', autoClose: false})
130+ var hasher = crypto.createHash('sha256')
131+ var hashThrough = new Transform({
132+ transform: function (data, encoding, cb) {
133+ hasher.update(data)
134+ cb(null, data)
135+ }
136+ })
137+ res.pipe(hashThrough).pipe(writeStream, {end: false})
138+ res.on('error', function (err) {
139+ writeStream.end(function (err1) {
140+ fs.unlink(blobTmpPath, function (err2) {
141+ cb(err || err1 || err2)
142+ })
143+ })
144+ })
145+ hashThrough.on('end', function () {
146+ var receivedHash = hasher.digest()
147+ if (hash.compare(receivedHash)) {
148+ writeStream.end(function (err) {
149+ fs.unlink(blobTmpPath, function (err1) {
150+ cb(err || err1)
151+ })
152+ })
153+ } else {
154+ res.unpipe(hashThrough)
155+ rename(blobTmpPath, filename, function (err) {
156+ if (err) return console.error(err)
157+ cb(null, fs.createReadStream(null, {fd: fd, start: 0}))
158+ })
159+ }
160+ })
161+ })
162+ })
163+ })
164+ req.on('error', cb)
165+}
166+
167+function servePkg(pkgs, req, res, pathname) {
168+ var self = this
169+ var parts = pathname.split('/')
170+ var pkgName = parts.shift()
171+ if (parts[0] === '-rev') return serveStatus(res, 501, 'Unpublish is not supported')
172+ if (parts.length > 0) return serveStatus(res, 404)
173+ var pkg = pkgs[pkgName] || {}
174+ serveStatus(res, 200, null, JSON.stringify(pkg, 0, 2))
175+}
176+
177+function startServers(opts, cb) {
178+ var waiting = 2
179+ opts.blobsServer = http.createServer(serveBlobs.bind(null, opts))
180+ .listen(opts.blobsPort, opts.blobsHost, function () {
181+ if (opts.verbose) console.error('blobs listening on ' + formatHttpAddr(this.address()))
182+ if (!--waiting) cb()
183+ })
184+ opts.registryServer = http.createServer(serveRegistry.bind(null, opts))
185+ .listen(opts.registryPort, opts.registryHost, function () {
186+ if (opts.verbose) console.error('registry listening on ' + formatHttpAddr(this.address()))
187+ if (!--waiting) cb()
188+ })
189+}
190+
191+process.nextTick(function () {
192+ var args = process.argv.slice(2)
193+ var opts = {
194+ pkgs: exports.pkgs || {},
195+ blobsPort: 8989,
196+ registryPort: 8043,
197+ }
198+ var cmd
199+
200+ getopts: while (args.length) {
201+ var arg = args.shift()
202+ switch (arg) {
203+ case '--help': return usage(0)
204+ case '--verbose': opts.verbose = true; break
205+ case '--blobs-host': opts.blobsHost = args.shift(); break
206+ case '--blobs-port': opts.blobsPort = args.shift(); break
207+ case '--registry-port': opts.registryPort = args.shift(); break
208+ case '--registry-host': opts.registryHost = args.shift(); break
209+ case '--blobs-remote': opts.remote = args.shift(); break
210+ case '--': cmd = args.shift(); break getopts
211+ default: if (/^--/.test(arg)) return usage(1); cmd = arg; break getopts
212+ }
213+ }
214+
215+ startServers(opts, function (err) {
216+ if (err) throw err
217+ if (cmd) {
218+ var regHost = opts.registryHost || 'localhost'
219+ var regHostname = regHost + ':' + opts.registryPort
220+ if (cmd === 'npm' || cmd === 'npx') {
221+ args.push('--registry=http://' + regHostname + '/')
222+ args.push('--//' + regHostname + '/:_authToken=1')
223+ }
224+ var child = proc.spawn(cmd, args, {
225+ stdio: 'inherit'
226+ })
227+ child.on('exit', process.exit)
228+ process.on('SIGINT', function () { child.kill('SIGINT') })
229+ process.on('SIGTERM', function () { child.kill('SIGTERM') })
230+ process.on('uncaughtException', function (e) {
231+ console.error(e)
232+ child.kill('SIGKILL')
233+ process.exit(1)
234+ })
235+ }
236+ })
237+})

Built with git-ssb-web