UNPKG

@redocly/cli

Version:

[@Redocly](https://redocly.com) CLI is your all-in-one OpenAPI utility. It builds, manages, improves, and quality-checks your OpenAPI descriptions, all of which comes in handy for various phases of the API Lifecycle. Create your own rulesets to make API g

971 lines (959 loc) 31 kB
#!/usr/bin/env node import * as path from 'path'; import * as dotenv from 'dotenv'; import './utils/assert-node-version'; import * as yargs from 'yargs'; import * as colors from 'colorette'; import { outputExtensions, regionChoices } from './types'; import { previewDocs } from './commands/preview-docs'; import { handleStats } from './commands/stats'; import { handleSplit } from './commands/split'; import { handleJoin } from './commands/join'; import { handlePushStatus } from './reunite/commands/push-status'; import { handleLint } from './commands/lint'; import { handleBundle } from './commands/bundle'; import { handleLogin, handleLogout } from './commands/auth'; import { handlerBuildCommand } from './commands/build-docs'; import { cacheLatestVersion, notifyUpdateCliVersion, version, } from './utils/update-version-notifier'; import { commandWrapper } from './wrapper'; import { previewProject } from './commands/preview-project'; import { handleTranslations } from './commands/translations'; import { handleEject } from './commands/eject'; import { PRODUCT_PLANS } from './commands/preview-project/constants'; import { commonPushHandler } from './commands/push'; import type { Arguments } from 'yargs'; import type { OutputFormat, RuleSeverity } from '@redocly/openapi-core'; import type { GenerateArazzoFileOptions, RespectOptions } from '@redocly/respect-core'; import type { BuildDocsArgv } from './commands/build-docs/types'; import type { PushStatusOptions } from './reunite/commands/push-status'; import type { PushArguments } from './types'; import type { EjectOptions } from './commands/eject'; dotenv.config({ path: path.resolve(process.cwd(), './.env') }); if (!('replaceAll' in String.prototype)) { require('core-js/actual/string/replace-all'); } cacheLatestVersion(); yargs .version('version', 'Show version number.', version) .help('help', 'Show help.') .parserConfiguration({ 'greedy-arrays': false, 'camel-case-expansion': false }) .command( 'stats [api]', 'Show statistics for an API description.', (yargs) => yargs.positional('api', { type: 'string' }).option({ config: { description: 'Path to the config file.', type: 'string' }, 'lint-config': { description: 'Severity level for config file linting.', choices: ['warn', 'error', 'off'] as ReadonlyArray<RuleSeverity>, default: 'warn' as RuleSeverity, }, format: { description: 'Use a specific output format.', choices: ['stylish', 'json', 'markdown'] as ReadonlyArray<OutputFormat>, default: 'stylish' as OutputFormat, }, }), (argv) => { process.env.REDOCLY_CLI_COMMAND = 'stats'; commandWrapper(handleStats)(argv); } ) .command( 'split [api]', 'Split an API description into a multi-file structure.', (yargs) => yargs .positional('api', { description: 'API description file that you want to split', type: 'string', }) .option({ outDir: { description: 'Output directory where files will be saved.', required: true, type: 'string', }, separator: { description: 'File path separator used while splitting.', required: false, type: 'string', default: '_', }, config: { description: 'Path to the config file.', requiresArg: true, type: 'string', }, 'lint-config': { description: 'Severity level for config file linting.', choices: ['warn', 'error', 'off'] as ReadonlyArray<RuleSeverity>, default: 'warn' as RuleSeverity, }, }) .demandOption('api'), (argv) => { process.env.REDOCLY_CLI_COMMAND = 'split'; commandWrapper(handleSplit)(argv); } ) .command( 'join [apis...]', 'Join multiple API descriptions into one [experimental].', (yargs) => yargs .positional('apis', { array: true, type: 'string', demandOption: true, }) .option({ 'prefix-tags-with-info-prop': { description: 'Prefix tags with property value from info object.', requiresArg: true, type: 'string', }, 'prefix-tags-with-filename': { description: 'Prefix tags with property value from file name.', type: 'boolean', default: false, }, 'prefix-components-with-info-prop': { description: 'Prefix components with property value from info object.', requiresArg: true, type: 'string', }, 'without-x-tag-groups': { description: 'Skip automated x-tagGroups creation', type: 'boolean', }, output: { description: 'Output file.', alias: 'o', type: 'string', }, config: { description: 'Path to the config file.', requiresArg: true, type: 'string', }, 'lint-config': { description: 'Severity level for config file linting.', choices: ['warn', 'error', 'off'] as ReadonlyArray<RuleSeverity>, default: 'warn' as RuleSeverity, }, lint: { hidden: true, deprecated: true, }, decorate: { hidden: true, deprecated: true, }, preprocess: { hidden: true, deprecated: true, }, }), (argv) => { const DEPRECATED_OPTIONS = ['lint', 'preprocess', 'decorate']; const DECORATORS_DOCUMENTATION_LINK = 'https://redocly.com/docs/cli/decorators/#decorators'; const JOIN_COMMAND_DOCUMENTATION_LINK = 'https://redocly.com/docs/cli/commands/join/#join'; DEPRECATED_OPTIONS.forEach((option) => { if (argv[option]) { process.stdout.write( `${colors.red( `Option --${option} is no longer supported. Please review join command documentation ${JOIN_COMMAND_DOCUMENTATION_LINK}.` )}` ); process.stdout.write('\n\n'); if (['preprocess', 'decorate'].includes(option)) { process.stdout.write( `${colors.red( `If you are looking for decorators, please review the decorators documentation ${DECORATORS_DOCUMENTATION_LINK}.` )}` ); process.stdout.write('\n\n'); } yargs.showHelp(); process.exit(1); } }); process.env.REDOCLY_CLI_COMMAND = 'join'; commandWrapper(handleJoin)(argv); } ) .command( 'push-status [pushId]', false, (yargs) => yargs .positional('pushId', { description: 'Push id.', type: 'string', required: true, }) .implies('max-execution-time', 'wait') .option({ organization: { description: 'Name of the organization to push to.', type: 'string', alias: 'o', }, project: { description: 'Name of the project to push to.', type: 'string', required: true, alias: 'p', }, domain: { description: 'Specify a domain.', alias: 'd', type: 'string', required: false }, wait: { description: 'Wait for build to finish.', type: 'boolean', default: false, }, 'max-execution-time': { description: 'Maximum execution time in seconds.', type: 'number', }, 'continue-on-deploy-failures': { description: 'Command does not fail even if the deployment fails.', type: 'boolean', default: false, }, 'lint-config': { description: 'Severity level for config file linting.', choices: ['warn', 'error', 'off'] as ReadonlyArray<RuleSeverity>, default: 'warn' as RuleSeverity, }, }), (argv) => { process.env.REDOCLY_CLI_COMMAND = 'push-status'; commandWrapper(handlePushStatus)(argv as Arguments<PushStatusOptions>); } ) .command( 'push [apis...]', 'Push an API description to the Redocly API registry.', (yargs) => yargs .positional('apis', { type: 'string', array: true, required: true, default: [], }) .hide('project') .hide('domain') .hide('mount-path') .hide('author') .hide('message') .hide('default-branch') .hide('verbose') .hide('commit-sha') .hide('commit-url') .hide('namespace') .hide('repository') .hide('wait-for-deployment') .hide('created-at') .hide('max-execution-time') .deprecateOption('batch-id', 'use --job-id') .implies('job-id', 'batch-size') .implies('batch-id', 'batch-size') .implies('batch-size', 'job-id') .implies('max-execution-time', 'wait-for-deployment') .option({ destination: { description: 'API name and version in the format `name@version`.', type: 'string', alias: 'd', }, branch: { description: 'Branch name to push to.', type: 'string', alias: 'b', }, upsert: { description: "Create the specified API version if it doesn't exist, update if it does exist.", type: 'boolean', alias: 'u', }, 'batch-id': { description: 'Specifies the ID of the CI job that the current push will be associated with.', type: 'string', requiresArg: true, deprecated: true, hidden: true, }, 'job-id': { description: 'ID of the CI job that the current push will be associated with.', type: 'string', requiresArg: true, }, 'batch-size': { description: 'Number of CI pushes to expect in a batch.', type: 'number', requiresArg: true, }, region: { description: 'Specify a region.', alias: 'r', choices: regionChoices }, 'skip-decorator': { description: 'Ignore certain decorators.', array: true, type: 'string', }, public: { description: 'Make the API description available to the public', type: 'boolean', }, files: { description: 'List of other folders and files to upload', array: true, type: 'string', }, 'lint-config': { description: 'Severity level for config file linting.', choices: ['warn', 'error', 'off'] as ReadonlyArray<RuleSeverity>, default: 'warn' as RuleSeverity, }, organization: { description: 'Name of the organization to push to.', type: 'string', alias: 'o', }, project: { description: 'Name of the project to push to.', type: 'string', alias: 'p', }, 'mount-path': { description: 'The path files should be pushed to.', type: 'string', alias: 'mp', }, author: { description: 'Author of the commit.', type: 'string', alias: 'a', }, message: { description: 'Commit message.', type: 'string', alias: 'm', }, 'commit-sha': { description: 'Commit SHA.', type: 'string', alias: 'sha', }, 'commit-url': { description: 'Commit URL.', type: 'string', alias: 'url', }, namespace: { description: 'Repository namespace.', type: 'string', }, repository: { description: 'Repository name.', type: 'string', }, 'created-at': { description: 'Commit creation date.', type: 'string', }, domain: { description: 'Specify a domain.', alias: 'd', type: 'string' }, config: { description: 'Path to the config file.', requiresArg: true, type: 'string', }, 'default-branch': { type: 'string', default: 'main', }, 'max-execution-time': { description: 'Maximum execution time in seconds.', type: 'number', }, 'wait-for-deployment': { description: 'Wait for build to finish.', type: 'boolean', default: false, }, verbose: { type: 'boolean', default: false, }, 'continue-on-deploy-failures': { description: 'Command does not fail even if the deployment fails.', type: 'boolean', default: false, }, }), (argv) => { process.env.REDOCLY_CLI_COMMAND = 'push'; commandWrapper(commonPushHandler(argv))(argv as Arguments<PushArguments>); } ) .command( 'lint [apis...]', 'Lint an API or Arazzo description.', (yargs) => yargs.positional('apis', { array: true, type: 'string', demandOption: true }).option({ format: { description: 'Use a specific output format.', choices: [ 'stylish', 'codeframe', 'json', 'checkstyle', 'codeclimate', 'summary', 'markdown', 'github-actions', ] as ReadonlyArray<OutputFormat>, default: 'codeframe' as OutputFormat, }, 'max-problems': { requiresArg: true, description: 'Reduce output to a maximum of N problems.', type: 'number', default: 100, }, 'generate-ignore-file': { description: 'Generate an ignore file.', type: 'boolean', }, 'skip-rule': { description: 'Ignore certain rules.', array: true, type: 'string', }, 'skip-preprocessor': { description: 'Ignore certain preprocessors.', array: true, type: 'string', }, 'lint-config': { description: 'Severity level for config file linting.', choices: ['warn', 'error', 'off'] as ReadonlyArray<RuleSeverity>, default: 'warn' as RuleSeverity, }, config: { description: 'Path to the config file.', requiresArg: true, type: 'string', }, extends: { description: 'Override extends configurations (defaults or config file settings).', requiresArg: true, array: true, type: 'string', }, }), (argv) => { process.env.REDOCLY_CLI_COMMAND = 'lint'; commandWrapper(handleLint)(argv); } ) .command( 'bundle [apis...]', 'Bundle a multi-file API description to a single file.', (yargs) => yargs .positional('apis', { array: true, type: 'string', demandOption: true }) .options({ output: { type: 'string', description: 'Output file or folder for inline APIs.', alias: 'o', }, ext: { description: 'Bundle file extension.', requiresArg: true, choices: outputExtensions, }, 'skip-preprocessor': { description: 'Ignore certain preprocessors.', array: true, type: 'string', }, 'skip-decorator': { description: 'Ignore certain decorators.', array: true, type: 'string', }, dereferenced: { alias: 'd', type: 'boolean', description: 'Produce a fully dereferenced bundle.', }, force: { alias: 'f', type: 'boolean', description: 'Produce bundle output even when errors occur.', }, config: { description: 'Path to the config file.', type: 'string', }, metafile: { description: 'Produce metadata about the bundle', type: 'string', }, extends: { description: 'Override extends configurations (defaults or config file settings).', requiresArg: true, array: true, type: 'string', hidden: true, }, 'remove-unused-components': { description: 'Remove unused components.', type: 'boolean', default: false, }, 'keep-url-references': { description: 'Keep absolute url references.', type: 'boolean', alias: 'k', }, 'lint-config': { description: 'Severity level for config file linting.', choices: ['warn', 'error', 'off'] as ReadonlyArray<RuleSeverity>, default: 'warn' as RuleSeverity, }, format: { hidden: true, deprecated: true, }, lint: { hidden: true, deprecated: true, }, 'skip-rule': { hidden: true, deprecated: true, array: true, type: 'string', }, 'max-problems': { hidden: true, deprecated: true, }, }) .check((argv) => { if (argv.output && (!argv.apis || argv.apis.length === 0)) { throw new Error('At least one inline API must be specified when using --output.'); } return true; }), (argv) => { const DEPRECATED_OPTIONS = ['lint', 'format', 'skip-rule', 'max-problems']; const LINT_AND_BUNDLE_DOCUMENTATION_LINK = 'https://redocly.com/docs/cli/guides/lint-and-bundle/#lint-and-bundle-api-descriptions-with-redocly-cli'; DEPRECATED_OPTIONS.forEach((option) => { if (argv[option]) { process.stdout.write( `${colors.red( `Option --${option} is no longer supported. Please use separate commands, as described in the ${LINT_AND_BUNDLE_DOCUMENTATION_LINK}.` )}` ); process.stdout.write('\n\n'); yargs.showHelp(); process.exit(1); } }); process.env.REDOCLY_CLI_COMMAND = 'bundle'; commandWrapper(handleBundle)(argv); } ) .command( 'check-config', 'Lint the Redocly configuration file.', async (yargs) => yargs.option({ config: { description: 'Path to the config file.', type: 'string', }, 'lint-config': { description: 'Severity level for config file linting.', choices: ['warn', 'error'] as ReadonlyArray<RuleSeverity>, default: 'error' as RuleSeverity, }, }), (argv) => { process.env.REDOCLY_CLI_COMMAND = 'check-config'; commandWrapper()(argv); } ) .command( 'login', 'Log in to Redocly.', async (yargs) => yargs.options({ verbose: { description: 'Include additional output.', type: 'boolean', }, residency: { description: 'Residency of the application. Defaults to `us`.', alias: ['r', 'region'], type: 'string', }, config: { description: 'Path to the config file.', requiresArg: true, type: 'string', }, next: { description: 'Use Reunite application to login.', type: 'boolean', }, }), (argv) => { process.env.REDOCLY_CLI_COMMAND = 'login'; commandWrapper(handleLogin)(argv); } ) .command( 'logout', 'Clear your stored credentials for the Redocly API registry.', (yargs) => yargs, (argv) => { process.env.REDOCLY_CLI_COMMAND = 'logout'; commandWrapper(handleLogout)(argv); } ) .command( 'preview', 'Preview Redocly project using one of the product NPM packages.', (yargs) => yargs.options({ product: { type: 'string', choices: ['redoc', 'revel', 'reef', 'realm', 'redoc-revel', 'redoc-reef', 'revel-reef'], description: "Product used to launch preview. Default is inferred from project's package.json or 'realm' is used.", }, plan: { type: 'string', choices: PRODUCT_PLANS, default: 'enterprise', }, port: { alias: 'p', type: 'number', description: 'Preview port.', default: 4000, }, 'project-dir': { alias: ['d', 'source-dir'], type: 'string', description: 'Specifies the project content directory. The default value is the directory where the command is executed.', default: '.', }, 'lint-config': { description: 'Severity level for config file linting.', choices: ['warn', 'error', 'off'] as ReadonlyArray<RuleSeverity>, default: 'warn' as RuleSeverity, }, }), (argv) => { if (process.argv.some((arg) => arg.startsWith('--source-dir'))) { process.stderr.write( colors.red( 'Option --source-dir is deprecated and will be removed soon. Use --project-dir instead.\n' ) ); } commandWrapper(previewProject)(argv); } ) .command( 'preview-docs [api]', 'Preview API reference docs for an API description.', (yargs) => yargs.positional('api', { type: 'string' }).options({ port: { alias: 'p', type: 'number', default: 8080, description: 'Preview port.', }, host: { alias: 'h', type: 'string', default: '127.0.0.1', description: 'Preview host.', }, 'skip-preprocessor': { description: 'Ignore certain preprocessors.', array: true, type: 'string', }, 'skip-decorator': { description: 'Ignore certain decorators.', array: true, type: 'string', }, 'use-community-edition': { description: 'Use Redoc CE for documentation preview.', type: 'boolean', }, force: { alias: 'f', type: 'boolean', description: 'Produce bundle output even when errors occur.', }, config: { description: 'Path to the config file.', type: 'string', }, 'lint-config': { description: 'Severity level for config file linting.', choices: ['warn', 'error', 'off'] as ReadonlyArray<RuleSeverity>, default: 'warn' as RuleSeverity, }, }), (argv) => { process.env.REDOCLY_CLI_COMMAND = 'preview-docs'; commandWrapper(previewDocs)(argv); } ) .command( 'build-docs [api]', 'Produce API documentation as an HTML file', (yargs) => yargs .positional('api', { type: 'string' }) .options({ o: { describe: 'Output destination file.', alias: 'output', type: 'string', default: 'redoc-static.html', }, title: { describe: 'Page title.', type: 'string', }, disableGoogleFont: { describe: 'Disable Google fonts.', type: 'boolean', default: false, }, t: { alias: 'template', describe: 'Path to handlebars page template, see https://github.com/Redocly/redocly-cli/blob/main/packages/cli/src/commands/build-docs/template.hbs for the example.', type: 'string', }, templateOptions: { describe: 'Additional options to pass to the template. Use dot notation, e.g. templateOptions.metaDescription', }, theme: { describe: 'Redoc theme.openapi configuration. Use dot notation, e.g. theme.openapi.nativeScrollbars', }, config: { describe: 'Path to the config file.', type: 'string', }, 'lint-config': { description: 'Severity level for config file linting.', choices: ['warn', 'error', 'off'] as ReadonlyArray<RuleSeverity>, default: 'warn' as RuleSeverity, }, }) .check((argv: any) => { if (argv.theme && !argv.theme?.openapi) throw Error('Invalid option: theme.openapi not set.'); return true; }), async (argv) => { process.env.REDOCLY_CLI_COMMAND = 'build-docs'; commandWrapper(handlerBuildCommand)(argv as Arguments<BuildDocsArgv>); } ) .command( 'translate <locale>', 'Creates or updates translations.yaml files and fills them with missing built-in translations and translations from the redocly.yaml and sidebars.yaml files.', (yargs) => yargs .positional('locale', { description: 'Locale code to generate translations for, or `all` for all current project locales.', type: 'string', demandOption: true, }) .options({ 'project-dir': { alias: 'd', type: 'string', description: 'Specifies the project content directory. The default value is the directory where the command is executed.', default: '.', }, 'lint-config': { description: 'Severity level for config file linting.', choices: ['warn', 'error', 'off'] as ReadonlyArray<RuleSeverity>, default: 'warn' as RuleSeverity, }, }), (argv) => { process.env.REDOCLY_CLI_COMMAND = 'translate'; commandWrapper(handleTranslations)(argv); } ) .command( 'eject <type> [path]', 'Helper function to eject project elements for customization.', (yargs) => yargs .positional('type', { description: 'Specifies what type of project element to eject. Currently this value must be `component`.', demandOption: true, choices: ['component'], }) .positional('path', { description: 'Filepath to a component or filepath with glob pattern.', type: 'string', }) .options({ 'project-dir': { alias: 'd', type: 'string', description: 'Specifies the project content directory. The default value is the directory where the command is executed.', default: '.', }, force: { alias: 'f', type: 'boolean', description: 'Skips the "overwrite existing" confirmation when ejecting a component that is already ejected in the destination.', }, 'lint-config': { description: 'Severity level for config file linting.', choices: ['warn', 'error', 'off'] as ReadonlyArray<RuleSeverity>, default: 'warn' as RuleSeverity, }, }), (argv) => { process.env.REDOCLY_CLI_COMMAND = 'eject'; commandWrapper(handleEject)(argv as Arguments<EjectOptions>); } ) .command( 'respect [files...]', 'Run Arazzo tests.', (yargs) => { return yargs .positional('files', { describe: 'Test files or glob pattern.', type: 'string', array: true, default: [], }) .env('REDOCLY_CLI_RESPECT') .options({ input: { alias: 'i', describe: 'Input parameters.', type: 'string', }, server: { alias: 'S', describe: 'Server parameters.', type: 'string', }, workflow: { alias: 'w', describe: 'Workflow name.', type: 'string', array: true, }, skip: { alias: 's', describe: 'Workflow to skip.', type: 'string', array: true, }, verbose: { alias: 'v', describe: 'Apply verbose mode.', type: 'boolean', }, 'har-output': { describe: 'Har file output name.', type: 'string', }, 'json-output': { describe: 'JSON file output name.', type: 'string', }, 'client-cert': { describe: 'Mutual TLS client certificate.', type: 'string', }, 'client-key': { describe: 'Mutual TLS client key.', type: 'string', }, 'ca-cert': { describe: 'Mutual TLS CA certificate.', type: 'string', }, severity: { describe: 'Severity of the check.', type: 'string', }, }); }, async (argv) => { process.env.REDOCLY_CLI_COMMAND = 'respect'; const { handleRun } = await import('@redocly/respect-core'); commandWrapper(handleRun)(argv as Arguments<RespectOptions>); } ) .command( 'generate-arazzo <descriptionPath>', 'Auto-generate arazzo description file from an API description.', (yargs) => { return yargs .positional('descriptionPath', { describe: 'Description file path.', type: 'string', }) .env('REDOCLY_CLI_RESPECT') .options({ 'output-file': { alias: 'o', describe: 'Output File name.', type: 'string', requiresArg: true, }, }); }, async (argv) => { process.env.REDOCLY_CLI_COMMAND = 'generate-arazzo'; const { handleGenerate } = await import('@redocly/respect-core'); commandWrapper(handleGenerate)(argv as Arguments<GenerateArazzoFileOptions>); } ) .completion('completion', 'Generate autocomplete script for `redocly` command.') .demandCommand(1) .middleware([notifyUpdateCliVersion]) .strict().argv;