@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 +3 -2
- package/src/api.js +111 -72
- package/src/cli.js +10 -16
- package/src/exit.js +1 -1
- package/src/help.js +12 -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.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": "
|
|
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
|
|
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
|
-
]
|
|
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
|
+
}
|
|
48
70
|
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
71
|
+
const [requestUrl, requestOptions] = mql.getApiUrl(
|
|
72
|
+
url,
|
|
73
|
+
mqlOpts,
|
|
74
|
+
mergedGotOpts
|
|
75
|
+
)
|
|
52
76
|
|
|
53
77
|
try {
|
|
54
|
-
spinner.start()
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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,
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
101
|
-
|
|
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(
|
|
109
|
-
const ttl = Number(
|
|
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 =
|
|
114
|
-
const fetchTime = fetchMode && `(${
|
|
115
|
-
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
|
|
116
161
|
|
|
117
162
|
console.error()
|
|
118
163
|
console.error(
|
|
119
|
-
|
|
120
|
-
gray(`${
|
|
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(' ',
|
|
170
|
+
console.error(' ', printText.keyValue(green('timing'), serverTiming))
|
|
126
171
|
}
|
|
127
172
|
|
|
128
173
|
if (cacheStatus) {
|
|
129
174
|
console.error(
|
|
130
175
|
' ',
|
|
131
|
-
|
|
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
|
-
|
|
183
|
+
printText.keyValue(green('mode'), `${fetchMode} ${gray(fetchTime)}`)
|
|
139
184
|
)
|
|
140
185
|
}
|
|
141
186
|
|
|
142
|
-
console.error(' ',
|
|
143
|
-
console.error(' ',
|
|
187
|
+
console.error(' ', printText.keyValue(green('uri'), uri))
|
|
188
|
+
console.error(' ', printText.keyValue(green('id'), id))
|
|
144
189
|
|
|
145
190
|
if (flags.copy) {
|
|
146
|
-
|
|
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
|
|
6
|
+
const parsed = mri(process.argv.slice(2), {
|
|
6
7
|
alias: { H: 'header' },
|
|
7
|
-
boolean: ['
|
|
8
|
+
boolean: ['copy', 'json', 'json-full', 'pretty'],
|
|
8
9
|
string: ['header'],
|
|
9
10
|
default: {
|
|
10
11
|
apiKey: process.env.MICROLINK_API_KEY,
|
|
11
|
-
pretty:
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
pretty: hasColorizedOutput(),
|
|
13
|
+
copy: false,
|
|
14
|
+
json: false,
|
|
15
|
+
'json-full': false
|
|
14
16
|
}
|
|
15
17
|
})
|
|
16
18
|
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
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
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
|
@@ -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
|