pear-terminal 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +50 -42
  2. package/index.js +260 -126
  3. package/package.json +17 -5
package/README.md CHANGED
@@ -16,10 +16,9 @@ Ask user to trust or unlock an app/template.
16
16
 
17
17
  Returns `Promise<void>`.
18
18
 
19
- * `ipc`: `{ permit({ key, password? }), close() }`
20
- * `info`: `{ key: Buffer|string, encrypted: boolean }`
21
- * `cmd`: `'run'|'init'|'stage'|'seed'|'dump'|'info'`
22
-
19
+ - `ipc`: `{ permit({ key, password? }), close() }`
20
+ - `info`: `{ key: Buffer|string, encrypted: boolean }`
21
+ - `cmd`: `'run'|'init'|'stage'|'seed'|'dump'|'info'`
23
22
 
24
23
  ### `confirm(dialog, ask, delim, validation, msg)`
25
24
 
@@ -27,20 +26,20 @@ One-shot confirmation prompt.
27
26
 
28
27
  Returns `Promise<void>`.
29
28
 
30
- * `dialog`: `string` preface text
31
- * `ask`: `string` prompt label
32
- * `delim`: `string` delimiter (e.g. `':'` or '?'`)
33
- * `validation`: `(value:string) => boolean|Promise<boolean>`
34
- * `msg`: `string` error message on invalid input
29
+ - `dialog`: `string` preface text
30
+ - `ask`: `string` prompt label
31
+ - `delim`: `string` delimiter (e.g. `':'` or '?'`)
32
+ - `validation`: `(value:string) => boolean|Promise<boolean>`
33
+ - `msg`: `string` error message on invalid input
35
34
 
36
35
  ### `const interact = new Interact(header, params[, opts])`
37
36
 
38
37
  Interactive prompt runner.
39
38
 
40
- * `header`: `string` shown once before prompts
41
- * `params`: `Array<{ name, prompt, default?, delim?, validation?, msg?, shave? }>`
42
- * `opts.masked`: `boolean` mask user input (passwords)
43
- * `opts.defaults`: `{ [name:string]: any }` fallback values
39
+ - `header`: `string` shown once before prompts
40
+ - `params`: `Array<{ name, prompt, default?, delim?, validation?, msg?, shave? }>`
41
+ - `opts.masked`: `boolean` mask user input (passwords)
42
+ - `opts.defaults`: `{ [name:string]: any }` fallback values
44
43
 
45
44
  #### `interact.run([opts])`
46
45
 
@@ -48,7 +47,7 @@ Process prompts and return answers.
48
47
 
49
48
  Returns `Promise<{ fields, shave }>`.
50
49
 
51
- * `opts.autosubmit`: `boolean` fill with defaults without prompting
50
+ - `opts.autosubmit`: `boolean` fill with defaults without prompting
52
51
 
53
52
  ### `stdio`
54
53
 
@@ -56,11 +55,11 @@ Thin stdio wrapper with Bare/TTY streams.
56
55
 
57
56
  Returns object with:
58
57
 
59
- * `in`, `out`, `err`: lazy streams
60
- * `size() -> { width, height }`
61
- * `raw(bool) -> void` set raw mode
62
- * `drained(stream) -> Promise<void>`
63
- * `inAttached`: `boolean`
58
+ - `in`, `out`, `err`: lazy streams
59
+ - `size() -> { width, height }`
60
+ - `raw(bool) -> void` set raw mode
61
+ - `drained(stream) -> Promise<void>`
62
+ - `inAttached`: `boolean`
64
63
 
65
64
  ### `ansi`
66
65
 
@@ -68,10 +67,10 @@ ANSI styling helpers (no-op on Windows).
68
67
 
69
68
  Returns object with:
70
69
 
71
- * text: `bold, dim, italic, underline, inverse, red, green, yellow, gray`
72
- * cursor: `upHome(n)`, `hideCursor()`, `showCursor()`
73
- * links: `link(url, text?)`
74
- * glyphs: `sep, tick, cross, warning, pear, dot, key, down, up`
70
+ - text: `bold, dim, italic, underline, inverse, red, green, yellow, gray`
71
+ - cursor: `upHome(n)`, `hideCursor()`, `showCursor()`
72
+ - links: `link(url, text?)`
73
+ - glyphs: `sep, tick, cross, warning, pear, dot, key, down, up`
75
74
 
76
75
  ### `indicator(value[, type])`
77
76
 
@@ -79,8 +78,8 @@ Status glyph helper.
79
78
 
80
79
  Returns `string`.
81
80
 
82
- * `value`: `true|false|null|number` (`>0` success, `<0` fail, `0|null` neutral)
83
- * `type`: `'success'|'diff'` (`diff`: `+ | - | ~`)
81
+ - `value`: `true|false|null|number` (`>0` success, `<0` fail, `0|null` neutral)
82
+ - `type`: `'success'|'diff'` (`diff`: `+ | - | ~`)
84
83
 
85
84
  ### `status(message[, success])`
86
85
 
@@ -88,8 +87,8 @@ Live status line (TTY-aware).
88
87
 
89
88
  Returns `void`.
90
89
 
91
- * `message`: `string`
92
- * `success`: `boolean|null|number` (see `indicator`)
90
+ - `message`: `string`
91
+ - `success`: `boolean|null|number` (see `indicator`)
93
92
 
94
93
  ### `print(message[, success])`
95
94
 
@@ -97,8 +96,8 @@ Plain line print with optional status glyph.
97
96
 
98
97
  Returns `void`.
99
98
 
100
- * `message`: `string`
101
- * `success`: `boolean|null|number`
99
+ - `message`: `string`
100
+ - `success`: `boolean|null|number`
102
101
 
103
102
  ### `byteDiff({ type, sizes, message })`
104
103
 
@@ -106,9 +105,9 @@ Pretty-print byte deltas.
106
105
 
107
106
  Returns `void`.
108
107
 
109
- * `type`: any value passed to `indicator(..., 'diff')`
110
- * `sizes`: `number[]` byte changes (signed)
111
- * `message`: `string`
108
+ - `type`: any value passed to `indicator(..., 'diff')`
109
+ - `sizes`: `number[]` byte changes (signed)
110
+ - `message`: `string`
112
111
 
113
112
  ### `outputter(cmd[, taggers])`
114
113
 
@@ -116,23 +115,32 @@ Create a stream consumer that routes tagged events to `print/status` (TTY) or JS
116
115
 
117
116
  Returns `(opts, stream, info?, ipc?) -> Promise<void>`.
118
117
 
119
- * `cmd`: `string` command name
120
- * `taggers`: `{ [tag]: (data, info, ipc) => string|{ output, message, success }|false|Promise<...> }`
121
- * `output`: `'print'|'status'`
122
- * `message`: `string|string[]`
123
- * `success`: `boolean`
124
- * `opts`: `{ json?: boolean, log?: (msg, { output, success? }) => void, ctrlTTY?: boolean }`
125
- * `stream`: `Readable|Array` of `{ tag, data }` events
126
- * `info`: any extra context
127
- * `ipc`: optional IPC handle
118
+ - `cmd`: `string` command name
119
+ - `taggers`: `{ [tag]: (data, info, ipc) => string|{ output, message, success }|false|Promise<...> }`
120
+ - `output`: `'print'|'status'`
121
+ - `message`: `string|string[]`
122
+ - `success`: `boolean`
123
+ - `opts`: `{ json?: boolean, log?: (msg, { output, success? }) => void, ctrlTTY?: boolean }`
124
+ - `stream`: `Readable|Array` of `{ tag, data }` events
125
+ - `info`: any extra context
126
+ - `ipc`: optional IPC handle
128
127
 
129
128
  Behavior:
129
+
130
130
  - `opts.json === true` → emits JSON lines: `{ cmd, tag, data }`
131
131
  - `tagger` result `false`:
132
132
  - for `tag==='final'` prints default success/failure
133
133
  - otherwise suppressed
134
134
  - TTY: hides/shows cursor, handles Ctrl+C cleanup
135
135
 
136
+ ### `explain(bail)`
137
+
138
+ Processes failure mode flow for various Pear scenarios that prints sensible output showing stacks for operational errors and beautified output user errors.
139
+
140
+ Returns `void`.
141
+
142
+ - `bail`: a [paparam](https://github.com/holepunchto/paparam) bail object
143
+
136
144
  ### `isTTY`
137
145
 
138
146
  `boolean` indicating stdin TTY status.
@@ -145,4 +153,4 @@ Returns `string`.
145
153
 
146
154
  ## License
147
155
 
148
- Apache-2.0
156
+ Apache-2.0
package/index.js CHANGED
@@ -1,23 +1,42 @@
1
1
  'use strict'
2
- /* global Bare */
2
+
3
3
  const readline = require('bare-readline')
4
4
  const tty = require('bare-tty')
5
5
  const fs = require('bare-fs')
6
- const { Writable, Readable } = require('streamx')
6
+ const os = require('bare-os')
7
7
  const { Writable: BareWritable, Readable: BareReadable } = require('bare-stream')
8
- const { once } = require('bare-events')
8
+ const { Writable, Readable } = require('streamx')
9
9
  const hypercoreid = require('hypercore-id-encoding')
10
10
  const byteSize = require('tiny-byte-size')
11
11
  const { isWindows } = require('which-runtime')
12
12
  const { CHECKOUT } = require('pear-constants')
13
13
  const gracedown = require('pear-gracedown')
14
+ const errors = require('pear-errors')
14
15
  const opwait = require('pear-opwait')
15
16
  const isTTY = tty.isTTY(0)
16
17
 
18
+ function ERR_SIGINT(msg) {
19
+ return new errors(msg, ERR_SIGINT)
20
+ }
21
+
17
22
  const pt = (arg) => arg
18
23
  const es = () => ''
19
24
  const ansi = isWindows
20
- ? { 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 }
25
+ ? {
26
+ bold: pt,
27
+ dim: pt,
28
+ italic: pt,
29
+ underline: pt,
30
+ inverse: pt,
31
+ red: pt,
32
+ green: pt,
33
+ yellow: pt,
34
+ gray: pt,
35
+ upHome: es,
36
+ link: pt,
37
+ hideCursor: es,
38
+ showCursor: es
39
+ }
21
40
  : {
22
41
  bold: (s) => `\x1B[1m${s}\x1B[22m`,
23
42
  dim: (s) => `\x1B[2m${s}\x1B[22m`,
@@ -39,30 +58,36 @@ ansi.tick = isWindows ? '^' : ansi.green('✔')
39
58
  ansi.cross = isWindows ? 'x' : ansi.red('✖')
40
59
  ansi.warning = isWindows ? '!' : '⚠️'
41
60
  ansi.pear = isWindows ? '*' : '🍐'
42
- ansi.dot = isWindows ? '' : 'o'
61
+ ansi.dot = isWindows ? 'o' : ''
43
62
  ansi.key = isWindows ? '>' : '🔑'
44
63
  ansi.down = isWindows ? '↓' : '⬇'
45
64
  ansi.up = isWindows ? '↑' : '⬆'
46
65
 
47
- const stdio = new class Stdio {
66
+ const stdio = new (class Stdio {
48
67
  static WriteStream = class FdWriteStream extends BareWritable {
49
- constructor (fd) {
50
- super({ map: (data) => typeof data === 'string' ? Buffer.from(data) : data })
68
+ constructor(fd) {
69
+ super({
70
+ map: (data) => (typeof data === 'string' ? Buffer.from(data) : data)
71
+ })
51
72
  this.fd = fd
52
73
  }
53
74
 
54
- _writev (batch, cb) {
55
- fs.writev(this.fd, batch.map(({ chunk }) => chunk), cb)
75
+ _writev(batch, cb) {
76
+ fs.writev(
77
+ this.fd,
78
+ batch.map(({ chunk }) => chunk),
79
+ cb
80
+ )
56
81
  }
57
82
  }
58
83
 
59
84
  static ReadStream = class FdReadStream extends BareReadable {
60
- constructor (fd) {
85
+ constructor(fd) {
61
86
  super()
62
87
  this.fd = fd
63
88
  }
64
89
 
65
- _read (size) {
90
+ _read(size) {
66
91
  const buffer = Buffer.alloc(size)
67
92
  fs.read(this.fd, buffer, 0, size, null, (err, bytesRead) => {
68
93
  if (err) return this.destroy(err)
@@ -73,57 +98,71 @@ const stdio = new class Stdio {
73
98
  }
74
99
 
75
100
  drained = Writable.drained
76
- constructor () {
101
+ constructor() {
77
102
  this._in = null
78
103
  this._out = null
79
104
  this._err = null
80
105
  this.rawMode = false
81
106
  }
82
107
 
83
- get inAttached () { return this._in !== null }
108
+ get inAttached() {
109
+ return this._in !== null
110
+ }
84
111
 
85
- get in () {
112
+ get in() {
86
113
  if (this._in === null) {
87
114
  this._in = tty.isTTY(0) ? new tty.ReadStream(0) : new this.constructor.ReadStream(0)
88
- this._in.once('close', () => { this._in = null })
115
+ this._in.once('close', () => {
116
+ this._in = null
117
+ })
89
118
  }
90
119
  return this._in
91
120
  }
92
121
 
93
- get out () {
94
- if (this._out === null) this._out = tty.isTTY(1) ? new tty.WriteStream(1) : new this.constructor.WriteStream(1)
122
+ get out() {
123
+ if (this._out === null) {
124
+ this._out = tty.isTTY(1) ? new tty.WriteStream(1) : new this.constructor.WriteStream(1)
125
+ }
95
126
  return this._out
96
127
  }
97
128
 
98
- get err () {
99
- if (this._err === null) this._err = tty.isTTY(2) ? new tty.WriteStream(2) : new this.constructor.WriteStream(2)
129
+ get err() {
130
+ if (this._err === null) {
131
+ this._err = tty.isTTY(2) ? new tty.WriteStream(2) : new this.constructor.WriteStream(2)
132
+ }
100
133
  return this._err
101
134
  }
102
135
 
103
- size () {
136
+ size() {
104
137
  if (!this.out.getWindowSize) return [80, 80]
105
138
  const [width, height] = this.out.getWindowSize()
106
139
  return { width, height }
107
140
  }
108
141
 
109
- raw (rawMode) {
142
+ raw(rawMode) {
110
143
  this.rawMode = !!rawMode
111
- return this.in.setMode?.(this.rawMode ? this.tty.constants.MODE_RAW : this.tty.constants.MODE_NORMAL)
144
+ return this.in.setMode?.(
145
+ this.rawMode ? this.tty.constants.MODE_RAW : this.tty.constants.MODE_NORMAL
146
+ )
112
147
  }
113
- }()
148
+ })()
114
149
 
115
150
  class Interact {
116
- 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
117
- constructor (header, params, opts = {}) {
151
+ static rx =
152
+ /[\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
153
+ constructor(header, params, opts = {}) {
118
154
  this._header = header
119
155
  this._params = params
120
156
  this._defaults = opts.defaults || {}
121
157
 
122
158
  const mask = (data, cb) => {
123
- if (data.length > 4) { // is full line
159
+ if (data.length > 4) {
160
+ // is full line
124
161
  const prompt = this._rl._prompt
125
162
  const regex = new RegExp(`(${prompt})([\\x20-\\x7E]+)`, 'g') // match printable chars after prompt
126
- const masked = data.toString().replace(regex, (_, prompt, pwd) => prompt + '*'.repeat(pwd.length))
163
+ const masked = data
164
+ .toString()
165
+ .replace(regex, (_, prompt, pwd) => prompt + '*'.repeat(pwd.length))
127
166
  stdio.out.write(masked)
128
167
  } else {
129
168
  stdio.out.write(data)
@@ -135,15 +174,13 @@ class Interact {
135
174
  input: stdio.in,
136
175
  output: opts.masked ? new Writable({ write: mask }) : stdio.out
137
176
  })
138
-
139
- this._rl.input?.setMode?.(tty.constants.MODE_RAW)
140
177
  this._rl.on('close', () => {
141
- console.log() // new line
142
- Bare.exit()
178
+ console.log() // newline
143
179
  })
180
+ stdio.in?.setMode?.(tty.constants.MODE_RAW)
144
181
  }
145
182
 
146
- async run (opts) {
183
+ async run(opts) {
147
184
  try {
148
185
  return await this.#run(opts)
149
186
  } finally {
@@ -151,7 +188,7 @@ class Interact {
151
188
  }
152
189
  }
153
190
 
154
- async #run (opts = {}) {
191
+ async #run(opts = {}) {
155
192
  if (opts.autosubmit) return this.#autosubmit()
156
193
  stdio.out.write(this._header)
157
194
  const fields = {}
@@ -161,13 +198,16 @@ class Interact {
161
198
  const param = this._params.shift()
162
199
  while (true) {
163
200
  const deflt = defaults[param.name] ?? param.default
164
- let answer = await this.#input(`${param.prompt}${param.delim || ':'}${deflt && ' (' + deflt + ')'} `)
165
-
201
+ let answer = await this.#input(
202
+ `${param.prompt}${param.delim || ':'}${deflt && ' (' + deflt + ')'} `
203
+ )
166
204
  if (answer.length === 0) answer = defaults[param.name] ?? deflt
167
- if (!param.validation || await param.validation(answer)) {
205
+ if (!param.validation || (await param.validation(answer))) {
168
206
  if (typeof answer === 'string') answer = answer.replace(this.constructor.rx, '')
169
207
  fields[param.name] = answer
170
- if (Array.isArray(param.shave) && param.shave.every((ix) => typeof ix === 'number')) shave[param.name] = param.shave
208
+ if (Array.isArray(param.shave) && param.shave.every((ix) => typeof ix === 'number')) {
209
+ shave[param.name] = param.shave
210
+ }
171
211
  break
172
212
  } else {
173
213
  stdio.out.write(param.msg + '\n')
@@ -177,112 +217,144 @@ class Interact {
177
217
  return { fields, shave }
178
218
  }
179
219
 
180
- #autosubmit () {
220
+ #autosubmit() {
181
221
  const fields = {}
182
222
  const shave = {}
183
223
  const defaults = this._defaults
184
224
  while (this._params.length) {
185
225
  const param = this._params.shift()
186
226
  fields[param.name] = defaults[param.name] ?? param.default
187
- if (Array.isArray(param.shave) && param.shave.every((ix) => typeof ix === 'number')) shave[param.name] = param.shave
227
+ if (Array.isArray(param.shave) && param.shave.every((ix) => typeof ix === 'number')) {
228
+ shave[param.name] = param.shave
229
+ }
188
230
  }
189
231
  return { fields, shave }
190
232
  }
191
233
 
192
- async #input (prompt) {
234
+ async #input(prompt) {
193
235
  stdio.out.write(prompt)
194
236
  this._rl._prompt = prompt
195
- const answer = (await once(this._rl, 'data')).toString()
196
- return answer.trim() // remove return char
237
+ const answer = await new Promise((resolve, reject) => {
238
+ this._rl.once('data', (data) => {
239
+ resolve(data)
240
+ })
241
+ stdio.in?.once('data', (data) => {
242
+ if (data.length === 1 && data[0] === 3) {
243
+ reject(ERR_SIGINT('^C exit'))
244
+ os.kill(Pear.pid, 'SIGINT')
245
+ }
246
+ })
247
+ })
248
+ return answer.toString().trim() // remove return char
197
249
  }
198
250
  }
199
251
 
200
252
  let statusFrag = ''
201
253
 
202
- function status (msg, success) {
254
+ function status(msg, success) {
203
255
  msg = msg || ''
204
256
  const done = typeof success === 'boolean'
205
257
  if (msg) stdio.out.write(`\x1B[2K\r${indicator(success)}${msg}\n${done ? '' : ansi.upHome()}`)
206
258
  statusFrag = msg.slice(0, 3)
207
259
  }
208
260
 
209
- function print (message, success) {
261
+ function print(message, success) {
210
262
  statusFrag = ''
211
263
  console.log(`${typeof success !== 'undefined' ? indicator(success) : ''}${message}`)
212
264
  }
213
265
 
214
- function byteDiff ({ type, sizes, message }) {
266
+ function byteDiff({ type, sizes, message }) {
215
267
  statusFrag = ''
216
268
  sizes = sizes.map((size) => (size > 0 ? '+' : '') + byteSize(size)).join(', ')
217
269
  print(indicator(type, 'diff') + ' ' + message + ' (' + sizes + ')')
218
270
  }
219
271
 
220
- function indicator (value, type = 'success') {
272
+ function indicator(value, type = 'success') {
221
273
  if (value === undefined) return ''
222
274
  if (value === true) value = 1
223
275
  else if (value === false) value = -1
224
- else if (value == null) value = 0
225
- if (type === 'diff') return value === 0 ? ansi.yellow('~') : (value === 1 ? ansi.green('+') : ansi.red('-'))
226
- return value < 0 ? ansi.cross + ' ' : (value > 0 ? ansi.tick + ' ' : ansi.gray('- '))
276
+ else if (value === null) value = 0
277
+ if (type === 'diff') {
278
+ return value === 0 ? ansi.yellow('~') : value === 1 ? ansi.green('+') : ansi.red('-')
279
+ }
280
+ return value < 0 ? ansi.cross + ' ' : value > 0 ? ansi.tick + ' ' : ansi.gray('- ')
227
281
  }
228
282
 
229
- const outputter = (cmd, taggers = {}) => (opts, stream, info = {}, ipc) => {
230
- if (Array.isArray(stream)) stream = Readable.from(stream)
231
- const asTTY = opts.ctrlTTY ?? isTTY
232
- if (asTTY) stdio.out.write(ansi.hideCursor())
233
- const dereg = asTTY
234
- ? gracedown(() => {
235
- if (!isWindows) stdio.out.write('\x1B[1K\x1B[G' + statusFrag) // clear ^C
236
- stdio.out.write(ansi.showCursor())
237
- })
238
- : null
239
- if (typeof opts === 'boolean') opts = { json: opts }
240
- const { json = false, log } = opts
241
- const promise = opwait(stream, ({ tag, data }) => {
242
- if (json) {
243
- const str = JSON.stringify({ cmd, tag, data })
244
- if (log) log(str)
245
- else print(str)
246
- return
247
- }
248
-
249
- const transform = Promise.resolve(typeof taggers[tag] === 'function' ? taggers[tag](data, info, ipc) : taggers[tag] || false)
250
- transform.then((result) => {
251
- if (result === undefined) return
252
- if (typeof result === 'string') result = { output: 'print', message: result }
253
- if (result === false) {
254
- if (tag === 'final') result = { output: 'print', message: (data.message ?? data.success ? 'Success' : 'Failure') }
255
- else result = {}
256
- }
257
- result.success = result.success ?? data?.success
258
- const { output, message, success = data?.success } = result
259
- if (log) {
260
- const logOpts = { output, ...(typeof success === 'boolean' ? { success } : {}) }
261
- if (Array.isArray(message) === false) log(message, logOpts)
262
- else for (const msg of message) log(msg, logOpts)
283
+ const outputter =
284
+ (cmd, taggers = {}) =>
285
+ (opts, stream, info = {}, ipc) => {
286
+ if (Array.isArray(stream)) stream = Readable.from(stream)
287
+ const asTTY = opts.ctrlTTY ?? isTTY
288
+ if (asTTY) stdio.out.write(ansi.hideCursor())
289
+ const dereg = asTTY
290
+ ? gracedown(() => {
291
+ if (!isWindows) stdio.out.write('\x1B[1K\x1B[G' + statusFrag) // clear ^C
292
+ stdio.out.write(ansi.showCursor())
293
+ })
294
+ : null
295
+ if (typeof opts === 'boolean') opts = { json: opts }
296
+ const { json = false, log } = opts
297
+ const promise = opwait(stream, ({ tag, data }) => {
298
+ if (json) {
299
+ const str = JSON.stringify({ cmd, tag, data })
300
+ if (log) log(str)
301
+ else print(str)
263
302
  return
264
303
  }
265
- let msg = Array.isArray(message) ? message.join('\n') : message
266
- if (tag === 'final') msg += '\n'
267
- if (output === 'print') print(msg, success)
268
- if (output === 'status') status(msg, success)
269
- }, (err) => stream.destroy(err))
270
- })
271
-
272
- return !asTTY
273
- ? promise
274
- : promise.finally(() => {
275
- stdio.out.write(ansi.showCursor())
276
- dereg(false)
304
+
305
+ const transform = Promise.resolve(
306
+ typeof taggers[tag] === 'function' ? taggers[tag](data, info, ipc) : taggers[tag] || false
307
+ )
308
+ transform.then(
309
+ (result) => {
310
+ if (result === undefined) return
311
+ if (typeof result === 'string') result = { output: 'print', message: result }
312
+ if (result === false) {
313
+ if (tag === 'final') {
314
+ result = {
315
+ output: 'print',
316
+ message: (data.message ?? data.success) ? 'Success' : 'Failure'
317
+ }
318
+ } else result = {}
319
+ }
320
+ result.success = result.success ?? data?.success
321
+ const { output, message, success = data?.success } = result
322
+ if (log) {
323
+ const logOpts = { output, ...(typeof success === 'boolean' ? { success } : {}) }
324
+ if (Array.isArray(message) === false) log(message, logOpts)
325
+ else for (const msg of message) log(msg, logOpts)
326
+ return
327
+ }
328
+ let msg = Array.isArray(message) ? message.join('\n') : message
329
+ if (tag === 'final') {
330
+ msg += '\n'
331
+ if (asTTY) {
332
+ stdio.out.write(ansi.showCursor())
333
+ dereg(false)
334
+ }
335
+ }
336
+
337
+ if (output === 'print') print(msg, success)
338
+ else if (output === 'status') status(msg, success)
339
+ },
340
+ (err) => stream.destroy(err)
341
+ )
277
342
  })
278
- }
343
+
344
+ return promise
345
+ }
279
346
 
280
347
  const banner = `${ansi.bold('Pear')} ~ ${ansi.dim('Welcome to the Internet of Peers')}`
281
348
  const version = `${CHECKOUT.fork || 0}.${CHECKOUT.length || 'dev'}.${CHECKOUT.key}`
282
349
  const header = ` ${banner}
283
350
  ${ansi.pear + ' '}${ansi.bold(ansi.gray('v' + version))}
284
351
  `
285
- const urls = ansi.link('https://pears.com', 'pears.com') + ' | ' + ansi.link('https://holepunch.to', 'holepunch.to') + ' | ' + ansi.link('https://keet.io', 'keet.io')
352
+ const urls =
353
+ ansi.link('https://pears.com', 'pears.com') +
354
+ ' | ' +
355
+ ansi.link('https://holepunch.to', 'holepunch.to') +
356
+ ' | ' +
357
+ ansi.link('https://keet.io', 'keet.io')
286
358
 
287
359
  const footer = {
288
360
  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
@@ -294,11 +366,13 @@ ${urls}\n${ansi.bold(ansi.dim('Pear'))} ~ ${ansi.dim('Welcome to the IoP')}
294
366
 
295
367
  const usage = { header, version, banner, footer }
296
368
 
297
- async function trust (ipc, key, cmd) {
369
+ async function trust(ipc, key, cmd) {
298
370
  const explain = {
299
- run: 'Be sure that software is trusted before running it\n' +
371
+ run:
372
+ 'Be sure that software is trusted before running it\n' +
300
373
  '\nType "TRUST" to allow execution or anything else to exit\n\n',
301
- init: 'This template is not trusted.\n' +
374
+ init:
375
+ 'This template is not trusted.\n' +
302
376
  '\nType "TRUST" to trust this template, or anything else to exit\n\n'
303
377
  }
304
378
 
@@ -337,22 +411,20 @@ async function trust (ipc, key, cmd) {
337
411
  Bare.exit()
338
412
  }
339
413
 
340
- async function password (ipc, key, cmd) {
414
+ async function password(ipc, key, cmd) {
341
415
  const z32 = hypercoreid.normalize(key)
342
416
 
343
417
  const explain = {
344
- run: 'pear://' + z32 + ' is an encrypted application. \n' +
418
+ run:
419
+ 'pear://' +
420
+ z32 +
421
+ ' is an encrypted application. \n' +
345
422
  '\nEnter the password to run the app.\n\n',
346
- stage: 'This application is encrypted.\n' +
347
- '\nEnter the password to stage the app.\n\n',
348
- seed: 'This application is encrypted.\n' +
349
- '\nEnter the password to seed the app.\n\n',
350
- dump: 'This application is encrypted.\n' +
351
- '\nEnter the password to dump the app.\n\n',
352
- init: 'This template is encrypted.\n' +
353
- '\nEnter the password to init from the template.\n\n',
354
- info: 'This application is encrypted.\n' +
355
- '\nEnter the password to retrieve info.\n\n'
423
+ stage: 'This application is encrypted.\n' + '\nEnter the password to stage the app.\n\n',
424
+ seed: 'This application is encrypted.\n' + '\nEnter the password to seed the app.\n\n',
425
+ dump: 'This application is encrypted.\n' + '\nEnter the password to dump the app.\n\n',
426
+ init: 'This template is encrypted.\n' + '\nEnter the password to init from the template.\n\n',
427
+ info: 'This application is encrypted.\n' + '\nEnter the password to retrieve info.\n\n'
356
428
  }
357
429
 
358
430
  const message = {
@@ -369,16 +441,20 @@ async function password (ipc, key, cmd) {
369
441
  const delim = ':'
370
442
  const validation = (key) => key.length > 0
371
443
  const msg = '\nPlease, enter a valid password.\n'
372
- const interact = new Interact(dialog, [
373
- {
374
- name: 'value',
375
- default: '',
376
- prompt: ask,
377
- delim,
378
- validation,
379
- msg
380
- }
381
- ], { masked: true })
444
+ const interact = new Interact(
445
+ dialog,
446
+ [
447
+ {
448
+ name: 'value',
449
+ default: '',
450
+ prompt: ask,
451
+ delim,
452
+ validation,
453
+ msg
454
+ }
455
+ ],
456
+ { masked: true }
457
+ )
382
458
  const { fields } = await interact.run()
383
459
  print(`\n${ansi.key} Hashing password...`)
384
460
  await ipc.permit({ key, password: fields.value })
@@ -387,7 +463,7 @@ async function password (ipc, key, cmd) {
387
463
  Bare.exit()
388
464
  }
389
465
 
390
- function permit (ipc, info, cmd) {
466
+ function permit(ipc, info, cmd) {
391
467
  const key = info.key
392
468
  if (info.encrypted) {
393
469
  return password(ipc, key, cmd)
@@ -396,7 +472,7 @@ function permit (ipc, info, cmd) {
396
472
  }
397
473
  }
398
474
 
399
- async function confirm (dialog, ask, delim, validation, msg) {
475
+ async function confirm(dialog, ask, delim, validation, msg) {
400
476
  const interact = new Interact(dialog, [
401
477
  {
402
478
  name: 'value',
@@ -410,4 +486,62 @@ async function confirm (dialog, ask, delim, validation, msg) {
410
486
  await interact.run()
411
487
  }
412
488
 
413
- module.exports = { usage, permit, stdio, ansi, indicator, status, print, outputter, isTTY, confirm, byteSize, byteDiff, Interact }
489
+ function explain(bail = {}) {
490
+ if (!bail.reason && bail.err) {
491
+ const known = errors.known()
492
+ if (known.includes(bail.err.code) === false) {
493
+ print(
494
+ errors.ERR_UNKNOWN(
495
+ 'Unknown [ code: ' + (bail.err.code || '(none)') + ' ] ' + bail.err.stack
496
+ ),
497
+ false
498
+ )
499
+ Bare.exit(1)
500
+ }
501
+ }
502
+ const messageUsage = (bail) => bail.err.message
503
+ const messageOnly = (bail) => bail.err.message
504
+ const opFail = (cmd) => cmd.err.info.message
505
+ const codemap = new Map([
506
+ ['UNKNOWN_FLAG', (bail) => 'Unrecognized Flag: --' + bail.flag.name],
507
+ [
508
+ 'UNKNOWN_ARG',
509
+ (bail) => 'Unrecognized Argument at index ' + bail.arg.index + ' with value ' + bail.arg.value
510
+ ],
511
+ ['MISSING_ARG', (bail) => bail.arg.value],
512
+ ['INVALID', messageUsage],
513
+ ['ERR_INVALID_INPUT', messageUsage],
514
+ ['ERR_LEGACY', messageOnly],
515
+ ['ERR_INVALID_TEMPLATE', messageOnly],
516
+ ['ERR_DIR_NONEMPTY', messageOnly],
517
+ ['ERR_OPERATION_FAILED', opFail]
518
+ ])
519
+ const nouse = [messageOnly, opFail]
520
+ const code = codemap.has(bail.err?.code) ? bail.err.code : bail.reason
521
+ const ref = codemap.get(code)
522
+ const reason = codemap.has(code) ? (codemap.get(code)(bail) ?? bail.reason) : bail.reason
523
+ Bare.exitCode = 1
524
+
525
+ print(reason, false)
526
+
527
+ if (nouse.some((fn) => fn === ref) || codemap.has(code) === false) return
528
+
529
+ print('\n' + bail.command.usage())
530
+ }
531
+
532
+ module.exports = {
533
+ explain,
534
+ usage,
535
+ permit,
536
+ stdio,
537
+ ansi,
538
+ indicator,
539
+ status,
540
+ print,
541
+ outputter,
542
+ isTTY,
543
+ confirm,
544
+ byteSize,
545
+ byteDiff,
546
+ Interact
547
+ }
package/package.json CHANGED
@@ -1,22 +1,33 @@
1
1
  {
2
2
  "name": "pear-terminal",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "main": "index.js",
5
5
  "type": "commonjs",
6
6
  "description": "Pear Terminal User Interface library",
7
7
  "license": "Apache-2.0",
8
8
  "files": [],
9
+ "imports": {
10
+ "process": {
11
+ "bare": "bare-process",
12
+ "default": "process"
13
+ }
14
+ },
9
15
  "scripts": {
16
+ "format": "prettier --write .",
17
+ "lint": "prettier --check . && lunte",
10
18
  "test:gen": "brittle -r test/all.js test/*.test.js",
11
- "test": "brittle-bare --coverage test/all.js",
12
- "lint": "standard"
19
+ "test": "brittle-bare test/all.js",
20
+ "test:bare": "brittle-bare test/all.js"
13
21
  },
14
22
  "devDependencies": {
15
23
  "bare-module": "^5.0.2",
16
- "bare-os": "^3.6.1",
24
+ "bare-os": "^3.6.2",
25
+ "bare-process": "^4.2.2",
17
26
  "brittle": "^3.0.0",
27
+ "lunte": "^1.0.0",
18
28
  "pear-ipc": "^5.5.0",
19
- "standard": "^17.1.2"
29
+ "prettier": "^3.6.2",
30
+ "prettier-config-holepunch": "^2.0.0"
20
31
  },
21
32
  "dependencies": {
22
33
  "bare-events": "^2.6.1",
@@ -26,6 +37,7 @@
26
37
  "bare-tty": "^5.0.2",
27
38
  "hypercore-id-encoding": "^1.3.0",
28
39
  "pear-constants": "^1.0.0",
40
+ "pear-errors": "^1.0.1",
29
41
  "pear-gracedown": "^1.0.0",
30
42
  "pear-opwait": "^1.0.0",
31
43
  "streamx": "^2.22.1",