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 +0 -0
- package/bin/config.js +61 -0
- package/bin/help.js +43 -11
- package/bin/index.js +132 -70
- package/bin/login.js +148 -0
- package/bin/select.js +98 -0
- package/bin/style.js +12 -1
- package/package.json +12 -11
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
|
-
[
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
|
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',
|
|
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
|
-
|
|
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(
|
|
203
|
-
if (error.url) console.error(' ', keyValue(
|
|
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
|
-
|
|
229
|
+
color('code'),
|
|
209
230
|
`${error.code}${error.statusCode ? ` (${error.statusCode})` : ''}`
|
|
210
231
|
)
|
|
211
232
|
)
|
|
212
233
|
}
|
|
213
|
-
if (error.more) console.error(' ', keyValue(
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
) {
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
|
|
352
|
+
let rules
|
|
309
353
|
if (command === 'extract') {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"@microlink/
|
|
49
|
-
"
|
|
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
|
-
"
|
|
80
|
-
|
|
78
|
+
"scripts": {
|
|
79
|
+
"test": "ava && tsd"
|
|
80
|
+
}
|
|
81
|
+
}
|