microlink.io 0.2.0 → 0.5.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.
package/LICENSE.md CHANGED
File without changes
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 => {
@@ -248,7 +275,7 @@ const takeHttpHeaders = flags => {
248
275
 
249
276
  const argv = mri(process.argv.slice(2), {
250
277
  alias: { H: 'header' },
251
- boolean: ['trace', 'trace-full', 'help'],
278
+ boolean: ['trace', 'trace-full', 'help', 'html', 'markdown'],
252
279
  string: ['header', 'api-key', 'data', 'file', 'endpoint']
253
280
  })
254
281
 
@@ -263,6 +290,8 @@ let {
263
290
  endpoint: endpointFlag,
264
291
  trace,
265
292
  'trace-full': traceFull,
293
+ html: htmlFlag,
294
+ markdown: markdownFlag,
266
295
  ...flags
267
296
  } = argv
268
297
 
@@ -270,69 +299,102 @@ const isTrace = trace || traceFull
270
299
 
271
300
  if (!command) showHelp()
272
301
 
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
- })
302
+ if (command === 'login' || command === 'logout') {
303
+ if (help) showHelp(command)
304
+ if (command === 'logout') {
305
+ console.error(clearConfig() ? 'Logged out.' : 'Already logged out.')
306
+ process.exit(0)
307
+ }
308
+ login().then(
309
+ () => process.exit(0),
310
+ error => {
311
+ console.error(error.message)
312
+ process.exit(error.code === 'ABORT' ? 130 : 1)
313
+ }
314
+ )
315
+ } else {
316
+ const apiKey =
317
+ apiKeyFlag || apiKeyCamel || process.env.MICROLINK_API_KEY || readApiKey()
318
+ const endpoint = endpointFlag
319
+ const client = create({
320
+ ...(apiKey && { apiKey }),
321
+ ...(endpoint && { endpoint })
322
+ })
279
323
 
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)
324
+ if (typeof client[command] !== 'function') {
325
+ if (!target && URL.canParse(command)) {
326
+ target = command
327
+ command = 'metadata'
328
+ } else if (help) {
329
+ showHelp()
330
+ } else {
331
+ console.error(
332
+ `Unknown command \`${command}\`. Run \`microlink --help\` to see the available commands.`
333
+ )
334
+ process.exit(1)
335
+ }
291
336
  }
292
- }
293
337
 
294
- if (help || !target) showHelp(command)
338
+ if (help || !target) showHelp(command)
295
339
 
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
- }
340
+ if (
341
+ isTrace &&
342
+ (command === 'search' || command === 'function' || command === 'run')
343
+ ) {
344
+ console.error(`\`--trace\` is not supported for \`${command}\`.`)
345
+ process.exit(1)
346
+ }
303
347
 
304
- const options = { ...flags }
305
- const headers = { ...takeHttpHeaders(options), ...parseHeaders(header) }
306
- if (Object.keys(headers).length > 0) options.headers = headers
348
+ const options = { ...flags }
349
+ const headers = { ...takeHttpHeaders(options), ...parseHeaders(header) }
350
+ if (Object.keys(headers).length > 0) options.headers = headers
307
351
 
308
- const invoke = () => {
352
+ let rules
309
353
  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)
354
+ try {
355
+ rules = JSON.parse(data)
356
+ } catch {
357
+ printFail({ message: 'Invalid --data JSON' })
358
+ process.exit(1)
359
+ }
315
360
  }
316
- return client[command](target, options)
317
- }
318
361
 
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)
362
+ const invoke = () => {
363
+ if (command === 'extract') {
364
+ return client.extract(target, rules, options)
365
+ }
366
+ if (command === 'function' || command === 'run') {
367
+ const code = readFileSync(path.resolve(file), 'utf8')
368
+ return client.function(target, code, options)
369
+ }
370
+ if (command === 'search') {
371
+ return client.search(target, {
372
+ ...options,
373
+ ...(htmlFlag && { html: true }),
374
+ ...(markdownFlag && { markdown: true })
375
+ })
376
+ }
377
+ return client[command](target, options)
337
378
  }
338
- })()
379
+
380
+ const spin = !isTrace && shouldSpin() ? spinner() : null
381
+
382
+ ;(async () => {
383
+ spin?.start()
384
+ const started = Date.now()
385
+ try {
386
+ const result = await invoke()
387
+ const duration = Date.now() - started
388
+ spin?.stop()
389
+ if (isTrace) printJson(tracePayload({ ...client.last, full: traceFull }))
390
+ else if (typeof result === 'string') console.log(result)
391
+ else printJson({ status: 'success', data: result })
392
+ if (!isTrace) printFooter({ duration, response: client.last.response })
393
+ process.exit(0)
394
+ } catch (error) {
395
+ spin?.stop()
396
+ printFail(error)
397
+ process.exit(1)
398
+ }
399
+ })()
400
+ }
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.0",
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
+ "mri": "~1.2.0",
48
+ "terminal-link": "~5.0.0",
49
+ "@microlink/google": "1.2.0",
50
+ "@microlink/function": "0.3.2",
51
+ "@microlink/mql": "0.18.1"
50
52
  },
51
53
  "devDependencies": {
52
54
  "ava": "latest",
@@ -61,9 +63,6 @@
61
63
  "bin",
62
64
  "src"
63
65
  ],
64
- "scripts": {
65
- "test": "ava && tsd"
66
- },
67
66
  "preferGlobal": true,
68
67
  "license": "MIT",
69
68
  "ava": {
@@ -76,5 +75,7 @@
76
75
  },
77
76
  "directory": "test"
78
77
  },
79
- "gitHead": "3766602b11599a8284e4aa31815d409e0ffa0788"
80
- }
78
+ "scripts": {
79
+ "test": "ava && tsd"
80
+ }
81
+ }