diff --git a/cli.js b/cli.js index 938123632..e9913d6f3 100644 --- a/cli.js +++ b/cli.js @@ -5,6 +5,8 @@ import chalk from 'chalk' import { messageWithCauses, stackWithCauses } from 'pony-cause' import * as cliCommands from './lib/commands/index.js' +import { logSymbols } from './lib/utils/chalk-markdown.js' +import { AuthError, InputError } from './lib/utils/errors.js' import { meowWithSubcommands } from './lib/utils/meow-with-subcommands.js' // TODO: Add autocompletion using https://www.npmjs.com/package/omelette @@ -19,10 +21,31 @@ try { } ) } catch (err) { - console.error( - chalk.bgRed('Unexpected error:') + - (err instanceof Error ? ' ' + messageWithCauses(err) + '\n\n' + stackWithCauses(err) : '') + - '\n' - ) + /** @type {string} */ + let errorTitle + /** @type {string} */ + let errorMessage = '' + /** @type {string|undefined} */ + let errorBody + + if (err instanceof AuthError) { + errorTitle = 'Authentication error' + errorMessage = err.message + } else if (err instanceof InputError) { + errorTitle = 'Invalid input' + errorMessage = err.message + } else if (err instanceof Error) { + errorTitle = 'Unexpected error' + errorMessage = messageWithCauses(err) + errorBody = stackWithCauses(err) + } else { + errorTitle = 'Unexpected error with no details' + } + + console.error(`${logSymbols.error} ${chalk.white.bgRed(errorTitle + ':')} ${errorMessage}`) + if (errorBody) { + console.error('\n' + errorBody) + } + process.exit(1) } diff --git a/lib/commands/report/create.js b/lib/commands/report/create.js index fcdd9fe7c..07011e829 100644 --- a/lib/commands/report/create.js +++ b/lib/commands/report/create.js @@ -1,5 +1,11 @@ +/* eslint-disable no-console */ +import chalk from 'chalk' import meow from 'meow' +import ora from 'ora' +import { ErrorWithCause } from 'pony-cause' +import { ChalkOrMarkdown } from '../../utils/chalk-markdown.js' +import { AuthError } from '../../utils/errors.js' import { printFlagList } from '../../utils/formatting.js' import { setupSdk } from '../../utils/sdk.js' @@ -9,46 +15,75 @@ const description = 'Create a project report' const run = async (argv, importMeta, { parentName }) => { const name = parentName + ' create' - // ...else provide basic instructions and help const cli = meow(` Usage - $ ${name} + $ ${name} Options ${printFlagList({ - // FIXME: Remove rainbow - '--rainbow': 'Foobar' + '--json': 'Output result as json', + '--markdown': 'Output result as markdown', }, 6)} Examples - $ ${name} --help + $ ${name} . `, { argv, description, importMeta, + // TODO: Add a --verbose? flags: { - // FIXME: Remove rainbow - rainbow: { + json: { type: 'boolean', - alias: 'r' - } + alias: 'j', + }, + markdown: { + type: 'boolean', + alias: 'm', + }, } }) - // TODO: Remove - console.log('LETS CREATE!', cli.flags, cli.input) + const { + json: outputJson, + markdown: outputMarkdown, + } = cli.flags const socketSdk = await setupSdk() + const spinner = ora('Creating report').start() + + /** @type {Awaited>} */ + let result + try { // FIXME: Take files from input? - const result = await socketSdk.createReportFromFilePaths(['package.json']) - // TODO: Remove - console.log('RESULT!', result) - } catch (err) { - // TODO: Remove - console.log('err!', err) + result = await socketSdk.createReportFromFilePaths(['package.json']) + } catch (cause) { + spinner.fail() + throw new ErrorWithCause('Failed creating report', { cause }) + } + + if (result.success === false) { + if (result.status === 401 || result.status === 403) { + spinner.stop() + throw new AuthError(result.error.message) + } + spinner.fail(chalk.white.bgRed('API returned an error:') + ' ' + result.error.message) + process.exit(1) } + + spinner.stop() + + if (outputJson) { + console.log(JSON.stringify(result.data, undefined, 2)) + return + } + + const format = new ChalkOrMarkdown(!!outputMarkdown) + + console.log(format.header(format.logSymbols.success + ' Report created')) + console.log('New report: ' + format.hyperlink(result.data.id, result.data.url, { fallbackToUrl: true })) } /** @type {import('../../utils/meow-with-subcommands').CliSubcommand} */ diff --git a/lib/utils/chalk-markdown.js b/lib/utils/chalk-markdown.js new file mode 100644 index 000000000..3dbd7faef --- /dev/null +++ b/lib/utils/chalk-markdown.js @@ -0,0 +1,125 @@ +import chalk from 'chalk' +import isUnicodeSupported from 'is-unicode-supported' +import terminalLink from 'terminal-link' + +// From the 'log-symbols' module +const unicodeLogSymbols = { + info: chalk.blue('ℹ'), + success: chalk.green('✔'), + warning: chalk.yellow('⚠'), + error: chalk.red('✖'), +} + +// From the 'log-symbols' module +const fallbackLogSymbols = { + info: chalk.blue('i'), + success: chalk.green('√'), + warning: chalk.yellow('‼'), + error: chalk.red('×'), +} + +// From the 'log-symbols' module +export const logSymbols = isUnicodeSupported() ? unicodeLogSymbols : fallbackLogSymbols + +const markdownLogSymbols = { + info: ':information_source:', + error: ':stop_sign:', + success: ':white_check_mark:', + warning: ':warning:', +} + +export class ChalkOrMarkdown { + /** @type {boolean} */ + useMarkdown + + /** + * @param {boolean} useMarkdown + */ + constructor (useMarkdown) { + this.useMarkdown = !!useMarkdown + } + + /** + * @param {string} text + * @param {number} [level] + * @returns {string} + */ + header (text, level = 1) { + return this.useMarkdown + ? `\n${''.padStart(level, '#')} ${text}\n` + : chalk.underline(`\n${level === 1 ? chalk.bold(text) : text}\n`) + } + + /** + * @param {string} text + * @returns {string} + */ + bold (text) { + return this.useMarkdown + ? `**${text}**` + : chalk.bold(`${text}`) + } + + /** + * @param {string} text + * @returns {string} + */ + italic (text) { + return this.useMarkdown + ? `_${text}_` + : chalk.italic(`${text}`) + } + + /** + * @param {string} text + * @param {string|undefined} url + * @param {{ fallback?: boolean, fallbackToUrl?: boolean }} options + * @returns {string} + */ + hyperlink (text, url, { fallback = true, fallbackToUrl } = {}) { + if (!url) return text + return this.useMarkdown + ? `[${text}](${url})` + : terminalLink(text, url, { + fallback: fallbackToUrl ? (_text, url) => url : fallback + }) + } + + /** + * @param {string[]} items + * @returns {string} + */ + list (items) { + const indentedContent = items.map(item => this.indent(item).trimStart()) + return this.useMarkdown + ? '* ' + indentedContent.join('\n* ') + '\n' + : indentedContent.join('\n') + '\n' + } + + /** + * @returns {typeof logSymbols} + */ + get logSymbols () { + return this.useMarkdown ? markdownLogSymbols : logSymbols + } + + /** + * @param {string} text + * @param {number} [level] + * @returns {string} + */ + indent (text, level = 1) { + const indent = ''.padStart(level * 2, ' ') + return indent + text.split('\n').join('\n' + indent) + } + + /** + * @param {unknown} value + * @returns {string} + */ + json (value) { + return this.useMarkdown + ? '```json\n' + JSON.stringify(value) + '\n```' + : JSON.stringify(value) + } +} diff --git a/lib/utils/errors.js b/lib/utils/errors.js new file mode 100644 index 000000000..728be91f7 --- /dev/null +++ b/lib/utils/errors.js @@ -0,0 +1,2 @@ +export class AuthError extends Error {} +export class InputError extends Error {} diff --git a/lib/utils/sdk.js b/lib/utils/sdk.js index 14f689be0..d4fdb3545 100644 --- a/lib/utils/sdk.js +++ b/lib/utils/sdk.js @@ -1,6 +1,7 @@ import isInteractive from 'is-interactive' import prompts from 'prompts' +import { AuthError } from './errors.js' import { SocketSdk } from './socket-sdk.js' export async function setupSdk () { @@ -17,8 +18,7 @@ export async function setupSdk () { } if (!apiKey) { - // FIXME: Throw a detailed error that is properly shown - throw new Error('Missing API-key') + throw new AuthError('You need to provide an API key') } /** @type {import('./socket-sdk').SocketSdkOptions["agent"]} */ @@ -38,5 +38,5 @@ export async function setupSdk () { baseUrl: process.env['SOCKET_SECURITY_API_BASE_URL'], } - return new SocketSdk(apiKey, sdkOptions) + return new SocketSdk(apiKey || '', sdkOptions) } diff --git a/lib/utils/socket-sdk.js b/lib/utils/socket-sdk.js index d250fa444..bd8835f61 100644 --- a/lib/utils/socket-sdk.js +++ b/lib/utils/socket-sdk.js @@ -33,6 +33,7 @@ export class SocketSdk { /** * @param {string} apiKey * @param {SocketSdkOptions} options + * @throws {SocketSdkAuthError} */ constructor (apiKey, options = {}) { const { @@ -42,7 +43,7 @@ export class SocketSdk { // FIXME: Handle rate limit! Seems like got is handling that now? // TODO: Add timeout - // TODO: Add debug() + // TODO: Add debug() and/or take a logging function this.#client = got.extend({ prefixUrl: baseUrl, diff --git a/package.json b/package.json index 8b3e5a153..caaff1a1f 100644 --- a/package.json +++ b/package.json @@ -74,8 +74,11 @@ "got": "^12.5.2", "hpagent": "^1.2.0", "is-interactive": "^2.0.0", + "is-unicode-supported": "^1.3.0", "meow": "^11.0.0", + "ora": "^6.1.2", "pony-cause": "^2.1.4", - "prompts": "^2.4.2" + "prompts": "^2.4.2", + "terminal-link": "^3.0.0" } }