This repository was archived by the owner on Aug 19, 2026. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1
Merged
feat: json flag #112
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
6b278fc
chore: add json serialization
Kikobeats abf2014
fix: don’t check timings
Kikobeats 756ce6f
refactor: less is more
Kikobeats 6ef2bbc
ci: downgrade ava
Kikobeats 7904332
refactor: less is more
Kikobeats a2ac8f5
test: moar
Kikobeats 3e95b41
chore: remove unused
Kikobeats File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,150 +6,189 @@ require('update-notifier')({ pkg: require('../package.json') }).notify() | |
|
|
||
| const getContentType = require('@kikobeats/content-type') | ||
| const { URLSearchParams } = require('url') | ||
| const clipboardy = require('clipboardy') | ||
| const mql = require('@microlink/mql') | ||
| const prettyMs = require('pretty-ms') | ||
| const temp = require('temperment') | ||
| const fs = require('fs') | ||
| const os = require('os') | ||
|
|
||
| const { toClipboard, toPlainHeaders } = require('./util') | ||
| const { gray, green } = require('./colors') | ||
| const print = require('./print') | ||
| const printJson = require('./print-json') | ||
| const printText = require('./print-text') | ||
| const exit = require('./exit') | ||
|
|
||
| const microlinkUrl = () => | ||
| /^https?:\/\/((?!fonts|geolocation\.)[a-z0-9-]+\.)+microlink\.io/ | ||
|
|
||
| const normalizeInput = input => { | ||
| if (!input) return input | ||
| ;[ | ||
| let normalized = input | ||
| const sanitizers = [ | ||
| microlinkUrl, | ||
| () => require('is-local-address/ipv4').regex, | ||
| () => require('is-local-address/ipv6').regex | ||
| ].forEach(regex => { | ||
| return (input = input.replace(regex(), '')) | ||
| }) | ||
| return input.replace(/^\??url=/, '') | ||
| ] | ||
|
|
||
| for (const createRegex of sanitizers) { | ||
| normalized = normalized.replace(createRegex(), '') | ||
| } | ||
|
|
||
| return normalized.replace(/^\??url=/, '') | ||
| } | ||
|
|
||
| const getInput = input => { | ||
| const collection = input.length === 1 ? input[0].split(os.EOL) : input | ||
| return collection.reduce((acc, item) => acc + item.trim(), '') | ||
| return collection.map(item => item.trim()).join('') | ||
| } | ||
|
|
||
| const toPlainObject = input => Object.fromEntries(new URLSearchParams(input)) | ||
|
|
||
| const fetch = async (cli, gotOpts) => { | ||
| const { pretty, color, copy, endpoint, ...flags } = cli.flags | ||
| const { | ||
| pretty, | ||
| copy, | ||
| json, | ||
| 'json-full': jsonFull, | ||
| endpoint, | ||
| ...flags | ||
| } = cli.flags | ||
| const isJson = json || jsonFull | ||
| const input = getInput(cli.input, endpoint) | ||
| const { url, ...queryParams } = toPlainObject(`url=${normalizeInput(input)}`) | ||
| const mqlOpts = { endpoint, ...queryParams, ...flags } | ||
| const spinner = print.spinner() | ||
| const spinner = printText.spinner() | ||
| const shouldSpin = !isJson && pretty | ||
|
|
||
| let mergedGotOpts = gotOpts | ||
| if (Object.keys(cli.headers).length > 0) { | ||
| mergedGotOpts = { | ||
| ...gotOpts, | ||
| headers: { ...gotOpts.headers, ...cli.headers } | ||
| } | ||
| } | ||
|
|
||
| const mergedGotOpts = Object.keys(cli.headers).length > 0 | ||
| ? { ...gotOpts, headers: { ...gotOpts.headers, ...cli.headers } } | ||
| : gotOpts | ||
| const [requestUrl, requestOptions] = mql.getApiUrl( | ||
| url, | ||
| mqlOpts, | ||
| mergedGotOpts | ||
| ) | ||
|
|
||
| try { | ||
| spinner.start() | ||
| const response = await mql.buffer(url, mqlOpts, mergedGotOpts) | ||
| spinner.stop() | ||
| return { response, flags: { copy, pretty } } | ||
| if (shouldSpin) spinner.start() | ||
|
|
||
| const start = Date.now() | ||
| const request = isJson ? mql : mql.buffer | ||
| const mqlResponse = await request(url, mqlOpts, mergedGotOpts) | ||
| const duration = Date.now() - start | ||
|
|
||
| let response = mqlResponse | ||
| if (isJson) { | ||
| response = printJson({ | ||
| requestUrl, | ||
| requestOptions, | ||
| response: mqlResponse.response, | ||
| full: jsonFull, | ||
| pretty | ||
| }) | ||
| } | ||
|
|
||
| return { response, duration, flags: { copy, pretty, json: isJson } } | ||
| } catch (error) { | ||
| spinner.stop() | ||
| error.flags = cli.flags | ||
| throw error | ||
| } finally { | ||
| if (shouldSpin) spinner.stop() | ||
| } | ||
| } | ||
|
|
||
| const render = ({ response, flags }) => { | ||
| const { headers, timings, requestUrl: uri, body } = response | ||
| if (!flags.pretty) return console.log(body.toString()) | ||
|
|
||
| const contentType = getContentType(headers['content-type']) | ||
| const time = prettyMs(timings.phases.total) | ||
| const serverTiming = headers['server-timing'] | ||
| const id = headers['x-request-id'] | ||
|
|
||
| const printMode = (() => { | ||
| if (body.toString().startsWith('data:')) return 'base64' | ||
| if (contentType !== 'application/json') return 'image' | ||
| })() | ||
|
|
||
| switch (printMode) { | ||
| case 'base64': { | ||
| const extension = contentType.split('/')[1].split(';')[0] | ||
| const filepath = temp.file({ extension }) | ||
| fs.writeFileSync(filepath, body.toString().split(',')[1], 'base64') | ||
| print.image(filepath) | ||
| break | ||
| } | ||
| case 'image': | ||
| print.image(body) | ||
| console.log() | ||
| break | ||
| default: { | ||
| const isText = contentType === 'text/plain' | ||
| const isHtml = contentType === 'text/html' | ||
| const output = isText || isHtml ? body.toString() : JSON.parse(body) | ||
| print.json(output, flags) | ||
| break | ||
| } | ||
| const render = ({ response, duration, flags }) => { | ||
| const { headers, requestUrl, url: responseUrl, body } = response | ||
|
|
||
| if (flags.json) { | ||
| if (flags.copy) toClipboard(JSON.parse(response), flags) | ||
| if (!flags.pretty) return console.log(response) | ||
|
|
||
| return printText.json(JSON.parse(response), { color: true }) | ||
| } | ||
|
|
||
| const edgeCacheStatus = headers['cf-cache-status'] | ||
| const unifiedCacheStatus = headers['x-cache-status'] | ||
| const plainHeaders = toPlainHeaders(headers) | ||
|
|
||
| const bodyBuffer = Buffer.isBuffer(body) ? body : Buffer.from(body) | ||
| const bodyText = bodyBuffer.toString() | ||
|
|
||
| if (!flags.pretty) return console.log(bodyText) | ||
|
|
||
| const contentType = getContentType(plainHeaders['content-type']) | ||
| const time = Number.isFinite(duration) ? prettyMs(duration) : 'unknown' | ||
| const serverTiming = plainHeaders['server-timing'] | ||
| const id = plainHeaders['x-request-id'] | ||
|
|
||
| if (bodyText.startsWith('data:')) { | ||
| const extension = contentType | ||
| ? contentType.split('/')[1].split(';')[0] | ||
| : 'png' | ||
| const filepath = temp.file({ extension }) | ||
| fs.writeFileSync(filepath, bodyText.split(',')[1], 'base64') | ||
| printText.image(filepath) | ||
| } else if (contentType !== 'application/json') { | ||
| printText.image(bodyBuffer) | ||
| console.log() | ||
| } else { | ||
| const isText = contentType === 'text/plain' | ||
| const isHtml = contentType === 'text/html' | ||
| const output = isText || isHtml ? bodyText : JSON.parse(bodyText) | ||
| printText.json(output, flags) | ||
| } | ||
|
|
||
| const edgeCacheStatus = plainHeaders['cf-cache-status'] | ||
| const unifiedCacheStatus = plainHeaders['x-cache-status'] | ||
|
|
||
| const cacheStatus = | ||
| unifiedCacheStatus === 'MISS' && edgeCacheStatus === 'HIT' | ||
| ? edgeCacheStatus | ||
| : unifiedCacheStatus | ||
|
|
||
| const timestamp = Number(headers['x-timestamp']) | ||
| const ttl = Number(headers['x-cache-ttl']) | ||
| const timestamp = Number(plainHeaders['x-timestamp']) | ||
| const ttl = Number(plainHeaders['x-cache-ttl']) | ||
| const expires = timestamp + ttl - Date.now() | ||
| const expiration = prettyMs(expires) | ||
| const expiredAt = cacheStatus === 'HIT' ? `(${expiration})` : '' | ||
| const fetchMode = headers['x-fetch-mode'] | ||
| const fetchTime = fetchMode && `(${headers['x-fetch-time']})` | ||
| const size = Number(headers['content-length'] || Buffer.byteLength(body)) | ||
| const fetchMode = plainHeaders['x-fetch-mode'] | ||
| const fetchTime = fetchMode && `(${plainHeaders['x-fetch-time']})` | ||
| const size = Number(plainHeaders['content-length'] || bodyBuffer.length) | ||
| const uri = requestUrl || responseUrl | ||
|
|
||
| console.error() | ||
| console.error( | ||
| print.label('success', 'green'), | ||
| gray(`${print.bytes(size)} in ${time}`) | ||
| printText.label('success', 'green'), | ||
| gray(`${printText.bytes(size)} in ${time}`) | ||
| ) | ||
| console.error() | ||
|
|
||
| if (serverTiming) { | ||
| console.error(' ', print.keyValue(green('timing'), serverTiming)) | ||
| console.error(' ', printText.keyValue(green('timing'), serverTiming)) | ||
| } | ||
|
|
||
| if (cacheStatus) { | ||
| console.error( | ||
| ' ', | ||
| print.keyValue(green('cache'), `${cacheStatus} ${gray(expiredAt)}`) | ||
| printText.keyValue(green('cache'), `${cacheStatus} ${gray(expiredAt)}`) | ||
| ) | ||
| } | ||
|
|
||
| if (fetchMode) { | ||
| console.error( | ||
| ' ', | ||
| print.keyValue(green('mode'), `${fetchMode} ${gray(fetchTime)}`) | ||
| printText.keyValue(green('mode'), `${fetchMode} ${gray(fetchTime)}`) | ||
| ) | ||
| } | ||
|
|
||
| console.error(' ', print.keyValue(green('uri'), uri)) | ||
| console.error(' ', print.keyValue(green('id'), id)) | ||
| console.error(' ', printText.keyValue(green('uri'), uri)) | ||
| console.error(' ', printText.keyValue(green('id'), id)) | ||
|
|
||
| if (flags.copy) { | ||
| let copiedValue | ||
| try { | ||
| copiedValue = JSON.parse(body) | ||
| } catch (err) { | ||
| copiedValue = body | ||
| } | ||
| clipboardy.writeSync(JSON.stringify(copiedValue, null, 2)) | ||
| toClipboard(JSON.parse(bodyText), flags) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing try-catch crashes copy for non-JSON responsesHigh Severity
Reviewed by Cursor Bugbot for commit 3e95b41. Configure here. |
||
| console.error(`\n ${gray('Copied to clipboard!')}`) | ||
| } | ||
| } | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| 'use strict' | ||
|
|
||
| const { humanizeApiKey, toPlainHeaders, stringify } = require('./util') | ||
|
|
||
| module.exports = ({ | ||
| requestUrl, | ||
| requestOptions, | ||
| response, | ||
| full = false, | ||
| pretty = true | ||
| }) => { | ||
| const { responseType, ...requestOptionsWithoutResponseType } = requestOptions | ||
| const headers = { ...requestOptionsWithoutResponseType.headers } | ||
|
|
||
| if (!full && headers['x-api-key']) { | ||
| headers['x-api-key'] = humanizeApiKey(headers['x-api-key']) | ||
| } | ||
|
|
||
| return stringify( | ||
| { | ||
| request: { | ||
| url: requestUrl, | ||
| ...requestOptionsWithoutResponseType, | ||
| headers | ||
| }, | ||
| response: { | ||
| ...response, | ||
| headers: toPlainHeaders(response.headers) | ||
| } | ||
| }, | ||
| { pretty } | ||
| ) | ||
| } |
File renamed without changes.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copy with non-JSON body crashes without try-catch
Medium Severity
toClipboard(JSON.parse(bodyText), flags)will throw whenbodyTextisn't valid JSON (e.g., data URIs, image buffers, ortext/plainresponses) and--copyis used. The previous implementation wrappedJSON.parse(body)in a try-catch, falling back to the raw body on parse failure. That safety net was removed in this rewrite.Reviewed by Cursor Bugbot for commit a2ac8f5. Configure here.