UNPKG

pear-api

Version:

Pear API Base & Integration Module

386 lines (343 loc) 12.9 kB
'use strict' /* global Bare */ const Pipe = require('bare-pipe') const readline = require('readline') const tty = require('tty') const { Writable, Readable } = require('streamx') const { once } = require('events') const hypercoreid = require('hypercore-id-encoding') const byteSize = require('tiny-byte-size') const { isWindows } = require('which-runtime') const { CHECKOUT } = require('./constants') const teardown = require('./teardown') const opwait = require('./opwait') const isTTY = tty.isTTY(0) const pt = (arg) => arg const es = () => '' const ansi = isWindows ? { bold: pt, dim: pt, italic: pt, underline: pt, inverse: pt, red: pt, green: pt, yellow: pt, gray: pt, upHome: es, link: pt, hideCursor: es, showCursor: es } : { bold: (s) => `\x1B[1m${s}\x1B[22m`, dim: (s) => `\x1B[2m${s}\x1B[22m`, italic: (s) => `\x1B[3m${s}\x1B[23m`, underline: (s) => `\x1B[4m${s}\x1B[24m`, inverse: (s) => `\x1B[7m${s}\x1B[27m`, red: (s) => `\x1B[31m${s}\x1B[39m`, green: (s) => `\x1B[32m${s}\x1B[39m`, yellow: (s) => `\x1B[33m${s}\x1B[39m`, gray: (s) => `\x1B[90m${s}\x1B[39m`, upHome: (n = 1) => `\x1B[${n}F`, link: (url, text = url) => `\x1B]8;;${url}\x07${text}\x1B]8;;\x07`, hideCursor: () => '\x1B[?25l', showCursor: () => '\x1B[?25h' } ansi.sep = isWindows ? '-' : ansi.dim(ansi.green('∞')) ansi.tick = isWindows ? '^' : ansi.green('✔') ansi.cross = isWindows ? 'x' : ansi.red('✖') ansi.warning = isWindows ? '!' : '⚠️' ansi.pear = isWindows ? '*' : '🍐' ansi.dot = isWindows ? '•' : 'o' ansi.key = isWindows ? '>' : '🔑' ansi.down = isWindows ? '↓' : '⬇' ansi.up = isWindows ? '↑' : '⬆' const stdio = new class Stdio { drained = Writable.drained constructor () { this._in = null this._out = null this._err = null this.rawMode = false } get inAttached () { return this._in !== null } get in () { if (this._in === null) { this._in = tty.isTTY(0) ? new tty.ReadStream(0) : new Pipe(0) this._in.once('close', () => { this._in = null }) } return this._in } get out () { if (this._out === null) this._out = tty.isTTY(1) ? new tty.WriteStream(1) : new Pipe(1) return this._out } get err () { if (this._err === null) this._err = tty.isTTY(2) ? new tty.WriteStream(2) : new Pipe(2) return this._err } size () { if (!this.out.getWindowSize) return [80, 80] const [width, height] = this.out.getWindowSize() return { width, height } } raw (rawMode) { this.rawMode = !!rawMode return this.in.setMode(this.rawMode ? this.tty.constants.MODE_RAW : this.tty.constants.MODE_NORMAL) } }() class Interact { static rx = /[\x1B\x9B][[\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\d/#&.:=?%@~_]+)*|[a-zA-Z\d]+(?:;[-a-zA-Z\d/#&.:=?%@~_]*)*)?\x07)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g // eslint-disable-line no-control-regex constructor (header, params, opts = {}) { this._header = header this._params = params this._defaults = opts.defaults || {} const mask = (data, cb) => { if (data.length > 4) { // is full line const prompt = this._rl._prompt const regex = new RegExp(`(${prompt})([\\x20-\\x7E]+)`, 'g') // match printable chars after prompt const masked = data.toString().replace(regex, (_, prompt, pwd) => prompt + '*'.repeat(pwd.length)) stdio.out.write(masked) } else { stdio.out.write(data) } cb(null) } this._rl = readline.createInterface({ input: stdio.in, output: opts.masked ? new Writable({ write: mask }) : stdio.out }) this._rl.input?.setMode(tty.constants.MODE_RAW) this._rl.on('close', () => { console.log() // new line Bare.exit() }) } async run (opts) { try { return await this.#run(opts) } finally { if (stdio.inAttached) stdio.in.destroy() } } async #run (opts = {}) { if (opts.autosubmit) return this.#autosubmit() stdio.out.write(this._header) const fields = {} const shave = {} const defaults = this._defaults while (this._params.length) { const param = this._params.shift() while (true) { const deflt = defaults[param.name] ?? param.default let answer = await this.#input(`${param.prompt}${param.delim || ':'}${deflt && ' (' + deflt + ')'} `) if (answer.length === 0) answer = defaults[param.name] ?? deflt if (!param.validation || await param.validation(answer)) { if (typeof answer === 'string') answer = answer.replace(this.constructor.rx, '') fields[param.name] = answer if (Array.isArray(param.shave) && param.shave.every((ix) => typeof ix === 'number')) shave[param.name] = param.shave break } else { stdio.out.write(param.msg + '\n') } } } return { fields, shave } } #autosubmit () { const fields = {} const shave = {} const defaults = this._defaults while (this._params.length) { const param = this._params.shift() fields[param.name] = defaults[param.name] ?? param.default if (Array.isArray(param.shave) && param.shave.every((ix) => typeof ix === 'number')) shave[param.name] = param.shave } return { fields, shave } } async #input (prompt) { stdio.out.write(prompt) this._rl._prompt = prompt const answer = (await once(this._rl, 'data')).toString() return answer.trim() // remove return char } } let statusFrag = '' function status (msg, success) { msg = msg || '' const done = typeof success === 'boolean' if (msg) stdio.out.write(`\x1B[2K\r${indicator(success)}${msg}\n${done ? '' : ansi.upHome()}`) statusFrag = msg.slice(0, 3) } function print (message, success) { statusFrag = '' console.log(`${typeof success !== 'undefined' ? indicator(success) : ''}${message}`) } function byteDiff ({ type, sizes, message }) { statusFrag = '' sizes = sizes.map((size) => (size > 0 ? '+' : '') + byteSize(size)).join(', ') print(indicator(type, 'diff') + ' ' + message + ' (' + sizes + ')') } function indicator (value, type = 'success') { if (value === undefined) return '' if (value === true) value = 1 else if (value === false) value = -1 else if (value == null) value = 0 if (type === 'diff') return value === 0 ? ansi.yellow('~') : (value === 1 ? ansi.green('+') : ansi.red('-')) return value < 0 ? ansi.cross + ' ' : (value > 0 ? ansi.tick + ' ' : ansi.gray('- ')) } const outputter = (cmd, taggers = {}) => (opts, stream, info = {}, ipc) => { if (Array.isArray(stream)) stream = Readable.from(stream) const asTTY = opts.ctrlTTY ?? isTTY if (asTTY) stdio.out.write(ansi.hideCursor()) const dereg = asTTY ? teardown(() => { if (!isWindows) stdio.out.write('\x1B[1K\x1B[G' + statusFrag) // clear ^C stdio.out.write(ansi.showCursor()) }) : null if (typeof opts === 'boolean') opts = { json: opts } const { json = false, log } = opts const promise = opwait(stream, ({ tag, data }) => { if (json) { const str = JSON.stringify({ cmd, tag, data }) if (log) log(str) else print(str) return } const transform = Promise.resolve(typeof taggers[tag] === 'function' ? taggers[tag](data, info, ipc) : taggers[tag] || false) transform.then((result) => { if (result === undefined) return if (typeof result === 'string') result = { output: 'print', message: result } if (result === false) { if (tag === 'final') result = { output: 'print', message: (data.message ?? data.success ? 'Success' : 'Failure') } else result = {} } result.success = result.success ?? data?.success const { output, message, success = data?.success } = result if (log) { const logOpts = { output, ...(typeof success === 'boolean' ? { success } : {}) } if (Array.isArray(message) === false) log(message, logOpts) else for (const msg of message) log(msg, logOpts) return } let msg = Array.isArray(message) ? message.join('\n') : message if (tag === 'final') msg += '\n' if (output === 'print') print(msg, success) if (output === 'status') status(msg, success) }, (err) => stream.destroy(err)) }) return !asTTY ? promise : promise.finally(() => { stdio.out.write(ansi.showCursor()) dereg(false) }) } const banner = `${ansi.bold('Pear')} ~ ${ansi.dim('Welcome to the Internet of Peers')}` const version = `${CHECKOUT.fork || 0}.${CHECKOUT.length || 'dev'}.${CHECKOUT.key}` const header = ` ${banner} ${ansi.pear + ' '}${ansi.bold(ansi.gray('v' + version))} ` const urls = ansi.link('https://pears.com', 'pears.com') + ' | ' + ansi.link('https://holepunch.to', 'holepunch.to') + ' | ' + ansi.link('https://keet.io', 'keet.io') const footer = { overview: ` ${ansi.bold('Legend:')} [arg] = optional, <arg> = required, | = or \n Run ${ansi.bold('pear help')} to output full help for all commands\n For command help: ${ansi.bold('pear help [cmd]')} or ${ansi.bold('pear [cmd] -h')}\n ${ansi.pear + ' '}${version}\n${urls}\n${ansi.bold(ansi.dim('Pear'))} ~ ${ansi.dim('Welcome to the IoP')}`, help: `${ansi.pear + ' '}${version} ${urls}\n${ansi.bold(ansi.dim('Pear'))} ~ ${ansi.dim('Welcome to the IoP')} ` } const usage = { header, version, banner, footer } async function trust (ipc, key, cmd) { const explain = { run: 'Be sure that software is trusted before running it\n' + '\nType "TRUST" to allow execution or anything else to exit\n\n', init: 'This template is not trusted.\n' + '\nType "TRUST" to trust this template, or anything else to exit\n\n' } const act = { run: 'Use pear run again to execute trusted application', init: 'Use pear init again to initalize from trusted template' } const ask = { run: 'Trust application', init: 'Trust template' } const z32 = hypercoreid.encode(key) const dialog = ansi.cross + ' Key pear://' + z32 + ' is not known\n\n' + explain[cmd] const delim = '?' const validation = (value) => value === 'TRUST' const msg = '\n' + ansi.cross + ' uppercase TRUST to confirm\n' const interact = new Interact(dialog, [ { name: 'value', default: '', prompt: ask[cmd], delim, validation, msg } ]) await interact.run() await ipc.permit({ key }) print('\n' + ansi.tick + ' pear://' + z32 + ' is now trusted\n') print(act[cmd] + '\n') await ipc.close() Bare.exit() } async function password (ipc, key, cmd) { const z32 = hypercoreid.normalize(key) const explain = { run: 'pear://' + z32 + ' is an encrypted application. \n' + '\nEnter the password to run the app.\n\n', stage: 'This application is encrypted.\n' + '\nEnter the password to stage the app.\n\n', seed: 'This application is encrypted.\n' + '\nEnter the password to seed the app.\n\n', dump: 'This application is encrypted.\n' + '\nEnter the password to dump the app.\n\n', init: 'This template is encrypted.\n' + '\nEnter the password to init from the template.\n\n', info: 'This application is encrypted.\n' + '\nEnter the password to retrieve info.\n\n' } const message = { run: 'Added encryption key for pear://' + z32, stage: 'Added encryption key, run stage again to complete it.', seed: 'Added encryption key, run seed again to complete it.', dump: 'Added encryption key, run dump again to complete it.', init: 'Added encryption key, run init again to complete it.', info: 'Added encryption key, run info again to complete it.' } const dialog = ansi.cross + ' ' + explain[cmd] const ask = 'Password' const delim = ':' const validation = (key) => key.length > 0 const msg = '\nPlease, enter a valid password.\n' const interact = new Interact(dialog, [ { name: 'value', default: '', prompt: ask, delim, validation, msg } ], { masked: true }) const { fields } = await interact.run() print(`\n${ansi.key} Hashing password...`) await ipc.permit({ key, password: fields.value }) print('\n' + ansi.tick + ' ' + message[cmd] + '\n') await ipc.close() Bare.exit() } function permit (ipc, info, cmd) { const key = info.key if (info.encrypted) { return password(ipc, key, cmd) } else { return trust(ipc, key, cmd) } } async function confirm (dialog, ask, delim, validation, msg) { const interact = new Interact(dialog, [ { name: 'value', default: '', prompt: ask, delim, validation, msg } ]) await interact.run() } module.exports = { usage, permit, stdio, ansi, indicator, status, print, outputter, isTTY, confirm, byteSize, byteDiff, Interact }