microlink.io 0.2.0 → 0.5.1

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.
package/bin/config.js ADDED
@@ -0,0 +1,61 @@
1
+ 'use strict'
2
+
3
+ const { homedir } = require('os')
4
+ const { join } = require('path')
5
+ const {
6
+ mkdirSync,
7
+ readFileSync,
8
+ writeFileSync,
9
+ unlinkSync,
10
+ chmodSync
11
+ } = require('fs')
12
+
13
+ const configDir = () =>
14
+ join(process.env.XDG_CONFIG_HOME || join(homedir(), '.config'), 'microlink')
15
+
16
+ const configPath = () => join(configDir(), 'config.json')
17
+
18
+ const configPathDisplay = () => {
19
+ const file = configPath()
20
+ const home = homedir()
21
+ return file.startsWith(home + '/') ? `~${file.slice(home.length)}` : file
22
+ }
23
+
24
+ const readConfig = () => {
25
+ try {
26
+ return JSON.parse(readFileSync(configPath(), 'utf8'))
27
+ } catch {
28
+ return {}
29
+ }
30
+ }
31
+
32
+ const writeConfig = data => {
33
+ mkdirSync(configDir(), { recursive: true, mode: 0o700 })
34
+ writeFileSync(configPath(), JSON.stringify(data) + '\n', { mode: 0o600 })
35
+ chmodSync(configPath(), 0o600)
36
+ }
37
+
38
+ const clearConfig = () => {
39
+ try {
40
+ unlinkSync(configPath())
41
+ return true
42
+ } catch (error) {
43
+ if (error.code === 'ENOENT') return false
44
+ throw error
45
+ }
46
+ }
47
+
48
+ const readApiKey = () => {
49
+ const { apiKey } = readConfig()
50
+ return typeof apiKey === 'string' && apiKey ? apiKey : undefined
51
+ }
52
+
53
+ module.exports = {
54
+ configDir,
55
+ configPath,
56
+ configPathDisplay,
57
+ readConfig,
58
+ writeConfig,
59
+ clearConfig,
60
+ readApiKey
61
+ }
package/bin/help.js CHANGED
@@ -12,7 +12,10 @@ const cmd = (rest, comment) =>
12
12
  const rows = items => items.map(([name, desc]) => col(name, desc)).join('\n')
13
13
 
14
14
  const CLI = [
15
- ['--api-key', 'Microlink API key (defaults to MICROLINK_API_KEY env)'],
15
+ [
16
+ '--api-key',
17
+ 'Microlink API key (flag, MICROLINK_API_KEY, or `microlink login`)'
18
+ ],
16
19
  ['--endpoint', 'Microlink API endpoint'],
17
20
  ['--header, -H', "Extra request header as 'Name: value' (repeatable)"],
18
21
  [
@@ -71,6 +74,23 @@ const COLLECTION = [
71
74
 
72
75
  const ALIAS = { run: 'function' }
73
76
 
77
+ const COMMANDS = {
78
+ login: {
79
+ usage: 'login',
80
+ desc: 'Save an API key from your Microlink account',
81
+ flags: [],
82
+ cli: [],
83
+ examples: [['login', 'open the dashboard and save an API key']]
84
+ },
85
+ logout: {
86
+ usage: 'logout',
87
+ desc: 'Remove the saved API key from this machine',
88
+ flags: [],
89
+ cli: [],
90
+ examples: [['logout', 'forget the saved API key']]
91
+ }
92
+ }
93
+
74
94
  const content = (name, desc) => ({
75
95
  usage: `${name} <url> [options]`,
76
96
  desc,
@@ -222,6 +242,9 @@ const PRODUCTS = {
222
242
  'news, images, videos, places, maps, shopping, scholar, patents, autocomplete'
223
243
  ],
224
244
  ['--limit', 'Maximum number of results'],
245
+ ['--page', 'Results page (1 default)'],
246
+ ['--html', 'Fetch HTML for the results page and each result'],
247
+ ['--markdown', 'Fetch Markdown for the results page and each result'],
225
248
  ['--location', 'Country or locale (e.g. es)'],
226
249
  ['--period', 'Recency: hour, day, week, month, year'],
227
250
  ['--timeout', 'Request timeout']
@@ -235,7 +258,12 @@ const PRODUCTS = {
235
258
  [
236
259
  'search "open source llm" --type news --period week',
237
260
  'news results from the past week'
238
- ]
261
+ ],
262
+ [
263
+ 'search "the matrix" --markdown',
264
+ 'include Markdown for the SERP and each result'
265
+ ],
266
+ ['search "the matrix" --page 2', 'second page of results']
239
267
  ]
240
268
  },
241
269
  function: {
@@ -258,9 +286,18 @@ const productList = Object.entries(PRODUCTS)
258
286
  .map(([name, product]) => col(name, product.desc))
259
287
  .join('\n')
260
288
 
289
+ const commandList = Object.entries(COMMANDS)
290
+ .map(([name, command]) => col(name, command.desc))
291
+ .join('\n')
292
+
261
293
  const global = `Usage
262
294
  ${cmd('<url> [options]')}
263
295
  ${cmd('<product> <url|query> [options]')}
296
+ ${cmd('login')}
297
+ ${cmd('logout')}
298
+
299
+ Commands
300
+ ${commandList}
264
301
 
265
302
  Products
266
303
  ${productList}
@@ -269,6 +306,7 @@ Options
269
306
  ${rows(CLI)}
270
307
 
271
308
  Examples
309
+ ${cmd('login', 'save an API key from your account')}
272
310
  ${cmd('https://example.com', 'unified metadata (default)')}
273
311
  ${cmd(
274
312
  'https://example.com --trace',
@@ -307,15 +345,8 @@ const render = (name, product) => {
307
345
  .join('\n')
308
346
  const cli = product.cli ?? CLI
309
347
  const options = [...product.flags, ...cli]
310
- const parts = [
311
- 'Usage',
312
- usage,
313
- '',
314
- gray(product.desc),
315
- '',
316
- 'Options',
317
- rows(options)
318
- ]
348
+ const parts = ['Usage', usage, '', gray(product.desc)]
349
+ if (options.length > 0) parts.push('', 'Options', rows(options))
319
350
  if (product.browser) parts.push('', 'Browser', rows(BROWSER))
320
351
  if (product.note) parts.push('', gray(product.note))
321
352
  if (product.examples) {
@@ -330,5 +361,6 @@ const render = (name, product) => {
330
361
 
331
362
  module.exports = command => {
332
363
  const name = ALIAS[command] ?? command
364
+ if (COMMANDS[name]) return render(name, COMMANDS[name])
333
365
  return PRODUCTS[name] ? render(name, PRODUCTS[name]) : global
334
366
  }
package/bin/index.js CHANGED
@@ -5,7 +5,9 @@ const { readFileSync } = require('fs')
5
5
  const path = require('path')
6
6
  const mri = require('mri')
7
7
  const helpText = require('./help')
8
- const { gray, white, green, red, styleText } = require('./style')
8
+ const { readApiKey, clearConfig } = require('./config')
9
+ const login = require('./login')
10
+ const { gray, white, green, red, orange, link, styleText } = require('./style')
9
11
 
10
12
  const create = require('../src')
11
13
 
@@ -15,7 +17,7 @@ const CLEAR_LINE = '\r\u001b[K'
15
17
  const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
16
18
 
17
19
  const label = (text, color) =>
18
- styleText(['inverse', 'bold', color], ` ${text.toUpperCase()} `)
20
+ styleText(['inverse', 'bold'], color(` ${text.toUpperCase()} `))
19
21
  const keyValue = (key, value) => key + ' ' + gray(value)
20
22
 
21
23
  const prettyMs = ms => {
@@ -63,7 +65,9 @@ const printPretty = (value, indent = 0) => {
63
65
  if (typeof value !== 'object') return white(String(value))
64
66
 
65
67
  const isArray = Array.isArray(value)
66
- const keys = isArray ? value : Object.keys(value)
68
+ const keys = isArray
69
+ ? value.filter(item => typeof item !== 'function')
70
+ : Object.keys(value).filter(key => typeof value[key] !== 'function')
67
71
  if (keys.length === 0) return gray(isArray ? '[]' : '{}')
68
72
 
69
73
  const pad = ' '.repeat(indent)
@@ -167,7 +171,7 @@ const printFooter = ({ duration, response }) => {
167
171
 
168
172
  if (process.stdout.isTTY) console.error()
169
173
  console.error(
170
- label('success', 'green'),
174
+ label('success', green),
171
175
  gray(`${prettyBytes(size)} in ${time}`)
172
176
  )
173
177
  console.error()
@@ -187,30 +191,53 @@ const printFooter = ({ duration, response }) => {
187
191
  keyValue(green('mode'), `${fetchMode} ${gray(fetchTime)}`.trim())
188
192
  )
189
193
  }
190
- if (uri) console.error(' ', keyValue(green('uri'), uri))
194
+ if (uri) console.error(' ', keyValue(green('uri'), link(uri)))
191
195
  if (id) console.error(' ', keyValue(green('id'), id))
192
196
  }
193
197
 
198
+ const isClientError = statusCode => statusCode >= 400 && statusCode < 500
199
+
200
+ /**
201
+ * The API reports the actionable reason per field under `data`, keeping
202
+ * `message` as a generic pointer to it.
203
+ */
204
+ const reasons = error => {
205
+ const values = Object.values(error.data ?? {}).filter(
206
+ value => typeof value === 'string'
207
+ )
208
+ return values.length > 0
209
+ ? values
210
+ : [String(error.message).replace(`${error.code}, `, '')]
211
+ }
212
+
194
213
  const printFail = error => {
214
+ const color = isClientError(error.statusCode) ? orange : red
215
+ const status = error.status || 'fail'
216
+ const [reason, ...rest] = reasons(error)
217
+ const indent = ' '.repeat(status.length + 2)
195
218
  if (process.stdout.isTTY) console.error()
196
- console.error(
197
- label(error.status || 'fail', 'red'),
198
- gray(String(error.message).replace(`${error.code}, `, ''))
199
- )
219
+ console.error(label(status, color), gray(reason))
220
+ for (const extra of rest) console.error(indent, gray(extra))
200
221
  console.error()
201
222
  const id = error.headers?.['x-request-id']
202
- if (id) console.error(' ', keyValue(red('id'), id))
203
- if (error.url) console.error(' ', keyValue(red('uri'), error.url))
223
+ if (id) console.error(' ', keyValue(color('id'), id))
224
+ if (error.url) console.error(' ', keyValue(color('uri'), link(error.url)))
204
225
  if (error.code) {
205
226
  console.error(
206
227
  ' ',
207
228
  keyValue(
208
- red('code'),
229
+ color('code'),
209
230
  `${error.code}${error.statusCode ? ` (${error.statusCode})` : ''}`
210
231
  )
211
232
  )
212
233
  }
213
- if (error.more) console.error(' ', keyValue(red('more'), error.more))
234
+ if (error.more) console.error(' ', keyValue(color('more'), link(error.more)))
235
+ if (error.statusCode === 429) {
236
+ console.error(
237
+ ' ',
238
+ keyValue(color('hint'), 'run `microlink login` to use an API key')
239
+ )
240
+ }
214
241
  }
215
242
 
216
243
  const showHelp = command => {
@@ -218,6 +245,19 @@ const showHelp = command => {
218
245
  process.exit(0)
219
246
  }
220
247
 
248
+ const httpUrl = value => {
249
+ if (!URL.canParse(value)) return
250
+ const { protocol } = new URL(value)
251
+ if (protocol === 'http:' || protocol === 'https:') return value
252
+ }
253
+
254
+ const asUrl = input => {
255
+ if (typeof input !== 'string' || !input) return
256
+ if (httpUrl(input)) return input
257
+ if (/^[a-z][a-z\d+.-]*:\/\//i.test(input) || !/[.:]/.test(input)) return
258
+ return httpUrl(`https://${input}`)
259
+ }
260
+
221
261
  const HTTP_HEADER = 'http.header.'
222
262
 
223
263
  const parseHeaders = input => {
@@ -248,7 +288,7 @@ const takeHttpHeaders = flags => {
248
288
 
249
289
  const argv = mri(process.argv.slice(2), {
250
290
  alias: { H: 'header' },
251
- boolean: ['trace', 'trace-full', 'help'],
291
+ boolean: ['trace', 'trace-full', 'help', 'html', 'markdown'],
252
292
  string: ['header', 'api-key', 'data', 'file', 'endpoint']
253
293
  })
254
294
 
@@ -263,6 +303,8 @@ let {
263
303
  endpoint: endpointFlag,
264
304
  trace,
265
305
  'trace-full': traceFull,
306
+ html: htmlFlag,
307
+ markdown: markdownFlag,
266
308
  ...flags
267
309
  } = argv
268
310
 
@@ -270,69 +312,104 @@ const isTrace = trace || traceFull
270
312
 
271
313
  if (!command) showHelp()
272
314
 
273
- const apiKey = apiKeyFlag || apiKeyCamel || process.env.MICROLINK_API_KEY
274
- const endpoint = endpointFlag
275
- const client = create({
276
- ...(apiKey && { apiKey }),
277
- ...(endpoint && { endpoint })
278
- })
315
+ if (command === 'login' || command === 'logout') {
316
+ if (help) showHelp(command)
317
+ if (command === 'logout') {
318
+ console.error(clearConfig() ? 'Logged out.' : 'Already logged out.')
319
+ process.exit(0)
320
+ }
321
+ login().then(
322
+ () => process.exit(0),
323
+ error => {
324
+ console.error(error.message)
325
+ process.exit(error.code === 'ABORT' ? 130 : 1)
326
+ }
327
+ )
328
+ } else {
329
+ const apiKey =
330
+ apiKeyFlag || apiKeyCamel || process.env.MICROLINK_API_KEY || readApiKey()
331
+ const endpoint = endpointFlag
332
+ const client = create({
333
+ ...(apiKey && { apiKey }),
334
+ ...(endpoint && { endpoint })
335
+ })
279
336
 
280
- if (typeof client[command] !== 'function') {
281
- if (!target && URL.canParse(command)) {
282
- target = command
283
- command = 'metadata'
284
- } else if (help) {
285
- showHelp()
286
- } else {
287
- console.error(
288
- `Unknown command \`${command}\`. Run \`microlink --help\` to see the available commands.`
289
- )
290
- process.exit(1)
337
+ if (typeof client[command] !== 'function') {
338
+ const url = asUrl(command)
339
+ if (!target && url) {
340
+ target = url
341
+ command = 'metadata'
342
+ } else if (help) {
343
+ showHelp()
344
+ } else {
345
+ console.error(
346
+ `Unknown command \`${command}\`. Run \`microlink --help\` to see the available commands.`
347
+ )
348
+ process.exit(1)
349
+ }
291
350
  }
292
- }
293
351
 
294
- if (help || !target) showHelp(command)
352
+ if (help || !target) showHelp(command)
353
+ if (command !== 'search') target = asUrl(target) ?? target
295
354
 
296
- if (
297
- isTrace &&
298
- (command === 'search' || command === 'function' || command === 'run')
299
- ) {
300
- console.error(`\`--trace\` is not supported for \`${command}\`.`)
301
- process.exit(1)
302
- }
355
+ if (
356
+ isTrace &&
357
+ (command === 'search' || command === 'function' || command === 'run')
358
+ ) {
359
+ console.error(`\`--trace\` is not supported for \`${command}\`.`)
360
+ process.exit(1)
361
+ }
303
362
 
304
- const options = { ...flags }
305
- const headers = { ...takeHttpHeaders(options), ...parseHeaders(header) }
306
- if (Object.keys(headers).length > 0) options.headers = headers
363
+ const options = { ...flags }
364
+ const headers = { ...takeHttpHeaders(options), ...parseHeaders(header) }
365
+ if (Object.keys(headers).length > 0) options.headers = headers
307
366
 
308
- const invoke = () => {
367
+ let rules
309
368
  if (command === 'extract') {
310
- return client.extract(target, JSON.parse(data), options)
311
- }
312
- if (command === 'function' || command === 'run') {
313
- const code = readFileSync(path.resolve(file), 'utf8')
314
- return client.function(target, code, options)
369
+ try {
370
+ rules = JSON.parse(data)
371
+ } catch {
372
+ printFail({ message: 'Invalid --data JSON' })
373
+ process.exit(1)
374
+ }
315
375
  }
316
- return client[command](target, options)
317
- }
318
376
 
319
- const spin = !isTrace && shouldSpin() ? spinner() : null
320
-
321
- ;(async () => {
322
- spin?.start()
323
- const started = Date.now()
324
- try {
325
- const result = await invoke()
326
- const duration = Date.now() - started
327
- spin?.stop()
328
- if (isTrace) printJson(tracePayload({ ...client.last, full: traceFull }))
329
- else if (typeof result === 'string') console.log(result)
330
- else printJson({ status: 'success', data: result })
331
- if (!isTrace) printFooter({ duration, response: client.last.response })
332
- process.exit(0)
333
- } catch (error) {
334
- spin?.stop()
335
- printFail(error)
336
- process.exit(1)
377
+ const invoke = () => {
378
+ if (command === 'extract') {
379
+ return client.extract(target, rules, options)
380
+ }
381
+ if (command === 'function' || command === 'run') {
382
+ const code = readFileSync(path.resolve(file), 'utf8')
383
+ return client.function(target, code, options)
384
+ }
385
+ if (command === 'search') {
386
+ return client.search(target, {
387
+ ...options,
388
+ ...(htmlFlag && { html: true }),
389
+ ...(markdownFlag && { markdown: true })
390
+ })
391
+ }
392
+ return client[command](target, options)
337
393
  }
338
- })()
394
+
395
+ const spin = !isTrace && shouldSpin() ? spinner() : null
396
+
397
+ ;(async () => {
398
+ spin?.start()
399
+ const started = Date.now()
400
+ try {
401
+ const result = await invoke()
402
+ const duration = Date.now() - started
403
+ spin?.stop()
404
+ if (isTrace) printJson(tracePayload({ ...client.last, full: traceFull }))
405
+ else if (typeof result === 'string') console.log(result)
406
+ else printJson({ status: 'success', data: result })
407
+ if (!isTrace) printFooter({ duration, response: client.last.response })
408
+ process.exit(0)
409
+ } catch (error) {
410
+ spin?.stop()
411
+ printFail(error)
412
+ process.exit(1)
413
+ }
414
+ })()
415
+ }
package/bin/login.js ADDED
@@ -0,0 +1,148 @@
1
+ 'use strict'
2
+
3
+ const { randomBytes } = require('crypto')
4
+ const { spawn } = require('child_process')
5
+ const http = require('http')
6
+ const { writeConfig, readApiKey, configPathDisplay } = require('./config')
7
+ const select = require('./select')
8
+ const { gray } = require('./style')
9
+
10
+ const TIMEOUT_MS = 5 * 60 * 1000
11
+
12
+ const dashboardUrl = () =>
13
+ process.env.MICROLINK_DASHBOARD_URL || 'https://dashboard.microlink.io'
14
+
15
+ const openUrl = url => {
16
+ const { platform } = process
17
+ const child =
18
+ platform === 'win32'
19
+ ? spawn('cmd', ['/c', 'start', '""', `"${url}"`], {
20
+ detached: true,
21
+ stdio: 'ignore',
22
+ windowsVerbatimArguments: true
23
+ })
24
+ : spawn(platform === 'darwin' ? 'open' : 'xdg-open', [url], {
25
+ detached: true,
26
+ stdio: 'ignore'
27
+ })
28
+ child.on('error', () => {})
29
+ child.unref()
30
+ }
31
+
32
+ const listen = state =>
33
+ new Promise((resolve, reject) => {
34
+ let settle
35
+ const token = new Promise((resolve, reject) => {
36
+ settle = { resolve, reject }
37
+ })
38
+
39
+ const cors = res => {
40
+ res.setHeader('Access-Control-Allow-Origin', '*')
41
+ res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS')
42
+ res.setHeader('Access-Control-Allow-Headers', 'content-type')
43
+ res.setHeader('Access-Control-Allow-Private-Network', 'true')
44
+ }
45
+
46
+ const server = http.createServer((req, res) => {
47
+ cors(res)
48
+ if (req.method === 'OPTIONS') {
49
+ res.writeHead(204)
50
+ res.end()
51
+ return
52
+ }
53
+ if (req.method !== 'POST') {
54
+ res.writeHead(405)
55
+ res.end()
56
+ return
57
+ }
58
+ const chunks = []
59
+ req.on('data', chunk => chunks.push(chunk))
60
+ req.on('end', () => {
61
+ try {
62
+ const body = JSON.parse(Buffer.concat(chunks).toString())
63
+ if (body.state !== state || typeof body.token !== 'string') {
64
+ res.writeHead(400)
65
+ res.end()
66
+ return
67
+ }
68
+ res.writeHead(204)
69
+ res.end()
70
+ clearTimeout(timer)
71
+ settle.resolve(body.token)
72
+ } catch {
73
+ res.writeHead(400)
74
+ res.end()
75
+ }
76
+ })
77
+ })
78
+
79
+ const close = () => {
80
+ clearTimeout(timer)
81
+ server.close()
82
+ }
83
+
84
+ const timer = setTimeout(() => {
85
+ close()
86
+ settle.reject(new Error('Timed out waiting for dashboard authorization'))
87
+ }, TIMEOUT_MS)
88
+
89
+ server.listen(0, '127.0.0.1', () => {
90
+ resolve({ port: server.address().port, token, close })
91
+ })
92
+ server.on('error', reject)
93
+ })
94
+
95
+ const fetchKeys = async token => {
96
+ const res = await fetch(new URL('/api/v1/connect/keys', dashboardUrl()), {
97
+ headers: { authorization: `Bearer ${token}` }
98
+ })
99
+ if (!res.ok) {
100
+ throw new Error(`Could not load API keys (${res.status})`)
101
+ }
102
+ const body = await res.json()
103
+ return Array.isArray(body) ? body : body.keys
104
+ }
105
+
106
+ const asChoice = key => ({
107
+ name: key.label,
108
+ hint: key.maskedKey,
109
+ value: key.apiKey
110
+ })
111
+
112
+ const login = async () => {
113
+ const state = randomBytes(16).toString('hex')
114
+ const { port, token: tokenP, close } = await listen(state)
115
+ const url = new URL('/connect', dashboardUrl())
116
+ url.searchParams.set('port', String(port))
117
+ url.searchParams.set('state', state)
118
+ process.stderr.write(`Opening ${url}\n`)
119
+ openUrl(url.toString())
120
+
121
+ try {
122
+ const keys = await fetchKeys(await tokenP)
123
+ if (!keys?.length) {
124
+ throw new Error(
125
+ `No API keys on this account. Create a plan at ${dashboardUrl()}/plans`
126
+ )
127
+ }
128
+
129
+ const choices = keys.map(asChoice)
130
+ const picked =
131
+ choices.length === 1
132
+ ? choices[0]
133
+ : await select({
134
+ message: 'Which API key?',
135
+ choices,
136
+ current: readApiKey()
137
+ })
138
+
139
+ writeConfig({ apiKey: picked.value })
140
+ process.stderr.write(
141
+ `\n${gray('Saved')} ${picked.name} ${gray(`to ${configPathDisplay()}`)}\n`
142
+ )
143
+ } finally {
144
+ close()
145
+ }
146
+ }
147
+
148
+ module.exports = login
package/bin/select.js ADDED
@@ -0,0 +1,98 @@
1
+ 'use strict'
2
+
3
+ const readline = require('readline')
4
+ const { gray, white, styleText } = require('./style')
5
+
6
+ const HIDE = '\u001b[?25l'
7
+ const SHOW = '\u001b[?25h'
8
+ const UP_N = n => `\u001b[${n}A`
9
+
10
+ const label = (choice, current) => {
11
+ const mark = current && choice.value === current ? gray(' (current)') : ''
12
+ return `${choice.name} ${gray(`(${choice.hint})`)}${mark}`
13
+ }
14
+
15
+ const numbered = ({ message, choices, current }) =>
16
+ new Promise((resolve, reject) => {
17
+ for (const [i, choice] of choices.entries()) {
18
+ process.stderr.write(` ${i + 1}) ${label(choice, current)}\n`)
19
+ }
20
+ const rl = readline.createInterface({
21
+ input: process.stdin,
22
+ output: process.stderr
23
+ })
24
+ rl.question(`${message} `, answer => {
25
+ rl.close()
26
+ const choice = choices[Number.parseInt(answer, 10) - 1]
27
+ if (!choice) reject(new Error('Invalid selection'))
28
+ else resolve(choice)
29
+ })
30
+ })
31
+
32
+ const arrows = ({ message, choices, current }) => {
33
+ let index = Math.max(
34
+ 0,
35
+ choices.findIndex(choice => choice.value === current)
36
+ )
37
+ const lines = choices.length + 1
38
+
39
+ const draw = first => {
40
+ if (!first) process.stderr.write(UP_N(lines))
41
+ process.stderr.write(`${styleText('cyan', '?')} ${white(message)}\n`)
42
+ for (const [i, choice] of choices.entries()) {
43
+ const active = i === index
44
+ const prefix = active ? styleText('cyan', '❯') : ' '
45
+ const text = active
46
+ ? styleText('cyan', label(choice, current))
47
+ : label(choice, current)
48
+ process.stderr.write(`\u001b[2K${prefix} ${text}\n`)
49
+ }
50
+ }
51
+
52
+ return new Promise((resolve, reject) => {
53
+ readline.emitKeypressEvents(process.stdin)
54
+ process.stdin.setRawMode(true)
55
+ process.stderr.write(HIDE)
56
+ draw(true)
57
+
58
+ const cleanup = () => {
59
+ process.stdin.setRawMode(false)
60
+ process.stdin.off('keypress', onKey)
61
+ process.stderr.write(SHOW)
62
+ }
63
+
64
+ const onKey = (_str, key) => {
65
+ if (key.name === 'up') {
66
+ index = (index - 1 + choices.length) % choices.length
67
+ draw()
68
+ return
69
+ }
70
+ if (key.name === 'down') {
71
+ index = (index + 1) % choices.length
72
+ draw()
73
+ return
74
+ }
75
+ if (key.name === 'return') {
76
+ cleanup()
77
+ resolve(choices[index])
78
+ return
79
+ }
80
+ if (key.ctrl && key.name === 'c') {
81
+ cleanup()
82
+ reject(Object.assign(new Error('Aborted'), { code: 'ABORT' }))
83
+ }
84
+ }
85
+
86
+ process.stdin.on('keypress', onKey)
87
+ })
88
+ }
89
+
90
+ const select = ({ message, choices, current }) => {
91
+ if (choices.length === 1) return Promise.resolve(choices[0])
92
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
93
+ return numbered({ message, choices, current })
94
+ }
95
+ return arrows({ message, choices, current })
96
+ }
97
+
98
+ module.exports = select
package/bin/style.js CHANGED
@@ -1,10 +1,21 @@
1
1
  'use strict'
2
2
 
3
3
  const { styleText } = require('node:util')
4
+ const { default: terminalLink } = require('terminal-link')
4
5
 
5
6
  const gray = str => styleText('gray', str)
6
7
  const white = str => styleText('white', str)
7
8
  const green = str => styleText('green', str)
8
9
  const red = str => styleText('red', str)
9
10
 
10
- module.exports = { gray, white, green, red, styleText }
11
+ const ORANGE_256 = '\u001b[38;5;208m'
12
+ const DEFAULT_FOREGROUND = '\u001b[39m'
13
+
14
+ const orange = str =>
15
+ process.stdout.hasColors?.()
16
+ ? `${ORANGE_256}${str}${DEFAULT_FOREGROUND}`
17
+ : String(str)
18
+
19
+ const link = url => terminalLink.stderr(url, url, { fallback: false })
20
+
21
+ module.exports = { gray, white, green, red, orange, link, styleText }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "microlink.io",
3
3
  "description": "The Microlink API organized into products, each returning a direct result",
4
4
  "homepage": "https://github.com/microlinkhq/microlink",
5
- "version": "0.2.0",
5
+ "version": "0.5.1",
6
6
  "types": "./src/index.d.ts",
7
7
  "main": "./src/index.js",
8
8
  "exports": {
@@ -14,7 +14,8 @@
14
14
  }
15
15
  },
16
16
  "bin": {
17
- "microlink": "bin/index.js"
17
+ "microlink": "bin/index.js",
18
+ "microlink.io": "bin/index.js"
18
19
  },
19
20
  "author": {
20
21
  "email": "hello@microlink.io",
@@ -43,10 +44,11 @@
43
44
  "seo"
44
45
  ],
45
46
  "dependencies": {
46
- "@microlink/function": "0.3.0",
47
- "@microlink/google": "1.0.6",
48
- "@microlink/mql": "0.18.0",
49
- "mri": "~1.2.0"
47
+ "@microlink/function": "0.3.2",
48
+ "@microlink/google": "1.2.0",
49
+ "@microlink/mql": "0.18.1",
50
+ "mri": "~1.2.0",
51
+ "terminal-link": "~5.0.0"
50
52
  },
51
53
  "devDependencies": {
52
54
  "ava": "latest",
@@ -76,5 +78,5 @@
76
78
  },
77
79
  "directory": "test"
78
80
  },
79
- "gitHead": "3766602b11599a8284e4aa31815d409e0ffa0788"
81
+ "gitHead": "231f3506d8b3345d55a0473a50f9dc1d90d45aed"
80
82
  }