Skip to content

Commit 9666748

Browse files
author
Pelle Wessman
authored
Merge pull request #7 from SocketDev/add-view-command
Add `socket report view` command + `--view` flag on `socket report create`
2 parents 9ca0f16 + 71f7a46 commit 9666748

File tree

7 files changed

+382
-88
lines changed

7 files changed

+382
-88
lines changed

README.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,21 @@ npm install -g @socketsecurity/cli
1515
```bash
1616
socket --help
1717
socket info [email protected]
18-
socket report create package.json
18+
socket report create package.json --view
19+
socket report view QXU8PmK7LfH608RAwfIKdbcHgwEd_ZeWJ9QEGv05FJUQ
1920
```
2021

2122
## Commands
2223

2324
* `socket info <package@version>` - looks up issues for a package
24-
* `socket report create` - uploads the specified `package.json` and/or `package-lock.json` to create a report on [socket.dev](https://socket.dev/). If only one of a `package.json`/`package-lock.json` has been specified, the other will be automatically found and uploaded if it exists
25+
* `socket report create <path(s)-to-folder-or-file>` - uploads the specified `package.json` and/or `package-lock.json` to create a report on [socket.dev](https://socket.dev/). If only one of a `package.json`/`package-lock.json` has been specified, the other will be automatically found and uploaded if it exists
26+
* `socket report view <report-id>` - looks up issues and scores from a report
2527

2628
## Flags
2729

28-
### Action flags
30+
### Command specific flags
2931

30-
* `--dry-run` - the `socket report create` supports running the command without actually uploading anything. All CLI tools that perform an action should have a dry run flag
32+
* `--view` - when set on `socket report create` the command will immediately do a `socket report view` style view of the created report, waiting for the server to complete it
3133

3234
### Output flags
3335

@@ -36,6 +38,7 @@ socket report create package.json
3638

3739
### Other flags
3840

41+
* `--dry-run` - like all CLI tools that perform an action should have, we have a dry run flag. Eg. `socket report create` supports running the command without actually uploading anything
3942
* `--debug` - outputs additional debug output. Great for debugging, geeks and us who develop. Hopefully you will never _need_ it, but it can still be fun, right?
4043
* `--help` - prints the help for the current command. All CLI tools should have this flag
4144
* `--version` - prints the version of the tool. All CLI tools should have this flag
@@ -45,6 +48,7 @@ socket report create package.json
4548
* `SOCKET_SECURITY_API_KEY` - if set, this will be used as the API-key
4649

4750
## Contributing
51+
4852
### Environment variables for development
4953

5054
* `SOCKET_SECURITY_API_BASE_URL` - if set, this will be the base for all API-calls. Defaults to `https://api.socket.dev/v0/`

lib/commands/info/index.js

Lines changed: 56 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,45 @@
33
import chalk from 'chalk'
44
import meow from 'meow'
55
import ora from 'ora'
6-
import { ErrorWithCause } from 'pony-cause'
76

7+
import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api-helpers.js'
88
import { ChalkOrMarkdown } from '../../utils/chalk-markdown.js'
9-
import { AuthError, InputError } from '../../utils/errors.js'
9+
import { InputError } from '../../utils/errors.js'
10+
import { getSeveritySummary } from '../../utils/format-issues.js'
1011
import { printFlagList } from '../../utils/formatting.js'
11-
import { stringJoinWithSeparateFinalSeparator } from '../../utils/misc.js'
1212
import { setupSdk } from '../../utils/sdk.js'
1313

14-
const description = 'Look up info regarding a package'
14+
/** @type {import('../../utils/meow-with-subcommands').CliSubcommand} */
15+
export const info = {
16+
description: 'Look up info regarding a package',
17+
async run (argv, importMeta, { parentName }) {
18+
const name = parentName + ' info'
19+
20+
const input = setupCommand(name, info.description, argv, importMeta)
21+
const result = input && await fetchPackageData(input.pkgName, input.pkgVersion)
22+
23+
if (result) {
24+
formatPackageDataOutput(result.data, { name, ...input })
25+
}
26+
}
27+
}
1528

16-
/** @type {import('../../utils/meow-with-subcommands').CliSubcommandRun} */
17-
const run = async (argv, importMeta, { parentName }) => {
18-
const name = parentName + ' info'
29+
// Internal functions
1930

31+
/**
32+
* @param {string} name
33+
* @param {string} description
34+
* @param {readonly string[]} argv
35+
* @param {ImportMeta} importMeta
36+
* @returns {void|{ outputJson: boolean, outputMarkdown: boolean, pkgName: string, pkgVersion: string }}
37+
*/
38+
function setupCommand (name, description, argv, importMeta) {
2039
const cli = meow(`
2140
Usage
2241
$ ${name} <name>
2342
2443
Options
2544
${printFlagList({
26-
'--debug': 'Output debug information',
2745
'--json': 'Output result as json',
2846
'--markdown': 'Output result as markdown',
2947
}, 6)}
@@ -36,11 +54,6 @@ const run = async (argv, importMeta, { parentName }) => {
3654
description,
3755
importMeta,
3856
flags: {
39-
debug: {
40-
type: 'boolean',
41-
alias: 'd',
42-
default: false,
43-
},
4457
json: {
4558
type: 'boolean',
4659
alias: 'j',
@@ -83,69 +96,56 @@ const run = async (argv, importMeta, { parentName }) => {
8396
throw new InputError('Need to specify a version, like eg: [email protected]')
8497
}
8598

86-
const socketSdk = await setupSdk()
99+
return {
100+
outputJson,
101+
outputMarkdown,
102+
pkgName,
103+
pkgVersion
104+
}
105+
}
87106

107+
/**
108+
* @param {string} pkgName
109+
* @param {string} pkgVersion
110+
* @returns {Promise<void|import('@socketsecurity/sdk').SocketSdkReturnType<'getIssuesByNPMPackage'>>}
111+
*/
112+
async function fetchPackageData (pkgName, pkgVersion) {
113+
const socketSdk = await setupSdk()
88114
const spinner = ora(`Looking up data for version ${pkgVersion} of ${pkgName}`).start()
89-
90-
/** @type {Awaited<ReturnType<import('@socketsecurity/sdk').SocketSdk["getIssuesByNPMPackage"]>>} */
91-
let result
92-
93-
try {
94-
result = await socketSdk.getIssuesByNPMPackage(pkgName, pkgVersion)
95-
} catch (cause) {
96-
spinner.fail()
97-
throw new ErrorWithCause('Failed to look up package', { cause })
98-
}
115+
const result = await handleApiCall(socketSdk.getIssuesByNPMPackage(pkgName, pkgVersion), spinner, 'looking up package')
99116

100117
if (result.success === false) {
101-
if (result.status === 401 || result.status === 403) {
102-
spinner.stop()
103-
throw new AuthError(result.error.message)
104-
}
105-
spinner.fail(chalk.white.bgRed('API returned an error:') + ' ' + result.error.message)
106-
process.exit(1)
118+
return handleUnsuccessfulApiResponse(result, spinner)
107119
}
108120

109-
const data = result.data
121+
// Conclude the status of the API call
110122

111-
/** @typedef {(typeof data)[number]["value"] extends infer U | undefined ? U : never} SocketSdkIssue */
112-
/** @type {Record<SocketSdkIssue["severity"], number>} */
113-
const severityCount = { low: 0, middle: 0, high: 0, critical: 0 }
114-
for (const issue of data) {
115-
const value = issue.value
116-
117-
if (!value) {
118-
continue
119-
}
120-
121-
if (severityCount[value.severity] !== undefined) {
122-
severityCount[value.severity] += 1
123-
}
124-
}
123+
const issueSummary = getSeveritySummary(result.data)
124+
spinner.succeed(`Found ${issueSummary || 'no'} issues for version ${pkgVersion} of ${pkgName}`)
125125

126-
const issueSummary = stringJoinWithSeparateFinalSeparator([
127-
severityCount.critical ? severityCount.critical + ' critical' : undefined,
128-
severityCount.high ? severityCount.high + ' high' : undefined,
129-
severityCount.middle ? severityCount.middle + ' middle' : undefined,
130-
severityCount.low ? severityCount.low + ' low' : undefined,
131-
])
126+
return result
127+
}
132128

133-
spinner.succeed(`Found ${issueSummary || 'no'} issues for version ${pkgVersion} of ${pkgName}`)
129+
/**
130+
* @param {import('@socketsecurity/sdk').SocketSdkReturnType<'getIssuesByNPMPackage'>["data"]} data
131+
* @param {{ name: string, outputJson: boolean, outputMarkdown: boolean, pkgName: string, pkgVersion: string }} context
132+
* @returns {void}
133+
*/
134+
function formatPackageDataOutput (data, { name, outputJson, outputMarkdown, pkgName, pkgVersion }) {
135+
// If JSON, output and return...
134136

135137
if (outputJson) {
136138
console.log(JSON.stringify(data, undefined, 2))
137139
return
138140
}
139141

142+
// ...else do the CLI / Markdown output dance
143+
140144
const format = new ChalkOrMarkdown(!!outputMarkdown)
141145
const url = `https://socket.dev/npm/package/${pkgName}/overview/${pkgVersion}`
142146

143147
console.log('\nDetailed info on socket.dev: ' + format.hyperlink(`${pkgName} v${pkgVersion}`, url, { fallbackToUrl: true }))
144-
145148
if (!outputMarkdown) {
146149
console.log(chalk.dim('\nOr rerun', chalk.italic(name), 'using the', chalk.italic('--json'), 'flag to get full JSON output'))
147150
}
148151
}
149-
150-
/** @type {import('../../utils/meow-with-subcommands').CliSubcommand} */
151-
export const info = { description, run }

lib/commands/report/create.js

Lines changed: 85 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,64 @@
33
import { stat } from 'node:fs/promises'
44
import path from 'node:path'
55

6-
import chalk from 'chalk'
76
import meow from 'meow'
87
import ora from 'ora'
98
import { ErrorWithCause } from 'pony-cause'
109

10+
import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api-helpers.js'
1111
import { ChalkOrMarkdown, logSymbols } from '../../utils/chalk-markdown.js'
12-
import { AuthError, InputError } from '../../utils/errors.js'
12+
import { InputError } from '../../utils/errors.js'
1313
import { printFlagList } from '../../utils/formatting.js'
1414
import { createDebugLogger } from '../../utils/misc.js'
1515
import { setupSdk } from '../../utils/sdk.js'
1616
import { isErrnoException } from '../../utils/type-helpers.js'
17+
import { fetchReportData, formatReportDataOutput } from './view.js'
1718

18-
const description = 'Create a project report'
19+
/** @type {import('../../utils/meow-with-subcommands').CliSubcommand} */
20+
export const create = {
21+
description: 'Create a project report',
22+
async run (argv, importMeta, { parentName }) {
23+
const name = parentName + ' create'
24+
25+
const input = await setupCommand(name, create.description, argv, importMeta)
26+
27+
if (input) {
28+
const {
29+
cwd,
30+
debugLog,
31+
dryRun,
32+
outputJson,
33+
outputMarkdown,
34+
packagePaths,
35+
view,
36+
} = input
37+
38+
const result = input && await createReport(packagePaths, { cwd, debugLog, dryRun })
39+
40+
if (result && view) {
41+
const reportId = result.data.id
42+
const reportResult = input && await fetchReportData(reportId)
43+
44+
if (reportResult) {
45+
formatReportDataOutput(reportResult.data, { name, outputJson, outputMarkdown, reportId })
46+
}
47+
} else if (result) {
48+
formatReportCreationOutput(result.data, { outputJson, outputMarkdown })
49+
}
50+
}
51+
}
52+
}
1953

20-
/** @type {import('../../utils/meow-with-subcommands').CliSubcommandRun} */
21-
const run = async (argv, importMeta, { parentName }) => {
22-
const name = parentName + ' create'
54+
// Internal functions
2355

56+
/**
57+
* @param {string} name
58+
* @param {string} description
59+
* @param {readonly string[]} argv
60+
* @param {ImportMeta} importMeta
61+
* @returns {Promise<void|{ cwd: string, debugLog: typeof console.error, dryRun: boolean, outputJson: boolean, outputMarkdown: boolean, packagePaths: string[], view: boolean }>}
62+
*/
63+
async function setupCommand (name, description, argv, importMeta) {
2464
const cli = meow(`
2565
Usage
2666
$ ${name} <paths-to-package-folders-and-files>
@@ -31,6 +71,7 @@ const run = async (argv, importMeta, { parentName }) => {
3171
'--dry-run': 'Only output what will be done without actually doing it',
3272
'--json': 'Output result as json',
3373
'--markdown': 'Output result as markdown',
74+
'--view': 'Will wait for and return the created report'
3475
}, 6)}
3576
3677
Examples
@@ -61,13 +102,19 @@ const run = async (argv, importMeta, { parentName }) => {
61102
alias: 'm',
62103
default: false,
63104
},
105+
view: {
106+
type: 'boolean',
107+
alias: 'v',
108+
default: false,
109+
},
64110
}
65111
})
66112

67113
const {
68114
dryRun,
69115
json: outputJson,
70116
markdown: outputMarkdown,
117+
view,
71118
} = cli.flags
72119

73120
if (!cli.input[0]) {
@@ -80,50 +127,60 @@ const run = async (argv, importMeta, { parentName }) => {
80127
const cwd = process.cwd()
81128
const packagePaths = await resolvePackagePaths(cwd, cli.input)
82129

130+
return {
131+
cwd,
132+
debugLog,
133+
dryRun,
134+
outputJson,
135+
outputMarkdown,
136+
packagePaths,
137+
view,
138+
}
139+
}
140+
141+
/**
142+
* @param {string[]} packagePaths
143+
* @param {{ cwd: string, debugLog: typeof console.error, dryRun: boolean }} context
144+
* @returns {Promise<void|import('@socketsecurity/sdk').SocketSdkReturnType<'createReport'>>}
145+
*/
146+
async function createReport (packagePaths, { cwd, debugLog, dryRun }) {
83147
debugLog(`${logSymbols.info} Uploading:`, packagePaths.join(`\n${logSymbols.info} Uploading:`))
84148

85149
if (dryRun) {
86150
return
87151
}
88152

89153
const socketSdk = await setupSdk()
90-
91154
const spinner = ora(`Creating report with ${packagePaths.length} package files`).start()
92-
93-
/** @type {Awaited<ReturnType<typeof socketSdk.createReportFromFilePaths>>} */
94-
let result
95-
96-
try {
97-
result = await socketSdk.createReportFromFilePaths(packagePaths, cwd)
98-
} catch (cause) {
99-
spinner.fail()
100-
throw new ErrorWithCause('Failed creating report', { cause })
101-
}
155+
const result = await handleApiCall(socketSdk.createReportFromFilePaths(packagePaths, cwd), spinner, 'creating report')
102156

103157
if (result.success === false) {
104-
if (result.status === 401 || result.status === 403) {
105-
spinner.stop()
106-
throw new AuthError(result.error.message)
107-
}
108-
spinner.fail(chalk.white.bgRed('API returned an error:') + ' ' + result.error.message)
109-
process.exit(1)
158+
return handleUnsuccessfulApiResponse(result, spinner)
110159
}
111160

161+
// Conclude the status of the API call
162+
112163
spinner.succeed()
113164

165+
return result
166+
}
167+
168+
/**
169+
* @param {import('@socketsecurity/sdk').SocketSdkReturnType<'createReport'>["data"]} data
170+
* @param {{ outputJson: boolean, outputMarkdown: boolean }} context
171+
* @returns {void}
172+
*/
173+
function formatReportCreationOutput (data, { outputJson, outputMarkdown }) {
114174
if (outputJson) {
115-
console.log(JSON.stringify(result.data, undefined, 2))
175+
console.log(JSON.stringify(data, undefined, 2))
116176
return
117177
}
118178

119179
const format = new ChalkOrMarkdown(!!outputMarkdown)
120180

121-
console.log('\nNew report: ' + format.hyperlink(result.data.id, result.data.url, { fallbackToUrl: true }))
181+
console.log('\nNew report: ' + format.hyperlink(data.id, data.url, { fallbackToUrl: true }))
122182
}
123183

124-
/** @type {import('../../utils/meow-with-subcommands').CliSubcommand} */
125-
export const create = { description, run }
126-
127184
// TODO: Add globbing support with support for ignoring, as a "./**/package.json" in a project also traverses eg. node_modules
128185
/**
129186
* Takes paths to folders and/or package.json / package-lock.json files and resolves to package.json + package-lock.json pairs (where feasible)

0 commit comments

Comments
 (0)