From 44ca22c7a6033fc8d36d24bab1e895c3a772fb0b Mon Sep 17 00:00:00 2001 From: Peter van der Zee Date: Fri, 28 Feb 2025 12:12:31 +0100 Subject: [PATCH] Fix organization cmd, support json/md flags --- src/cli.ts | 4 +- src/commands/organization/cmd-organization.ts | 63 ++++++++++++ src/commands/organization/get-organization.ts | 98 +++++++++++++++++++ .../organizations/cmd-organizations.ts | 40 -------- .../organizations/get-organizations.ts | 40 -------- src/utils/meow-with-subcommands.ts | 1 + test/dry-run.test.ts | 19 ++++ 7 files changed, 183 insertions(+), 82 deletions(-) create mode 100644 src/commands/organization/cmd-organization.ts create mode 100644 src/commands/organization/get-organization.ts delete mode 100644 src/commands/organizations/cmd-organizations.ts delete mode 100644 src/commands/organizations/get-organizations.ts diff --git a/src/cli.ts b/src/cli.ts index a4d8fa4d4..195721cb0 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -22,7 +22,7 @@ import { cmdNpm } from './commands/npm/cmd-npm' import { cmdNpx } from './commands/npx/cmd-npx' import { cmdOops } from './commands/oops/cmd-oops' import { cmdOptimize } from './commands/optimize/cmd-optimize' -import { cmdOrganizations } from './commands/organizations/cmd-organizations' +import { cmdOrganization } from './commands/organization/cmd-organization' import { cmdRawNpm } from './commands/raw-npm/cmd-raw-npm' import { cmdRawNpx } from './commands/raw-npx/cmd-raw-npx' import { cmdReport } from './commands/report/cmd-report' @@ -58,7 +58,7 @@ void (async () => { npx: cmdNpx, oops: cmdOops, optimize: cmdOptimize, - organization: cmdOrganizations, + organization: cmdOrganization, 'raw-npm': cmdRawNpm, 'raw-npx': cmdRawNpx, report: cmdReport, diff --git a/src/commands/organization/cmd-organization.ts b/src/commands/organization/cmd-organization.ts new file mode 100644 index 000000000..eec64f1fe --- /dev/null +++ b/src/commands/organization/cmd-organization.ts @@ -0,0 +1,63 @@ +import { getOrganization } from './get-organization.ts' +import { meowOrExit } from '../../utils/meow-with-subcommands' +import { getFlagListOutput } from '../../utils/output-formatting' + +import type { CliCommandConfig } from '../../utils/meow-with-subcommands' +import { commonFlags, outputFlags } from '../../flags.ts' +import colors from 'yoctocolors-cjs' + +const config: CliCommandConfig = { + commandName: 'organizations', + description: 'List organizations associated with the API key used', + hidden: false, + flags: { + ...commonFlags, + ...outputFlags + }, + help: (command, _config) => ` + Usage + $ ${command} + + Options + ${getFlagListOutput(config.flags, 6)} + ` +} + +export const cmdOrganization = { + description: config.description, + hidden: config.hidden, + run +} + +async function run( + argv: readonly string[], + importMeta: ImportMeta, + { parentName }: { parentName: string } +): Promise { + const cli = meowOrExit({ + argv, + config, + importMeta, + parentName + }) + + const json = Boolean(cli.flags['json']) + const markdown = Boolean(cli.flags['markdown']) + if (json && markdown) { + // Use exit status of 2 to indicate incorrect usage, generally invalid + // options or missing arguments. + // https://www.gnu.org/software/bash/manual/html_node/Exit-Status.html + process.exitCode = 2 + console.error(` + ${colors.bgRed(colors.white('Input error'))}: Please provide the required fields:\n + - The json and markdown flags cannot be both set, pick one + `) + return + } + + if (cli.flags['dryRun']) { + return console.log('[DryRun] Bailing now') + } + + await getOrganization(json ? 'json' : markdown ? 'markdown' : 'text') +} diff --git a/src/commands/organization/get-organization.ts b/src/commands/organization/get-organization.ts new file mode 100644 index 000000000..8c47433a0 --- /dev/null +++ b/src/commands/organization/get-organization.ts @@ -0,0 +1,98 @@ +import colors from 'yoctocolors-cjs' + +import { Spinner } from '@socketsecurity/registry/lib/spinner' + +import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api' +import { AuthError } from '../../utils/errors' +import { getDefaultToken, setupSdk } from '../../utils/sdk' + +export async function getOrganization( + format: 'text' | 'json' | 'markdown' = 'text' +): Promise { + const apiToken = getDefaultToken() + if (!apiToken) { + throw new AuthError( + 'User must be authenticated to run this command. To log in, run the command `socket login` and enter your API key.' + ) + } + + await printOrganizationsFromToken(apiToken, format) +} + +async function printOrganizationsFromToken( + apiToken: string, + format: 'text' | 'json' | 'markdown' = 'text' +) { + const spinner = new Spinner({ text: 'Fetching organizations...' }).start() + const socketSdk = await setupSdk(apiToken) + const result = await handleApiCall( + socketSdk.getOrganizations(), + 'looking up organizations' + ) + + if (result.success === false) { + handleUnsuccessfulApiResponse('getOrganizations', result, spinner) + return + } + + spinner.stop('') + + const organizations = Object.values(result.data.organizations) + + if (format === 'json') { + const obj = Array.from(organizations).map(o => ({ + name: o.name, + id: o.id, + plan: o.plan + })) + console.log(JSON.stringify(obj, null, 2)) + return + } + + if (format === 'markdown') { + // | Syntax | Description | + // | ----------- | ----------- | + // | Header | Title | + // | Paragraph | Text | + + let mw1 = 4 + let mw2 = 2 + let mw3 = 4 + for (const o of organizations) { + mw1 = Math.max(mw1, (o?.name || '').length) + mw2 = Math.max(mw2, (o?.id || '').length) + mw3 = Math.max(mw3, (o?.plan || '').length) + } + + console.log('# Organizations\n') + console.log( + `List of organizations associated with your API key, ending with: ${colors.italic(apiToken.slice(-10, -4))}\n` + ) + console.log( + `| Name${' '.repeat(mw1 - 4)} | ID${' '.repeat(mw2 - 2)} | Plan${' '.repeat(mw3 - 4)} |` + ) + console.log( + `| ${'-'.repeat(mw1)} | ${'-'.repeat(mw2)} | ${'-'.repeat(mw3)} |` + ) + for (const o of organizations) { + console.log( + `| ${(o?.name || '').padEnd(mw1, ' ')} | ${(o?.id || '').padEnd(mw2, ' ')} | ${(o?.plan || '').padEnd(mw3, ' ')} |` + ) + } + console.log( + `| ${'-'.repeat(mw1)} | ${'-'.repeat(mw2)} | ${'-'.repeat(mw3)} |` + ) + return + } + + console.log( + `List of organizations associated with your API key, ending with: ${colors.italic(apiToken.slice(-10, -4))}\n` + ) + + // Just dump + for (const o of organizations) { + console.log( + `- Name: ${colors.bold(o?.name)}, ID: ${colors.bold(o?.id)}, Plan: ${colors.bold(o?.plan)}` + ) + } +} diff --git a/src/commands/organizations/cmd-organizations.ts b/src/commands/organizations/cmd-organizations.ts deleted file mode 100644 index b6cfa59ea..000000000 --- a/src/commands/organizations/cmd-organizations.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { getOrganizations } from './get-organizations' -import { meowOrExit } from '../../utils/meow-with-subcommands' - -import type { CliCommandConfig } from '../../utils/meow-with-subcommands' - -const config: CliCommandConfig = { - commandName: 'organizations', - description: 'List organizations associated with the API key used', - hidden: false, - flags: {}, - help: (command, _config) => ` - Usage - $ ${command} - ` -} - -export const cmdOrganizations = { - description: config.description, - hidden: config.hidden, - run -} - -async function run( - argv: readonly string[], - importMeta: ImportMeta, - { parentName }: { parentName: string } -): Promise { - const cli = meowOrExit({ - argv, - config, - importMeta, - parentName - }) - - if (cli.flags['dryRun']) { - return console.log('[DryRun] Bailing now') - } - - await getOrganizations() -} diff --git a/src/commands/organizations/get-organizations.ts b/src/commands/organizations/get-organizations.ts deleted file mode 100644 index 494c4bc52..000000000 --- a/src/commands/organizations/get-organizations.ts +++ /dev/null @@ -1,40 +0,0 @@ -import colors from 'yoctocolors-cjs' - -import { Spinner } from '@socketsecurity/registry/lib/spinner' - -import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api' -import { AuthError } from '../../utils/errors' -import { getDefaultToken, setupSdk } from '../../utils/sdk' - -export async function getOrganizations(): Promise { - const apiToken = getDefaultToken() - if (!apiToken) { - throw new AuthError( - 'User must be authenticated to run this command. To log in, run the command `socket login` and enter your API key.' - ) - } - const spinner = new Spinner({ text: 'Fetching organizations...' }).start() - const socketSdk = await setupSdk(apiToken) - const result = await handleApiCall( - socketSdk.getOrganizations(), - 'looking up organizations' - ) - - if (result.success === false) { - handleUnsuccessfulApiResponse('getOrganizations', result, spinner) - return - } - - spinner.stop( - `List of organizations associated with your API key: ${colors.italic(apiToken)}` - ) - - const organizations = Object.values(result.data.organizations) - for (const o of organizations) { - console.log(` -Name: ${o?.name} -ID: ${o?.id} -Plan: ${o?.plan} - `) - } -} diff --git a/src/utils/meow-with-subcommands.ts b/src/utils/meow-with-subcommands.ts index b070e6a3d..ffef491f4 100644 --- a/src/utils/meow-with-subcommands.ts +++ b/src/utils/meow-with-subcommands.ts @@ -172,6 +172,7 @@ export function meowOrExit({ if (constants.ENV[SOCKET_CLI_SHOW_BANNER]) { console.log(getAsciiHeader(command)) } + // This exits if .printHelp() is called either by meow itself or by us. const cli = meow({ argv, diff --git a/test/dry-run.test.ts b/test/dry-run.test.ts index 7d3649c23..760b9e5c0 100644 --- a/test/dry-run.test.ts +++ b/test/dry-run.test.ts @@ -507,6 +507,25 @@ describe('dry-run on all commands', async () => { ) }) + cmdit(['organization', '--dry-run'], 'should support', async cmd => { + const { code, status, stderr, stdout } = await invoke(...cmd) + expect(`\n ${stdout}`).toMatchInlineSnapshot(` + " + _____ _ _ /--------------- + | __|___ ___| |_ ___| |_ | Socket.dev CLI ver + |__ | . | _| '_| -_| _| | Node: , API token set: + |_____|___|___|_,_|___|_|.dev | Command: \`socket organizations\`, cwd: + + [DryRun] Bailing now" + `) + expect(stderr).toMatchInlineSnapshot(`""`) + + expect(code, 'dry-run should exit with code 0 if input is ok').toBe(0) + expect(stdout, 'header should include command (without params)').toContain( + cmd.slice(0, cmd.indexOf('--dry-run')).join(' ') + ) + }) + cmdit(['raw-npm', '--dry-run'], 'should support', async cmd => { const { code, status, stderr, stdout } = await invoke(...cmd) expect(`\n ${stdout}`).toMatchInlineSnapshot(`