@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 +5 -4
- package/src/api.js +112 -69
- package/src/cli.js +16 -5
- package/src/exit.js +1 -1
- package/src/help.js +14 -1
- package/src/index.js +1 -2
- package/src/print-json.js +33 -0
- package/src/util.js +44 -0
- /package/src/{print.js → print-text.js} +0 -0
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.
|
|
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.
|
|
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.
|
|
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": "
|
|
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
|
|
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
|
-
]
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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.
|
|
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 {
|
|
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 =
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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,
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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 =
|
|
97
|
-
const unifiedCacheStatus =
|
|
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(
|
|
105
|
-
const ttl = Number(
|
|
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 =
|
|
110
|
-
const fetchTime = fetchMode && `(${
|
|
111
|
-
const size = Number(
|
|
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
|
-
|
|
116
|
-
gray(`${
|
|
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(' ',
|
|
170
|
+
console.error(' ', printText.keyValue(green('timing'), serverTiming))
|
|
122
171
|
}
|
|
123
172
|
|
|
124
173
|
if (cacheStatus) {
|
|
125
174
|
console.error(
|
|
126
175
|
' ',
|
|
127
|
-
|
|
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
|
-
|
|
183
|
+
printText.keyValue(green('mode'), `${fetchMode} ${gray(fetchTime)}`)
|
|
135
184
|
)
|
|
136
185
|
}
|
|
137
186
|
|
|
138
|
-
console.error(' ',
|
|
139
|
-
console.error(' ',
|
|
187
|
+
console.error(' ', printText.keyValue(green('uri'), uri))
|
|
188
|
+
console.error(' ', printText.keyValue(green('id'), id))
|
|
140
189
|
|
|
141
190
|
if (flags.copy) {
|
|
142
|
-
|
|
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
|
|
6
|
-
|
|
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:
|
|
10
|
-
|
|
11
|
-
|
|
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
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
|
@@ -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
|