@microlink/cli 2.1.54 → 2.1.56

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/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@microlink/cli",
3
3
  "description": "Interacting with Microlink API from your terminal.",
4
4
  "homepage": "https://github.com/microlinkhq/cli",
5
- "version": "2.1.54",
5
+ "version": "2.1.56",
6
6
  "main": "src/index.js",
7
7
  "bin": {
8
8
  "microlink": "bin/microlink",
@@ -51,7 +51,7 @@
51
51
  "devDependencies": {
52
52
  "@commitlint/cli": "latest",
53
53
  "@commitlint/config-conventional": "latest",
54
- "ava": "latest",
54
+ "ava": "7",
55
55
  "c8": "latest",
56
56
  "ci-publish": "latest",
57
57
  "finepack": "latest",
@@ -95,6 +95,7 @@
95
95
  },
96
96
  "nano-staged": {
97
97
  "*.js": [
98
+ "npx @kikobeats/prettier-standard",
98
99
  "standard --fix"
99
100
  ],
100
101
  "*.md": [
package/src/api.js CHANGED
@@ -6,15 +6,16 @@ require('update-notifier')({ pkg: require('../package.json') }).notify()
6
6
 
7
7
  const getContentType = require('@kikobeats/content-type')
8
8
  const { URLSearchParams } = require('url')
9
- const clipboardy = require('clipboardy')
10
9
  const mql = require('@microlink/mql')
11
10
  const prettyMs = require('pretty-ms')
12
11
  const temp = require('temperment')
13
12
  const fs = require('fs')
14
13
  const os = require('os')
15
14
 
15
+ const { toClipboard, toPlainHeaders } = require('./util')
16
16
  const { gray, green } = require('./colors')
17
- const print = require('./print')
17
+ const printJson = require('./print-json')
18
+ const printText = require('./print-text')
18
19
  const exit = require('./exit')
19
20
 
20
21
  const microlinkUrl = () =>
@@ -22,134 +23,172 @@ const microlinkUrl = () =>
22
23
 
23
24
  const normalizeInput = input => {
24
25
  if (!input) return input
25
- ;[
26
+ let normalized = input
27
+ const sanitizers = [
26
28
  microlinkUrl,
27
29
  () => require('is-local-address/ipv4').regex,
28
30
  () => require('is-local-address/ipv6').regex
29
- ].forEach(regex => {
30
- return (input = input.replace(regex(), ''))
31
- })
32
- return input.replace(/^\??url=/, '')
31
+ ]
32
+
33
+ for (const createRegex of sanitizers) {
34
+ normalized = normalized.replace(createRegex(), '')
35
+ }
36
+
37
+ return normalized.replace(/^\??url=/, '')
33
38
  }
34
39
 
35
40
  const getInput = input => {
36
41
  const collection = input.length === 1 ? input[0].split(os.EOL) : input
37
- return collection.reduce((acc, item) => acc + item.trim(), '')
42
+ return collection.map(item => item.trim()).join('')
38
43
  }
39
44
 
40
45
  const toPlainObject = input => Object.fromEntries(new URLSearchParams(input))
41
46
 
42
47
  const fetch = async (cli, gotOpts) => {
43
- const { pretty, color, copy, endpoint, ...flags } = cli.flags
48
+ const {
49
+ pretty,
50
+ copy,
51
+ json,
52
+ 'json-full': jsonFull,
53
+ endpoint,
54
+ ...flags
55
+ } = cli.flags
56
+ const isJson = json || jsonFull
44
57
  const input = getInput(cli.input, endpoint)
45
58
  const { url, ...queryParams } = toPlainObject(`url=${normalizeInput(input)}`)
46
59
  const mqlOpts = { endpoint, ...queryParams, ...flags }
47
- const spinner = print.spinner()
60
+ const spinner = printText.spinner()
61
+ const shouldSpin = !isJson && pretty
62
+
63
+ let mergedGotOpts = gotOpts
64
+ if (Object.keys(cli.headers).length > 0) {
65
+ mergedGotOpts = {
66
+ ...gotOpts,
67
+ headers: { ...gotOpts.headers, ...cli.headers }
68
+ }
69
+ }
48
70
 
49
- const mergedGotOpts = Object.keys(cli.headers).length > 0
50
- ? { ...gotOpts, headers: { ...gotOpts.headers, ...cli.headers } }
51
- : gotOpts
71
+ const [requestUrl, requestOptions] = mql.getApiUrl(
72
+ url,
73
+ mqlOpts,
74
+ mergedGotOpts
75
+ )
52
76
 
53
77
  try {
54
- spinner.start()
55
- const response = await mql.buffer(url, mqlOpts, mergedGotOpts)
56
- spinner.stop()
57
- return { response, flags: { copy, pretty } }
78
+ if (shouldSpin) spinner.start()
79
+
80
+ const start = Date.now()
81
+ const request = isJson ? mql : mql.buffer
82
+ const mqlResponse = await request(url, mqlOpts, mergedGotOpts)
83
+ const duration = Date.now() - start
84
+
85
+ let response = mqlResponse
86
+ if (isJson) {
87
+ response = printJson({
88
+ requestUrl,
89
+ requestOptions,
90
+ response: mqlResponse.response,
91
+ full: jsonFull,
92
+ pretty
93
+ })
94
+ }
95
+
96
+ return { response, duration, flags: { copy, pretty, json: isJson } }
58
97
  } catch (error) {
59
- spinner.stop()
60
98
  error.flags = cli.flags
61
99
  throw error
100
+ } finally {
101
+ if (shouldSpin) spinner.stop()
62
102
  }
63
103
  }
64
104
 
65
- const render = ({ response, flags }) => {
66
- const { headers, timings, requestUrl: uri, body } = response
67
- if (!flags.pretty) return console.log(body.toString())
68
-
69
- const contentType = getContentType(headers['content-type'])
70
- const time = prettyMs(timings.phases.total)
71
- const serverTiming = headers['server-timing']
72
- const id = headers['x-request-id']
73
-
74
- const printMode = (() => {
75
- if (body.toString().startsWith('data:')) return 'base64'
76
- if (contentType !== 'application/json') return 'image'
77
- })()
78
-
79
- switch (printMode) {
80
- case 'base64': {
81
- const extension = contentType.split('/')[1].split(';')[0]
82
- const filepath = temp.file({ extension })
83
- fs.writeFileSync(filepath, body.toString().split(',')[1], 'base64')
84
- print.image(filepath)
85
- break
86
- }
87
- case 'image':
88
- print.image(body)
89
- console.log()
90
- break
91
- default: {
92
- const isText = contentType === 'text/plain'
93
- const isHtml = contentType === 'text/html'
94
- const output = isText || isHtml ? body.toString() : JSON.parse(body)
95
- print.json(output, flags)
96
- break
97
- }
105
+ const render = ({ response, duration, flags }) => {
106
+ const { headers, requestUrl, url: responseUrl, body } = response
107
+
108
+ if (flags.json) {
109
+ if (flags.copy) toClipboard(JSON.parse(response), flags)
110
+ if (!flags.pretty) return console.log(response)
111
+
112
+ return printText.json(JSON.parse(response), { color: true })
98
113
  }
99
114
 
100
- const edgeCacheStatus = headers['cf-cache-status']
101
- const unifiedCacheStatus = headers['x-cache-status']
115
+ const plainHeaders = toPlainHeaders(headers)
116
+
117
+ const bodyBuffer = Buffer.isBuffer(body) ? body : Buffer.from(body)
118
+ const bodyText = bodyBuffer.toString()
119
+
120
+ if (!flags.pretty) return console.log(bodyText)
121
+
122
+ const contentType = getContentType(plainHeaders['content-type'])
123
+ const time = Number.isFinite(duration) ? prettyMs(duration) : 'unknown'
124
+ const serverTiming = plainHeaders['server-timing']
125
+ const id = plainHeaders['x-request-id']
126
+
127
+ if (bodyText.startsWith('data:')) {
128
+ const extension = contentType
129
+ ? contentType.split('/')[1].split(';')[0]
130
+ : 'png'
131
+ const filepath = temp.file({ extension })
132
+ fs.writeFileSync(filepath, bodyText.split(',')[1], 'base64')
133
+ printText.image(filepath)
134
+ } else if (contentType !== 'application/json') {
135
+ printText.image(bodyBuffer)
136
+ console.log()
137
+ } else {
138
+ const isText = contentType === 'text/plain'
139
+ const isHtml = contentType === 'text/html'
140
+ const output = isText || isHtml ? bodyText : JSON.parse(bodyText)
141
+ printText.json(output, flags)
142
+ }
143
+
144
+ const edgeCacheStatus = plainHeaders['cf-cache-status']
145
+ const unifiedCacheStatus = plainHeaders['x-cache-status']
102
146
 
103
147
  const cacheStatus =
104
148
  unifiedCacheStatus === 'MISS' && edgeCacheStatus === 'HIT'
105
149
  ? edgeCacheStatus
106
150
  : unifiedCacheStatus
107
151
 
108
- const timestamp = Number(headers['x-timestamp'])
109
- const ttl = Number(headers['x-cache-ttl'])
152
+ const timestamp = Number(plainHeaders['x-timestamp'])
153
+ const ttl = Number(plainHeaders['x-cache-ttl'])
110
154
  const expires = timestamp + ttl - Date.now()
111
155
  const expiration = prettyMs(expires)
112
156
  const expiredAt = cacheStatus === 'HIT' ? `(${expiration})` : ''
113
- const fetchMode = headers['x-fetch-mode']
114
- const fetchTime = fetchMode && `(${headers['x-fetch-time']})`
115
- const size = Number(headers['content-length'] || Buffer.byteLength(body))
157
+ const fetchMode = plainHeaders['x-fetch-mode']
158
+ const fetchTime = fetchMode && `(${plainHeaders['x-fetch-time']})`
159
+ const size = Number(plainHeaders['content-length'] || bodyBuffer.length)
160
+ const uri = requestUrl || responseUrl
116
161
 
117
162
  console.error()
118
163
  console.error(
119
- print.label('success', 'green'),
120
- gray(`${print.bytes(size)} in ${time}`)
164
+ printText.label('success', 'green'),
165
+ gray(`${printText.bytes(size)} in ${time}`)
121
166
  )
122
167
  console.error()
123
168
 
124
169
  if (serverTiming) {
125
- console.error(' ', print.keyValue(green('timing'), serverTiming))
170
+ console.error(' ', printText.keyValue(green('timing'), serverTiming))
126
171
  }
127
172
 
128
173
  if (cacheStatus) {
129
174
  console.error(
130
175
  ' ',
131
- print.keyValue(green('cache'), `${cacheStatus} ${gray(expiredAt)}`)
176
+ printText.keyValue(green('cache'), `${cacheStatus} ${gray(expiredAt)}`)
132
177
  )
133
178
  }
134
179
 
135
180
  if (fetchMode) {
136
181
  console.error(
137
182
  ' ',
138
- print.keyValue(green('mode'), `${fetchMode} ${gray(fetchTime)}`)
183
+ printText.keyValue(green('mode'), `${fetchMode} ${gray(fetchTime)}`)
139
184
  )
140
185
  }
141
186
 
142
- console.error(' ', print.keyValue(green('uri'), uri))
143
- console.error(' ', print.keyValue(green('id'), id))
187
+ console.error(' ', printText.keyValue(green('uri'), uri))
188
+ console.error(' ', printText.keyValue(green('id'), id))
144
189
 
145
190
  if (flags.copy) {
146
- let copiedValue
147
- try {
148
- copiedValue = JSON.parse(body)
149
- } catch (err) {
150
- copiedValue = body
151
- }
152
- clipboardy.writeSync(JSON.stringify(copiedValue, null, 2))
191
+ toClipboard(JSON.parse(bodyText), flags)
153
192
  console.error(`\n ${gray('Copied to clipboard!')}`)
154
193
  }
155
194
  }
package/src/cli.js CHANGED
@@ -1,30 +1,24 @@
1
1
  'use strict'
2
2
 
3
3
  const mri = require('mri')
4
+ const { hasColorizedOutput, parseHeaders } = require('./util')
4
5
 
5
- const { _, header, ...flags } = mri(process.argv.slice(2), {
6
+ const parsed = mri(process.argv.slice(2), {
6
7
  alias: { H: 'header' },
7
- boolean: ['color', 'copy', 'pretty'],
8
+ boolean: ['copy', 'json', 'json-full', 'pretty'],
8
9
  string: ['header'],
9
10
  default: {
10
11
  apiKey: process.env.MICROLINK_API_KEY,
11
- pretty: true,
12
- color: true,
13
- copy: false
12
+ pretty: hasColorizedOutput(),
13
+ copy: false,
14
+ json: false,
15
+ 'json-full': false
14
16
  }
15
17
  })
16
18
 
17
- const parseHeaders = raw => {
18
- if (!raw) return {}
19
- const entries = Array.isArray(raw) ? raw : [raw]
20
- const headers = {}
21
- for (const entry of entries) {
22
- const idx = entry.indexOf(':')
23
- if (idx === -1) continue
24
- headers[entry.slice(0, idx).trim().toLowerCase()] = entry.slice(idx + 1).trim()
25
- }
26
- return headers
27
- }
19
+ const { _, header, 'api-key': apiKey, ...flags } = parsed
20
+
21
+ if (apiKey !== undefined) flags.apiKey = apiKey
28
22
 
29
23
  const headers = parseHeaders(header)
30
24
 
package/src/exit.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  const { gray, red } = require('./colors')
4
4
 
5
- const print = require('./print')
5
+ const print = require('./print-text')
6
6
 
7
7
  module.exports = async (promise, { flags }) => {
8
8
  try {
package/src/help.js CHANGED
@@ -13,12 +13,21 @@ Flags
13
13
  white('$MICROLINK_API_KEY')
14
14
  )}`
15
15
  )}
16
- ${gray(`--colors colorize output (default is ${code(white('true'))}`)}
17
16
  ${gray(
18
17
  `--copy copy output to clipboard (default is ${code(
19
18
  white('false')
20
19
  )}).`
21
20
  )}
21
+ ${gray(
22
+ `--json print request & response payload as JSON (API key masked, default is ${code(
23
+ white('false')
24
+ )}).`
25
+ )}
26
+ ${gray(
27
+ `--json-full print request & response payload as JSON including full API key (default is ${code(
28
+ white('false')
29
+ )}).`
30
+ )}
22
31
  ${gray(
23
32
  `--pretty beauty response payload (default is ${code(
24
33
  white('true')
@@ -30,6 +39,8 @@ Flags
30
39
  Examples
31
40
  ${gray('microlink https://microlink.io&palette')}
32
41
  ${gray('microlink https://microlink.io&palette --no-pretty')}
42
+ ${gray('microlink https://microlink.io&palette --json')}
43
+ ${gray('microlink https://microlink.io&palette --json-full')}
33
44
  ${gray('microlink https://microlink.io&palette --api-key=MyApiKey')}
34
45
  ${gray("microlink https://example.com -H 'x-user-cookie: 1'")}
35
46
  `
package/src/index.js CHANGED
@@ -3,6 +3,5 @@
3
3
  module.exports = {
4
4
  cli: require('./cli'),
5
5
  api: require('./api'),
6
- exit: require('./exit'),
7
- print: require('./print')
6
+ exit: require('./exit')
8
7
  }
@@ -0,0 +1,33 @@
1
+ 'use strict'
2
+
3
+ const { humanizeApiKey, toPlainHeaders, stringify } = require('./util')
4
+
5
+ module.exports = ({
6
+ requestUrl,
7
+ requestOptions,
8
+ response,
9
+ full = false,
10
+ pretty = true
11
+ }) => {
12
+ const { responseType, ...requestOptionsWithoutResponseType } = requestOptions
13
+ const headers = { ...requestOptionsWithoutResponseType.headers }
14
+
15
+ if (!full && headers['x-api-key']) {
16
+ headers['x-api-key'] = humanizeApiKey(headers['x-api-key'])
17
+ }
18
+
19
+ return stringify(
20
+ {
21
+ request: {
22
+ url: requestUrl,
23
+ ...requestOptionsWithoutResponseType,
24
+ headers
25
+ },
26
+ response: {
27
+ ...response,
28
+ headers: toPlainHeaders(response.headers)
29
+ }
30
+ },
31
+ { pretty }
32
+ )
33
+ }
package/src/util.js ADDED
@@ -0,0 +1,44 @@
1
+ 'use strict'
2
+
3
+ const clipboardy = require('clipboardy')
4
+
5
+ const toPlainHeaders = headers => Object.fromEntries(headers.entries())
6
+
7
+ const humanizeApiKey = apiKey => `${apiKey.substring(0, 5)}…`
8
+
9
+ const stringify = (value, { pretty }) =>
10
+ JSON.stringify(value, null, pretty ? 2 : 0)
11
+
12
+ const toClipboard = (value, flags) =>
13
+ clipboardy.writeSync(stringify(value, flags))
14
+
15
+ const hasColorizedOutput = () =>
16
+ !process.env.NO_COLOR &&
17
+ process.env.FORCE_COLOR !== '0' &&
18
+ Boolean(process.stdout?.hasColors?.())
19
+
20
+ const parseHeaders = raw => {
21
+ if (!raw) return {}
22
+
23
+ const entries = Array.isArray(raw) ? raw : [raw]
24
+ const headers = {}
25
+
26
+ for (const entry of entries) {
27
+ const idx = entry.indexOf(':')
28
+ if (idx === -1) continue
29
+ headers[entry.slice(0, idx).trim().toLowerCase()] = entry
30
+ .slice(idx + 1)
31
+ .trim()
32
+ }
33
+
34
+ return headers
35
+ }
36
+
37
+ module.exports = {
38
+ hasColorizedOutput,
39
+ humanizeApiKey,
40
+ parseHeaders,
41
+ stringify,
42
+ toClipboard,
43
+ toPlainHeaders
44
+ }
File without changes