Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 28 additions & 5 deletions cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}
69 changes: 52 additions & 17 deletions lib/commands/report/create.js
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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} <path-to-lock-file>
$ ${name} <path-to-package-json>

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<ReturnType<typeof socketSdk.createReportFromFilePaths>>} */
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} */
Expand Down
125 changes: 125 additions & 0 deletions lib/utils/chalk-markdown.js
Original file line number Diff line number Diff line change
@@ -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)
}
}
2 changes: 2 additions & 0 deletions lib/utils/errors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export class AuthError extends Error {}
export class InputError extends Error {}
6 changes: 3 additions & 3 deletions lib/utils/sdk.js
Original file line number Diff line number Diff line change
@@ -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 () {
Expand All @@ -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"]} */
Expand All @@ -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)
}
3 changes: 2 additions & 1 deletion lib/utils/socket-sdk.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export class SocketSdk {
/**
* @param {string} apiKey
* @param {SocketSdkOptions} options
* @throws {SocketSdkAuthError}
*/
constructor (apiKey, options = {}) {
const {
Expand All @@ -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,
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}