@microlink/cli 2.1.53 → 2.1.55

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.53",
5
+ "version": "2.1.55",
6
6
  "main": "src/index.js",
7
7
  "bin": {
8
8
  "microlink": "bin/microlink",
@@ -33,10 +33,10 @@
33
33
  "microlink"
34
34
  ],
35
35
  "dependencies": {
36
- "@kikobeats/content-type": "~1.0.1",
36
+ "@kikobeats/content-type": "~1.0.4",
37
37
  "@microlink/mql": "~0.16.0",
38
38
  "clipboardy": "~2.3.0",
39
- "is-local-address": "~2.3.0",
39
+ "is-local-address": "~2.3.4",
40
40
  "jsome": "~2.5.0",
41
41
  "mri": "~1.2.0",
42
42
  "nanospinner": "~1.2.2",
@@ -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 -y @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,130 +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
+ }
70
+
71
+ const [requestUrl, requestOptions] = mql.getApiUrl(
72
+ url,
73
+ mqlOpts,
74
+ mergedGotOpts
75
+ )
48
76
 
49
77
  try {
50
- spinner.start()
51
- const response = await mql.buffer(url, mqlOpts, gotOpts)
52
- spinner.stop()
53
- 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 } }
54
97
  } catch (error) {
55
- spinner.stop()
56
98
  error.flags = cli.flags
57
99
  throw error
100
+ } finally {
101
+ if (shouldSpin) spinner.stop()
58
102
  }
59
103
  }
60
104
 
61
- const render = ({ response, flags }) => {
62
- const { headers, timings, requestUrl: uri, body } = response
63
- if (!flags.pretty) return console.log(body.toString())
64
-
65
- const contentType = getContentType(headers['content-type'])
66
- const time = prettyMs(timings.phases.total)
67
- const serverTiming = headers['server-timing']
68
- const id = headers['x-request-id']
69
-
70
- const printMode = (() => {
71
- if (body.toString().startsWith('data:')) return 'base64'
72
- if (contentType !== 'application/json') return 'image'
73
- })()
74
-
75
- switch (printMode) {
76
- case 'base64': {
77
- const extension = contentType.split('/')[1].split(';')[0]
78
- const filepath = temp.file({ extension })
79
- fs.writeFileSync(filepath, body.toString().split(',')[1], 'base64')
80
- print.image(filepath)
81
- break
82
- }
83
- case 'image':
84
- print.image(body)
85
- console.log()
86
- break
87
- default: {
88
- const isText = contentType === 'text/plain'
89
- const isHtml = contentType === 'text/html'
90
- const output = isText || isHtml ? body.toString() : JSON.parse(body)
91
- print.json(output, flags)
92
- break
93
- }
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 })
113
+ }
114
+
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)
94
142
  }
95
143
 
96
- const edgeCacheStatus = headers['cf-cache-status']
97
- const unifiedCacheStatus = headers['x-cache-status']
144
+ const edgeCacheStatus = plainHeaders['cf-cache-status']
145
+ const unifiedCacheStatus = plainHeaders['x-cache-status']
98
146
 
99
147
  const cacheStatus =
100
148
  unifiedCacheStatus === 'MISS' && edgeCacheStatus === 'HIT'
101
149
  ? edgeCacheStatus
102
150
  : unifiedCacheStatus
103
151
 
104
- const timestamp = Number(headers['x-timestamp'])
105
- const ttl = Number(headers['x-cache-ttl'])
152
+ const timestamp = Number(plainHeaders['x-timestamp'])
153
+ const ttl = Number(plainHeaders['x-cache-ttl'])
106
154
  const expires = timestamp + ttl - Date.now()
107
155
  const expiration = prettyMs(expires)
108
156
  const expiredAt = cacheStatus === 'HIT' ? `(${expiration})` : ''
109
- const fetchMode = headers['x-fetch-mode']
110
- const fetchTime = fetchMode && `(${headers['x-fetch-time']})`
111
- 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
112
161
 
113
162
  console.error()
114
163
  console.error(
115
- print.label('success', 'green'),
116
- gray(`${print.bytes(size)} in ${time}`)
164
+ printText.label('success', 'green'),
165
+ gray(`${printText.bytes(size)} in ${time}`)
117
166
  )
118
167
  console.error()
119
168
 
120
169
  if (serverTiming) {
121
- console.error(' ', print.keyValue(green('timing'), serverTiming))
170
+ console.error(' ', printText.keyValue(green('timing'), serverTiming))
122
171
  }
123
172
 
124
173
  if (cacheStatus) {
125
174
  console.error(
126
175
  ' ',
127
- print.keyValue(green('cache'), `${cacheStatus} ${gray(expiredAt)}`)
176
+ printText.keyValue(green('cache'), `${cacheStatus} ${gray(expiredAt)}`)
128
177
  )
129
178
  }
130
179
 
131
180
  if (fetchMode) {
132
181
  console.error(
133
182
  ' ',
134
- print.keyValue(green('mode'), `${fetchMode} ${gray(fetchTime)}`)
183
+ printText.keyValue(green('mode'), `${fetchMode} ${gray(fetchTime)}`)
135
184
  )
136
185
  }
137
186
 
138
- console.error(' ', print.keyValue(green('uri'), uri))
139
- console.error(' ', print.keyValue(green('id'), id))
187
+ console.error(' ', printText.keyValue(green('uri'), uri))
188
+ console.error(' ', printText.keyValue(green('id'), id))
140
189
 
141
190
  if (flags.copy) {
142
- let copiedValue
143
- try {
144
- copiedValue = JSON.parse(body)
145
- } catch (err) {
146
- copiedValue = body
147
- }
148
- clipboardy.writeSync(JSON.stringify(copiedValue, null, 2))
191
+ toClipboard(JSON.parse(bodyText), flags)
149
192
  console.error(`\n ${gray('Copied to clipboard!')}`)
150
193
  }
151
194
  }
package/src/cli.js CHANGED
@@ -1,22 +1,33 @@
1
1
  'use strict'
2
2
 
3
3
  const mri = require('mri')
4
+ const { hasColorizedOutput, parseHeaders } = require('./util')
4
5
 
5
- const { _, ...flags } = mri(process.argv.slice(2), {
6
- boolean: ['color', 'copy', 'pretty'],
6
+ const parsed = mri(process.argv.slice(2), {
7
+ alias: { H: 'header' },
8
+ boolean: ['copy', 'json', 'json-full', 'pretty'],
9
+ string: ['header'],
7
10
  default: {
8
11
  apiKey: process.env.MICROLINK_API_KEY,
9
- pretty: true,
10
- color: true,
11
- copy: false
12
+ pretty: hasColorizedOutput(),
13
+ copy: false,
14
+ json: false,
15
+ 'json-full': false
12
16
  }
13
17
  })
14
18
 
19
+ const { _, header, ...flags } = parsed
20
+
21
+ const headers = parseHeaders(header)
22
+
15
23
  module.exports = {
16
24
  flags,
25
+ headers,
17
26
  input: _,
18
27
  showHelp: () => {
19
28
  console.log(require('./help'))
20
29
  process.exit(0)
21
30
  }
22
31
  }
32
+
33
+ module.exports.parseHeaders = parseHeaders
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,21 +13,34 @@ 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')
25
34
  )}).`
26
35
  )}
36
+ ${gray('-H <header> pass custom HTTP header to the request (repeatable).')}
27
37
 
28
38
 
29
39
  Examples
30
40
  ${gray('microlink https://microlink.io&palette')}
31
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')}
32
44
  ${gray('microlink https://microlink.io&palette --api-key=MyApiKey')}
45
+ ${gray("microlink https://example.com -H 'x-user-cookie: 1'")}
33
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