diff --git a/lib/commands/index.js b/lib/commands/index.js index 0bdadb52..ade80f5c 100644 --- a/lib/commands/index.js +++ b/lib/commands/index.js @@ -10,3 +10,4 @@ export * from './report/index.js' export * from './wrapper/index.js' export * from './scan/index.js' export * from './audit-log/index.js' +export * from './repos/index.js' diff --git a/lib/commands/repos/create.js b/lib/commands/repos/create.js new file mode 100644 index 00000000..5390495d --- /dev/null +++ b/lib/commands/repos/create.js @@ -0,0 +1,166 @@ +/* eslint-disable no-console */ + +import chalk from 'chalk' +import meow from 'meow' +import ora from 'ora' + +import { outputFlags } from '../../flags/index.js' +import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api-helpers.js' +import { prepareFlags } from '../../utils/flags.js' +import { printFlagList } from '../../utils/formatting.js' +import { getDefaultKey, setupSdk } from '../../utils/sdk.js' + +/** @type {import('../../utils/meow-with-subcommands.js').CliSubcommand} */ +export const create = { + description: 'Create a repository in an organization', + async run (argv, importMeta, { parentName }) { + const name = parentName + ' create' + + const input = setupCommand(name, create.description, argv, importMeta) + if (input) { + const spinnerText = 'Creating repository... \n' + const spinner = ora(spinnerText).start() + await createRepo(input.orgSlug, input, spinner) + } + } +} + +const repositoryCreationFlags = prepareFlags({ + repoName: { + type: 'string', + shortFlag: 'n', + default: '', + description: 'Repository name', + }, + repoDescription: { + type: 'string', + shortFlag: 'd', + default: '', + description: 'Repository description', + }, + homepage: { + type: 'string', + shortFlag: 'h', + default: '', + description: 'Repository url', + }, + defaultBranch: { + type: 'string', + shortFlag: 'b', + default: 'main', + description: 'Repository default branch', + }, + visibility: { + type: 'string', + shortFlag: 'v', + default: 'private', + description: 'Repository visibility (Default Private)', + } +}) + +// Internal functions + +/** + * @typedef CommandContext + * @property {boolean} outputJson + * @property {boolean} outputMarkdown + * @property {string} orgSlug + * @property {string} name + * @property {string} description + * @property {string} homepage + * @property {string} default_branch + * @property {string} visibility + */ + +/** + * @param {string} name + * @param {string} description + * @param {readonly string[]} argv + * @param {ImportMeta} importMeta + * @returns {void|CommandContext} + */ +function setupCommand (name, description, argv, importMeta) { + const flags = { + ...outputFlags, + ...repositoryCreationFlags + } + + const cli = meow(` + Usage + $ ${name} + + Options + ${printFlagList(flags, 6)} + + Examples + $ ${name} FakeOrg --repoName=test-repo + `, { + argv, + description, + importMeta, + flags + }) + + const { + json: outputJson, + markdown: outputMarkdown, + repoName, + repoDescription, + homepage, + defaultBranch, + visibility + } = cli.flags + + const [orgSlug = ''] = cli.input + + if (!orgSlug) { + console.error(`${chalk.bgRed('Input error')}: Please provide an organization slug \n`) + cli.showHelp() + return + } + + if (!repoName) { + console.error(`${chalk.bgRed('Input error')}: Repository name is required. \n`) + cli.showHelp() + return + } + + return { + outputJson, + outputMarkdown, + orgSlug, + name: repoName, + description: repoDescription, + homepage, + default_branch: defaultBranch, + visibility + } +} + +/** + * @typedef RepositoryData + * @property {import('@socketsecurity/sdk').SocketSdkReturnType<'createOrgRepo'>["data"]} data + */ + +/** + * @param {string} orgSlug + * @param {CommandContext} input + * @param {import('ora').Ora} spinner + * @returns {Promise} + */ +async function createRepo (orgSlug, input, spinner) { + const socketSdk = await setupSdk(getDefaultKey()) + const result = await handleApiCall(socketSdk.createOrgRepo(orgSlug, input), 'creating repository') + + if (!result.success) { + return handleUnsuccessfulApiResponse('createOrgRepo', result, spinner) + } + + spinner.stop() + + console.log('\n✅ Repository created successfully \n') + + return { + data: result.data + } +} diff --git a/lib/commands/repos/delete.js b/lib/commands/repos/delete.js new file mode 100644 index 00000000..50339804 --- /dev/null +++ b/lib/commands/repos/delete.js @@ -0,0 +1,93 @@ +/* eslint-disable no-console */ + +import chalk from 'chalk' +import meow from 'meow' +import ora from 'ora' + +import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api-helpers.js' +import { getDefaultKey, setupSdk } from '../../utils/sdk.js' + +/** @type {import('../../utils/meow-with-subcommands.js').CliSubcommand} */ +export const del = { + description: 'Delete a repository in an organization', + async run (argv, importMeta, { parentName }) { + const name = parentName + ' del' + + const input = setupCommand(name, del.description, argv, importMeta) + if (input) { + const spinnerText = 'Deleting repository... \n' + const spinner = ora(spinnerText).start() + await deleteRepository(input.orgSlug, input.repoName, spinner) + } + } +} + +// Internal functions + +/** + * @typedef CommandContext + * @property {string} orgSlug + * @property {string} repoName + */ + +/** + * @param {string} name + * @param {string} description + * @param {readonly string[]} argv + * @param {ImportMeta} importMeta + * @returns {void|CommandContext} + */ +function setupCommand (name, description, argv, importMeta) { + const cli = meow(` + Usage + $ ${name} + + Examples + $ ${name} FakeOrg test-repo + `, { + argv, + description, + importMeta + }) + + const [orgSlug = '', repoName = ''] = cli.input + + if (!orgSlug || !repoName) { + console.error(`${chalk.bgRed('Input error')}: Please provide an organization slug and repository slug \n`) + cli.showHelp() + return + } + + return { + orgSlug, + repoName + } +} + +/** + * @typedef RepositoryData + * @property {import('@socketsecurity/sdk').SocketSdkReturnType<'deleteOrgRepo'>["data"]} data + */ + +/** + * @param {string} orgSlug + * @param {string} repoName + * @param {import('ora').Ora} spinner + * @returns {Promise} + */ +async function deleteRepository (orgSlug, repoName, spinner) { + const socketSdk = await setupSdk(getDefaultKey()) + const result = await handleApiCall(socketSdk.deleteOrgRepo(orgSlug, repoName), 'deleting repository') + + if (!result.success) { + return handleUnsuccessfulApiResponse('deleteOrgRepo', result, spinner) + } + + spinner.stop() + + console.log('\n✅ Repository deleted successfully \n') + + return { + data: result.data + } +} diff --git a/lib/commands/repos/index.js b/lib/commands/repos/index.js new file mode 100644 index 00000000..719b0b6c --- /dev/null +++ b/lib/commands/repos/index.js @@ -0,0 +1,30 @@ +import { create } from './create.js' +import { del } from './delete.js' +import { list } from './list.js' +import { update } from './update.js' +import { view } from './view.js' +import { meowWithSubcommands } from '../../utils/meow-with-subcommands.js' + +const description = 'Repositories related commands' + +/** @type {import('../../utils/meow-with-subcommands.js').CliSubcommand} */ +export const repo = { + description, + run: async (argv, importMeta, { parentName }) => { + await meowWithSubcommands( + { + create, + view, + list, + del, + update + }, + { + argv, + description, + importMeta, + name: parentName + ' repo', + } + ) + } +} diff --git a/lib/commands/repos/list.js b/lib/commands/repos/list.js new file mode 100644 index 00000000..45f80aa4 --- /dev/null +++ b/lib/commands/repos/list.js @@ -0,0 +1,170 @@ +/* eslint-disable no-console */ + +import chalk from 'chalk' +// @ts-ignore +import chalkTable from 'chalk-table' +import meow from 'meow' +import ora from 'ora' + +import { outputFlags } from '../../flags/index.js' +import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api-helpers.js' +import { prepareFlags } from '../../utils/flags.js' +import { printFlagList } from '../../utils/formatting.js' +import { getDefaultKey, setupSdk } from '../../utils/sdk.js' + +/** @type {import('../../utils/meow-with-subcommands.js').CliSubcommand} */ +export const list = { + description: 'List repositories in an organization', + async run (argv, importMeta, { parentName }) { + const name = parentName + ' list' + + const input = setupCommand(name, list.description, argv, importMeta) + if (input) { + const spinnerText = 'Listing repositories... \n' + const spinner = ora(spinnerText).start() + await listOrgRepos(input.orgSlug, input, spinner) + } + } +} + +const listRepoFlags = prepareFlags({ + sort: { + type: 'string', + shortFlag: 's', + default: 'created_at', + description: 'Sorting option', + }, + direction: { + type: 'string', + default: 'desc', + description: 'Direction option', + }, + perPage: { + type: 'number', + shortFlag: 'pp', + default: 30, + description: 'Number of results per page' + }, + page: { + type: 'number', + shortFlag: 'p', + default: 1, + description: 'Page number' + }, +}) + +// Internal functions + +/** + * @typedef CommandContext + * @property {boolean} outputJson + * @property {boolean} outputMarkdown + * @property {string} orgSlug + * @property {string} sort + * @property {string} direction + * @property {number} per_page + * @property {number} page + */ + +/** + * @param {string} name + * @param {string} description + * @param {readonly string[]} argv + * @param {ImportMeta} importMeta + * @returns {void|CommandContext} + */ +function setupCommand (name, description, argv, importMeta) { + const flags = { + ...outputFlags, + ...listRepoFlags + } + + const cli = meow(` + Usage + $ ${name} + + Options + ${printFlagList(flags, 6)} + + Examples + $ ${name} FakeOrg + `, { + argv, + description, + importMeta, + flags + }) + + const { + json: outputJson, + markdown: outputMarkdown, + perPage, + sort, + direction, + page + } = cli.flags + + if (!cli.input[0]) { + console.error(`${chalk.bgRed('Input error')}: Please provide an organization slug \n`) + cli.showHelp() + return + } + + const [orgSlug = ''] = cli.input + + return { + outputJson, + outputMarkdown, + orgSlug, + sort, + direction, + page, + per_page: perPage + } +} + +/** + * @typedef RepositoryData + * @property {import('@socketsecurity/sdk').SocketSdkReturnType<'getOrgRepoList'>["data"]} data + */ + +/** + * @param {string} orgSlug + * @param {CommandContext} input + * @param {import('ora').Ora} spinner + * @returns {Promise} + */ +async function listOrgRepos (orgSlug, input, spinner) { + const socketSdk = await setupSdk(getDefaultKey()) + const result = await handleApiCall(socketSdk.getOrgRepoList(orgSlug, input), 'listing repositories') + + if (!result.success) { + return handleUnsuccessfulApiResponse('getOrgRepoList', result, spinner) + } + + spinner.stop() + + const options = { + columns: [ + { field: 'id', name: chalk.magenta('ID') }, + { field: 'name', name: chalk.magenta('Name') }, + { field: 'visibility', name: chalk.magenta('Visibility') }, + { field: 'default_branch', name: chalk.magenta('Default branch') }, + { field: 'archived', name: chalk.magenta('Archived') } + ] + } + + const formattedResults = result.data.results.map(d => { + return { + ...d + } + }) + + const table = chalkTable(options, formattedResults) + + console.log(table, '\n') + + return { + data: result.data + } +} diff --git a/lib/commands/repos/update.js b/lib/commands/repos/update.js new file mode 100644 index 00000000..298a2ebc --- /dev/null +++ b/lib/commands/repos/update.js @@ -0,0 +1,166 @@ +/* eslint-disable no-console */ + +import chalk from 'chalk' +import meow from 'meow' +import ora from 'ora' + +import { outputFlags } from '../../flags/index.js' +import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api-helpers.js' +import { prepareFlags } from '../../utils/flags.js' +import { printFlagList } from '../../utils/formatting.js' +import { getDefaultKey, setupSdk } from '../../utils/sdk.js' + +/** @type {import('../../utils/meow-with-subcommands.js').CliSubcommand} */ +export const update = { + description: 'Update a repository in an organization', + async run (argv, importMeta, { parentName }) { + const name = parentName + ' update' + + const input = setupCommand(name, update.description, argv, importMeta) + if (input) { + const spinnerText = 'Updating repository... \n' + const spinner = ora(spinnerText).start() + await updateRepository(input.orgSlug, input, spinner) + } + } +} + +const repositoryUpdateFlags = prepareFlags({ + repoName: { + type: 'string', + shortFlag: 'n', + default: '', + description: 'Repository name', + }, + repoDescription: { + type: 'string', + shortFlag: 'd', + default: '', + description: 'Repository description', + }, + homepage: { + type: 'string', + shortFlag: 'h', + default: '', + description: 'Repository url', + }, + defaultBranch: { + type: 'string', + shortFlag: 'b', + default: 'main', + description: 'Repository default branch', + }, + visibility: { + type: 'string', + shortFlag: 'v', + default: 'private', + description: 'Repository visibility (Default Private)', + } +}) + +// Internal functions + +/** + * @typedef CommandContext + * @property {boolean} outputJson + * @property {boolean} outputMarkdown + * @property {string} orgSlug + * @property {string} name + * @property {string} description + * @property {string} homepage + * @property {string} default_branch + * @property {string} visibility + */ + +/** + * @param {string} name + * @param {string} description + * @param {readonly string[]} argv + * @param {ImportMeta} importMeta + * @returns {void|CommandContext} + */ +function setupCommand (name, description, argv, importMeta) { + const flags = { + ...outputFlags, + ...repositoryUpdateFlags + } + + const cli = meow(` + Usage + $ ${name} + + Options + ${printFlagList(flags, 6)} + + Examples + $ ${name} FakeOrg + `, { + argv, + description, + importMeta, + flags + }) + + const { + json: outputJson, + markdown: outputMarkdown, + repoName, + repoDescription, + homepage, + defaultBranch, + visibility + } = cli.flags + + const [orgSlug = ''] = cli.input + + if (!orgSlug) { + console.error(`${chalk.bgRed('Input error')}: Please provide an organization slug and repository name \n`) + cli.showHelp() + return + } + + if (!repoName) { + console.error(`${chalk.bgRed('Input error')}: Repository name is required. \n`) + cli.showHelp() + return + } + + return { + outputJson, + outputMarkdown, + orgSlug, + name: repoName, + description: repoDescription, + homepage, + default_branch: defaultBranch, + visibility + } +} + +/** + * @typedef RepositoryData + * @property {import('@socketsecurity/sdk').SocketSdkReturnType<'updateOrgRepo'>["data"]} data + */ + +/** + * @param {string} orgSlug + * @param {CommandContext} input + * @param {import('ora').Ora} spinner + * @returns {Promise} + */ +async function updateRepository (orgSlug, input, spinner) { + const socketSdk = await setupSdk(getDefaultKey()) + const result = await handleApiCall(socketSdk.updateOrgRepo(orgSlug, input.name, input), 'updating repository') + + if (!result.success) { + return handleUnsuccessfulApiResponse('updateOrgRepo', result, spinner) + } + + spinner.stop() + + console.log('\n✅ Repository updated successfully \n') + + return { + data: result.data + } +} diff --git a/lib/commands/repos/view.js b/lib/commands/repos/view.js new file mode 100644 index 00000000..a6251a75 --- /dev/null +++ b/lib/commands/repos/view.js @@ -0,0 +1,128 @@ +/* eslint-disable no-console */ + +import chalk from 'chalk' +// @ts-ignore +import chalkTable from 'chalk-table' +import meow from 'meow' +import ora from 'ora' + +import { outputFlags } from '../../flags/index.js' +import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api-helpers.js' +import { printFlagList } from '../../utils/formatting.js' +import { getDefaultKey, setupSdk } from '../../utils/sdk.js' + +/** @type {import('../../utils/meow-with-subcommands.js').CliSubcommand} */ +export const view = { + description: 'View repositories in an organization', + async run (argv, importMeta, { parentName }) { + const name = parentName + ' view' + + const input = setupCommand(name, view.description, argv, importMeta) + if (input) { + const spinnerText = 'Fetching repository... \n' + const spinner = ora(spinnerText).start() + await viewRepository(input.orgSlug, input.repositoryName, spinner) + } + } +} + +// Internal functions + +/** + * @typedef CommandContext + * @property {boolean} outputJson + * @property {boolean} outputMarkdown + * @property {string} orgSlug + * @property {string} repositoryName + */ + +/** + * @param {string} name + * @param {string} description + * @param {readonly string[]} argv + * @param {ImportMeta} importMeta + * @returns {void|CommandContext} + */ +function setupCommand (name, description, argv, importMeta) { + const flags = { + ...outputFlags + } + + const cli = meow(` + Usage + $ ${name} + + Options + ${printFlagList(flags, 6)} + + Examples + $ ${name} FakeOrg + `, { + argv, + description, + importMeta, + flags + }) + + const { + json: outputJson, + markdown: outputMarkdown + } = cli.flags + + if (!cli.input[0]) { + console.error(`${chalk.bgRed('Input error')}: Please provide an organization slug and repository name \n`) + cli.showHelp() + return + } + + const [orgSlug = '', repositoryName = ''] = cli.input + + return { + outputJson, + outputMarkdown, + orgSlug, + repositoryName + } +} + +/** + * @typedef RepositoryData + * @property {import('@socketsecurity/sdk').SocketSdkReturnType<'getOrgRepo'>["data"]} data + */ + +/** + * @param {string} orgSlug + * @param {string} repoName + * @param {import('ora').Ora} spinner + * @returns {Promise} + */ +async function viewRepository (orgSlug, repoName, spinner) { + const socketSdk = await setupSdk(getDefaultKey()) + const result = await handleApiCall(socketSdk.getOrgRepo(orgSlug, repoName), 'fetching repository') + + if (!result.success) { + return handleUnsuccessfulApiResponse('getOrgRepo', result, spinner) + } + + spinner.stop() + + const options = { + columns: [ + { field: 'id', name: chalk.magenta('ID') }, + { field: 'name', name: chalk.magenta('Name') }, + { field: 'visibility', name: chalk.magenta('Visibility') }, + { field: 'default_branch', name: chalk.magenta('Default branch') }, + { field: 'homepage', name: chalk.magenta('Homepage') }, + { field: 'archived', name: chalk.magenta('Archived') }, + { field: 'created_at', name: chalk.magenta('Created at') } + ] + } + + const table = chalkTable(options, [result.data]) + + console.log(table, '\n') + + return { + data: result.data + } +} diff --git a/package-lock.json b/package-lock.json index 000b3ad4..0f6e6386 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@cyclonedx/cdxgen": "^10.5.2", "@inquirer/select": "^2.3.5", "@socketsecurity/config": "^2.1.3", - "@socketsecurity/sdk": "^1.1.0", + "@socketsecurity/sdk": "^1.1.1", "chalk": "^5.3.0", "chalk-table": "^1.0.2", "execa": "^9.1.0", @@ -1615,9 +1615,9 @@ } }, "node_modules/@socketsecurity/sdk": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@socketsecurity/sdk/-/sdk-1.1.0.tgz", - "integrity": "sha512-9bI6C48C1p9dsug8JVUqCnsM0oMdILw8jPJaUZhdCEDupnkb2aqSM25s3u6sfV0U+9YNS5Dv5HTRHv04cESScQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@socketsecurity/sdk/-/sdk-1.1.1.tgz", + "integrity": "sha512-ACVBe0UiZxy3S1C/2OvetAOoaZDumanKRzr2gXq5qOjhI6neLr2tZxC5xI+UcMKAcTDkwOZM8mpqvMdSjd6EEw==", "dependencies": { "formdata-node": "^5.0.0", "got": "^12.5.3", diff --git a/package.json b/package.json index b722c346..2c9dda8e 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "@cyclonedx/cdxgen": "^10.5.2", "@inquirer/select": "^2.3.5", "@socketsecurity/config": "^2.1.3", - "@socketsecurity/sdk": "^1.1.0", + "@socketsecurity/sdk": "^1.1.1", "chalk": "^5.3.0", "chalk-table": "^1.0.2", "execa": "^9.1.0",