Repository: Redocly/redocly-cli (ts)

Issue:
lint command not working when no API is added to options
**Describe the bug**
When you have added a configuration file (`.redocly.yaml`) and defined your `apis` there, you're still unable to run `redocly lint` without other arguments. 

Example:

Working:
`redocly lint someApi@v1`
Not working:
`redocly lint`  

**To Reproduce**
Steps to reproduce the behavior:
1. Create `redocly.yaml` file and add a definition to the `apis` section.
2. Run this command - `redocly lint`  
4. See error:
`error: missing required argument apis.
`

**Expected behavior**
The `lint` command picks up the definitions defined in the `apis` section of the configuration file and works as expected.

**Logs**
N/a

**OpenAPI definition**
N/a

**Redocly Version(s)**
latest

**`Node.js` Version(s)**
15

**Additional context**
Some context can be found here: https://redoc-ly.slack.com/archives/C02UHE7EL3E/p1661807377826659

Pull request:
feat: handle error if api does not exist
## What/Why/How?
Add error handle in cases if `definition` does not exist. Previously `cli` throw native error 
<img width="880" alt="Screenshot 2022-09-26 at 17 19 41" src="https://user-images.githubusercontent.com/52038205/192579390-f9f11941-303e-4489-af0b-fe125c3704ea.png">

Also there was and issue if `root` of apis provided as link. See #849 and #850

## Reference
Closes #849, #850
## Testing

## Screenshots (optional)
Example with run `redocly lint resources/pets.yaml not-exist.yaml`
<img width="947" alt="Screenshot 2022-09-28 at 18 21 50" src="https://user-images.githubusercontent.com/52038205/192819437-812e6d46-9951-415f-a6e6-9301063ed9dc.png">

## Check yourself

- [ ] Code is linted
- [ ] Tested with redoc/reference-docs/workflows
- [ ] All new/updated code is covered with tests

## Security

- [ ] Security impact of change has been considered
- [ ] Code follows company security practices and guidelines


Patch:
diff --git a/packages/cli/src/__mocks__/@redocly/openapi-core.ts b/packages/cli/src/__mocks__/@redocly/openapi-core.ts
index 83d80e83..f3fcb52c 100644
--- a/packages/cli/src/__mocks__/@redocly/openapi-core.ts
+++ b/packages/cli/src/__mocks__/@redocly/openapi-core.ts
@@ -29,6 +29,7 @@ export const findConfig = jest.fn();
 export const doesYamlFileExist = jest.fn();
 export const bundleDocument = jest.fn(() => Promise.resolve({ problems: {} }));
 export const detectOpenAPI = jest.fn();
+export const isAbsoluteUrl = jest.fn();
 
 export class BaseResolver {
   cache = new Map<string, Promise<Document | ResolveError>>();
diff --git a/packages/cli/src/__mocks__/utils.ts b/packages/cli/src/__mocks__/utils.ts
index 1c8a5e2b..0a1c1c9a 100644
--- a/packages/cli/src/__mocks__/utils.ts
+++ b/packages/cli/src/__mocks__/utils.ts
@@ -1,3 +1,5 @@
+import { ConfigFixture } from '../__tests__/fixtures/config';
+
 export const getFallbackApisOrExit = jest.fn((entrypoints) =>
   entrypoints.map((path: string) => ({ path }))
 );
@@ -12,3 +14,4 @@ export const getOutputFileName = jest.fn(() => ({ outputFile: 'test.yaml', ext:
 export const handleError = jest.fn();
 export const exitWithError = jest.fn();
 export const writeYaml = jest.fn();
+export const loadConfigAndHandleErrors = jest.fn(() => ConfigFixture);
diff --git a/packages/cli/src/commands/build-docs/index.ts b/packages/cli/src/commands/build-docs/index.ts
index 5d98bd66..736d01cb 100644
--- a/packages/cli/src/commands/build-docs/index.ts
+++ b/packages/cli/src/commands/build-docs/index.ts
@@ -3,15 +3,20 @@ import { dirname, resolve } from 'path';
 import { writeFileSync, mkdirSync } from 'fs';
 import { performance } from 'perf_hooks';
 
-import { getObjectOrJSON, isURL, getPageHTML } from './utils';
+import { getObjectOrJSON, getPageHTML } from './utils';
 import type { BuildDocsArgv } from './types';
-import { exitWithError, getExecutionTime, getFallbackApisOrExit } from '../../utils';
-import { getMergedConfig, loadConfig } from '@redocly/openapi-core';
+import { getMergedConfig, isAbsoluteUrl } from '@redocly/openapi-core';
+import {
+  exitWithError,
+  getExecutionTime,
+  getFallbackApisOrExit,
+  loadConfigAndHandleErrors,
+} from '../../utils';
 
 export const handlerBuildCommand = async (argv: BuildDocsArgv) => {
   const startedAt = performance.now();
 
-  const configFromFile = await loadConfig({ configPath: argv.config });
+  const configFromFile = await loadConfigAndHandleErrors({ configPath: argv.config });
   const config = getMergedConfig(configFromFile, argv.api);
 
   const apis = await getFallbackApisOrExit(argv.api ? [argv.api] : [], config);
@@ -33,7 +38,9 @@ export const handlerBuildCommand = async (argv: BuildDocsArgv) => {
     const elapsed = getExecutionTime(startedAt);
 
     const api = await loadAndBundleSpec(
-      isURL(pathToApi) ? pathToApi : resolve(argv.config ? dirname(argv.config) : '', pathToApi)
+      isAbsoluteUrl(pathToApi)
+        ? pathToApi
+        : resolve(argv.config ? dirname(argv.config) : '', pathToApi)
     );
     const pageHTML = await getPageHTML(api, pathToApi, { ...options, redocCurrentVersion });
 
diff --git a/packages/cli/src/commands/build-docs/utils.ts b/packages/cli/src/commands/build-docs/utils.ts
index 1d0fffcb..fed68e4a 100644
--- a/packages/cli/src/commands/build-docs/utils.ts
+++ b/packages/cli/src/commands/build-docs/utils.ts
@@ -1,5 +1,6 @@
 import * as React from 'react';
 import { createStore, Redoc } from 'redoc';
+import { Config, isAbsoluteUrl } from '@redocly/openapi-core';
 
 import { renderToString } from 'react-dom/server';
 import { ServerStyleSheet } from 'styled-components';
@@ -8,7 +9,6 @@ import { join } from 'path';
 import { existsSync, lstatSync, readFileSync } from 'fs';
 
 import type { BuildDocsOptions } from './types';
-import type { Config } from '@redocly/openapi-core';
 import { red } from 'colorette';
 import { exitWithError } from '../../utils';
 
@@ -62,7 +62,7 @@ export async function getPageHTML(
 ) {
   process.stderr.write('Prerendering docs');
 
-  const apiUrl = redocOptions.specUrl || (isURL(pathToApi) ? pathToApi : undefined);
+  const apiUrl = redocOptions.specUrl || (isAbsoluteUrl(pathToApi) ? pathToApi : undefined);
   const store = await createStore(api, apiUrl, redocOptions);
   const sheet = new ServerStyleSheet();
 
@@ -93,10 +93,6 @@ export async function getPageHTML(
   });
 }
 
-export function isURL(str: string): boolean {
-  return /^(https?:)\/\//m.test(str);
-}
-
 export function sanitizeJSONString(str: string): string {
   return escapeClosingScriptTag(escapeUnicode(str));
 }
diff --git a/packages/cli/src/commands/bundle.ts b/packages/cli/src/commands/bundle.ts
index 10d9ed73..de243836 100644
--- a/packages/cli/src/commands/bundle.ts
+++ b/packages/cli/src/commands/bundle.ts
@@ -1,11 +1,4 @@
-import {
-  formatProblems,
-  getTotals,
-  loadConfig,
-  getMergedConfig,
-  lint,
-  bundle,
-} from '@redocly/openapi-core';
+import { formatProblems, getTotals, getMergedConfig, lint, bundle } from '@redocly/openapi-core';
 import {
   dumpBundle,
   getExecutionTime,
@@ -15,6 +8,7 @@ import {
   printUnusedWarnings,
   saveBundle,
   printLintTotals,
+  loadConfigAndHandleErrors,
 } from '../utils';
 import type { CommonOptions, OutputExtensions, Skips, Totals } from '../types';
 import { performance } from 'perf_hooks';
@@ -34,7 +28,10 @@ export type BundleOptions = CommonOptions &
   };
 
 export async function handleBundle(argv: BundleOptions, version: string) {
-  const config = await loadConfig({ configPath: argv.config, customExtends: argv.extends });
+  const config = await loadConfigAndHandleErrors({
+    configPath: argv.config,
+    customExtends: argv.extends,
+  });
   const removeUnusedComponents =
     argv['remove-unused-components'] ||
     config.rawConfig?.styleguide?.decorators?.hasOwnProperty('remove-unused-components');
diff --git a/packages/cli/src/commands/join.ts b/packages/cli/src/commands/join.ts
index 8abd80be..68ebd9a7 100644
--- a/packages/cli/src/commands/join.ts
+++ b/packages/cli/src/commands/join.ts
@@ -10,7 +10,6 @@ import {
   Document,
   StyleguideConfig,
   Oas3Tag,
-  loadConfig,
   formatProblems,
   getTotals,
   lintDocument,
@@ -25,6 +24,7 @@ import {
   printLintTotals,
   writeYaml,
   exitWithError,
+  loadConfigAndHandleErrors,
 } from '../utils';
 import { isObject, isString, keysOf } from '../js-utils';
 import { Oas3Parameter, Oas3PathItem, Oas3Server } from '@redocly/openapi-core/lib/typings/openapi';
@@ -78,7 +78,7 @@ export async function handleJoin(argv: JoinArgv, packageVersion: string) {
     );
   }
 
-  const config: Config = await loadConfig();
+  const config: Config = await loadConfigAndHandleErrors();
   const apis = await getFallbackApisOrExit(argv.apis, config);
   const externalRefResolver = new BaseResolver(config.resolve);
   const documents = await Promise.all(
diff --git a/packages/cli/src/commands/lint.ts b/packages/cli/src/commands/lint.ts
index 2f88a7b2..1b615019 100644
--- a/packages/cli/src/commands/lint.ts
+++ b/packages/cli/src/commands/lint.ts
@@ -7,7 +7,6 @@ import {
   findConfig,
   getMergedConfig,
   makeDocumentFromString,
-  loadConfig,
   stringifyYaml,
   RawConfig,
   RuleSeverity,
@@ -23,6 +22,7 @@ import {
   printConfigLintTotals,
   printUnusedWarnings,
   exitWithError,
+  loadConfigAndHandleErrors,
 } from '../utils';
 import type { CommonOptions, Skips, Totals } from '../types';
 import { blue, gray } from 'colorette';
@@ -39,7 +39,7 @@ export async function handleLint(argv: LintOptions, version: string) {
     return exitWithError('Please, provide valid path to the configuration file');
   }
 
-  const config: Config = await loadConfig({
+  const config: Config = await loadConfigAndHandleErrors({
     configPath: argv.config,
     customExtends: argv.extends,
     processRawConfig: lintConfigCallback(argv, version),
diff --git a/packages/cli/src/commands/login.ts b/packages/cli/src/commands/login.ts
index 5a2d62a7..e99ff0cc 100644
--- a/packages/cli/src/commands/login.ts
+++ b/packages/cli/src/commands/login.ts
@@ -1,6 +1,6 @@
-import { Region, RedoclyClient, loadConfig } from '@redocly/openapi-core';
+import { Region, RedoclyClient } from '@redocly/openapi-core';
 import { blue, green, gray } from 'colorette';
-import { promptUser } from '../utils';
+import { loadConfigAndHandleErrors, promptUser } from '../utils';
 
 export function promptClientToken(domain: string) {
   return promptUser(
@@ -12,7 +12,7 @@ export function promptClientToken(domain: string) {
 }
 
 export async function handleLogin(argv: { verbose?: boolean; region?: Region }) {
-  const region = argv.region || (await loadConfig()).region;
+  const region = argv.region || (await loadConfigAndHandleErrors()).region;
   const client = new RedoclyClient(region);
   const clientToken = await promptClientToken(client.domain);
   process.stdout.write(gray('\n  Logging in...\n'));
diff --git a/packages/cli/src/commands/preview-docs/index.ts b/packages/cli/src/commands/preview-docs/index.ts
index 74559ba9..07667ee9 100644
--- a/packages/cli/src/commands/preview-docs/index.ts
+++ b/packages/cli/src/commands/preview-docs/index.ts
@@ -2,14 +2,13 @@ import * as colorette from 'colorette';
 import * as chockidar from 'chokidar';
 import {
   bundle,
-  loadConfig,
   ResolveError,
   YamlParseError,
   RedoclyClient,
   getTotals,
   getMergedConfig,
 } from '@redocly/openapi-core';
-import { getFallbackApisOrExit } from '../../utils';
+import { getFallbackApisOrExit, loadConfigAndHandleErrors } from '../../utils';
 import startPreviewServer from './preview-server/preview-server';
 import type { Skips } from '../../types';
 
@@ -129,7 +128,7 @@ export async function previewDocs(
   });
 
   async function reloadConfig() {
-    const config = await loadConfig({ configPath: argv.config });
+    const config = await loadConfigAndHandleErrors({ configPath: argv.config });
     const redoclyClient = new RedoclyClient();
     isAuthorizedWithRedocly = await redoclyClient.isAuthorizedWithRedocly();
     const resolvedConfig = getMergedConfig(config, argv.api);
diff --git a/packages/cli/src/commands/push.ts b/packages/cli/src/commands/push.ts
index 2de37781..b42db36f 100644
--- a/packages/cli/src/commands/push.ts
+++ b/packages/cli/src/commands/push.ts
@@ -7,7 +7,6 @@ import { createHash } from 'crypto';
 import {
   bundle,
   Config,
-  loadConfig,
   RedoclyClient,
   IGNORE_FILE,
   BundleOutputFormat,
@@ -22,6 +21,7 @@ import {
   getFallbackApisOrExit,
   pluralize,
   dumpBundle,
+  loadConfigAndHandleErrors,
 } from '../utils';
 import { promptClientToken } from './login';
 
@@ -41,7 +41,7 @@ type PushArgs = {
 };
 
 export async function handlePush(argv: PushArgs): Promise<void> {
-  const config = await loadConfig({ region: argv.region, files: argv.files });
+  const config = await loadConfigAndHandleErrors({ region: argv.region, files: argv.files });
   const client = new RedoclyClient(config.region);
   const isAuthorized = await client.isAuthorizedWithRedoclyByRegion();
   if (!isAuthorized) {
diff --git a/packages/cli/src/commands/stats.ts b/packages/cli/src/commands/stats.ts
index 5724e343..38ae6f8e 100755
--- a/packages/cli/src/commands/stats.ts
+++ b/packages/cli/src/commands/stats.ts
@@ -3,7 +3,6 @@ import * as colors from 'colorette';
 import {
   Config,
   StyleguideConfig,
-  loadConfig,
   normalizeTypes,
   Oas3Types,
   Oas2Types,
@@ -21,7 +20,7 @@ import {
   bundle,
 } from '@redocly/openapi-core';
 
-import { getFallbackApisOrExit } from '../utils';
+import { getFallbackApisOrExit, loadConfigAndHandleErrors } from '../utils';
 import { printExecutionTime } from '../utils';
 
 const statsAccumulator: StatsAccumulator = {
@@ -66,7 +65,7 @@ function printStats(statsAccumulator: StatsAccumulator, api: string, format: str
 }
 
 export async function handleStats(argv: { config?: string; api?: string; format: string }) {
-  const config: Config = await loadConfig({ configPath: argv.config });
+  const config: Config = await loadConfigAndHandleErrors({ configPath: argv.config });
   const [{ path }] = await getFallbackApisOrExit(argv.api ? [argv.api] : [], config);
   const externalRefResolver = new BaseResolver(config.resolve);
   const { bundle: document } = await bundle({ config, ref: path });
diff --git a/packages/cli/src/types.ts b/packages/cli/src/types.ts
index 12a2db1b..373d20ce 100644
--- a/packages/cli/src/types.ts
+++ b/packages/cli/src/types.ts
@@ -1,4 +1,4 @@
-import type { BundleOutputFormat, OutputFormat, Region } from '@redocly/openapi-core';
+import type { BundleOutputFormat, OutputFormat, Region, Config } from '@redocly/openapi-core';
 
 export type Totals = {
   errors: number;
@@ -24,3 +24,5 @@ export type Skips = {
   'skip-decorator'?: string[];
   'skip-preprocessor'?: string[];
 };
+
+export type ConfigApis = Pick<Config, 'apis' | 'configFile'>;
diff --git a/packages/cli/src/utils.ts b/packages/cli/src/utils.ts
index b7aef7bd..b65a7c11 100644
--- a/packages/cli/src/utils.ts
+++ b/packages/cli/src/utils.ts
@@ -1,43 +1,54 @@
-import { basename, dirname, extname, join, resolve } from 'path';
+import { basename, dirname, extname, join, resolve, relative, isAbsolute } from 'path';
 import { blue, gray, green, red, yellow } from 'colorette';
 import { performance } from 'perf_hooks';
 import * as glob from 'glob-promise';
 import * as fs from 'fs';
-import * as path from 'path';
 import * as readline from 'readline';
 import { Writable } from 'stream';
 import {
   BundleOutputFormat,
-  Config,
   StyleguideConfig,
   ResolveError,
   YamlParseError,
+  ResolvedApi,
   parseYaml,
   stringifyYaml,
+  isAbsoluteUrl,
+  loadConfig,
+  RawConfig,
+  Region,
+  Config,
 } from '@redocly/openapi-core';
-import { Totals, outputExtensions, Entrypoint } from './types';
+import { Totals, outputExtensions, Entrypoint, ConfigApis } from './types';
 
 export async function getFallbackApisOrExit(
   argsApis: string[] | undefined,
-  config: Config
+  config: ConfigApis
 ): Promise<Entrypoint[]> {
   const { apis } = config;
   const shouldFallbackToAllDefinitions =
     !isNotEmptyArray(argsApis) && apis && Object.keys(apis).length > 0;
   const res = shouldFallbackToAllDefinitions
-    ? Object.entries(apis).map(([alias, { root }]) => ({
-        path: resolve(getConfigDirectory(config), root),
-        alias,
-      }))
+    ? fallbackToAllDefinitions(apis, config)
     : await expandGlobsInEntrypoints(argsApis!, config);
-  if (!isNotEmptyArray(res)) {
-    process.stderr.write('error: missing required argument `apis`.\n');
+
+  const filteredInvalidEntrypoints = res.filter(({ path }) => !isApiPathValid(path));
+  if (isNotEmptyArray(filteredInvalidEntrypoints)) {
+    for (const { path } of filteredInvalidEntrypoints) {
+      process.stderr.write(
+        yellow(
+          `\n ${relative(process.cwd(), path)} ${red(
+            `does not exist or is invalid. Please provide a valid path. \n\n`
+          )}`
+        )
+      );
+    }
     process.exit(1);
   }
   return res;
 }
 
-function getConfigDirectory(config: Config) {
+function getConfigDirectory(config: ConfigApis) {
   return config.configFile ? dirname(config.configFile) : process.cwd();
 }
 
@@ -45,7 +56,25 @@ function isNotEmptyArray<T>(args?: T[]): boolean {
   return Array.isArray(args) && !!args.length;
 }
 
-function getAliasOrPath(config: Config, aliasOrPath: string): Entrypoint {
+function isApiPathValid(apiPath: string): string | void {
+  if (!apiPath.trim()) {
+    exitWithError('Path cannot be empty.');
+    return;
+  }
+  return fs.existsSync(apiPath) || isAbsoluteUrl(apiPath) ? apiPath : undefined;
+}
+
+function fallbackToAllDefinitions(
+  apis: Record<string, ResolvedApi>,
+  config: ConfigApis
+): Entrypoint[] {
+  return Object.entries(apis).map(([alias, { root }]) => ({
+    path: isAbsoluteUrl(root) ? root : resolve(getConfigDirectory(config), root),
+    alias,
+  }));
+}
+
+function getAliasOrPath(config: ConfigApis, aliasOrPath: string): Entrypoint {
   return config.apis[aliasOrPath]
     ? { path: config.apis[aliasOrPath]?.root, alias: aliasOrPath }
     : {
@@ -53,16 +82,16 @@ function getAliasOrPath(config: Config, aliasOrPath: string): Entrypoint {
         // find alias by path, take the first match
         alias:
           Object.entries(config.apis).find(([_alias, api]) => {
-            return path.resolve(api.root) === path.resolve(aliasOrPath);
+            return resolve(api.root) === resolve(aliasOrPath);
           })?.[0] ?? undefined,
       };
 }
 
-async function expandGlobsInEntrypoints(args: string[], config: Config) {
+async function expandGlobsInEntrypoints(args: string[], config: ConfigApis) {
   return (
     await Promise.all(
       (args as string[]).map(async (aliasOrPath) => {
-        return glob.hasMagic(aliasOrPath)
+        return glob.hasMagic(aliasOrPath) && !isAbsoluteUrl(aliasOrPath)
           ? (await glob(aliasOrPath)).map((g: string) => getAliasOrPath(config, g))
           : getAliasOrPath(config, aliasOrPath);
       })
@@ -120,7 +149,7 @@ export function dumpBundle(obj: any, format: BundleOutputFormat, dereference?: b
 }
 
 export function saveBundle(filename: string, output: string) {
-  fs.mkdirSync(path.dirname(filename), { recursive: true });
+  fs.mkdirSync(dirname(filename), { recursive: true });
   fs.writeFileSync(filename, output);
 }
 
@@ -327,6 +356,23 @@ export function exitWithError(message: string) {
  * Checks if dir is subdir of parent
  */
 export function isSubdir(parent: string, dir: string): boolean {
-  const relative = path.relative(parent, dir);
-  return !!relative && !/^..($|\/)/.test(relative) && !path.isAbsolute(relative);
+  const relativePath = relative(parent, dir);
+  return !!relativePath && !/^..($|\/)/.test(relativePath) && !isAbsolute(relativePath);
+}
+
+export async function loadConfigAndHandleErrors(
+  options: {
+    configPath?: string;
+    customExtends?: string[];
+    processRawConfig?: (rawConfig: RawConfig) => void | Promise<void>;
+    files?: string[];
+    region?: Region;
+  } = {}
+): Promise<Config> {
+  try {
+    return await loadConfig(options);
+  } catch (e) {
+    exitWithError(e.message);
+    return new Config({ apis: {}, styleguide: {} });
+  }
 }
diff --git a/packages/core/src/config/config-resolvers.ts b/packages/core/src/config/config-resolvers.ts
index 1312e03d..46b4e71a 100644
--- a/packages/core/src/config/config-resolvers.ts
+++ b/packages/core/src/config/config-resolvers.ts
@@ -79,14 +79,18 @@ export function resolvePlugins(
     }
 
     if (isString(plugin)) {
-      const absoltePluginPath = path.resolve(path.dirname(configPath), plugin);
-      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
-      // @ts-ignore
-      return typeof __webpack_require__ === 'function'
-        ? // eslint-disable-next-line @typescript-eslint/ban-ts-comment
-          // @ts-ignore
-          __non_webpack_require__(absoltePluginPath)
-        : require(absoltePluginPath);
+      try {
+        const absoltePluginPath = path.resolve(path.dirname(configPath), plugin);
+        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+        // @ts-ignore
+        return typeof __webpack_require__ === 'function'
+          ? // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+            // @ts-ignore
+            __non_webpack_require__(absoltePluginPath)
+          : require(absoltePluginPath);
+      } catch (e) {
+        throw new Error(`Failed to load plugin "${plugin}". Please provide a valid path`);
+      }
     }
 
     return plugin;
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index 046d63ce..69fcbca5 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -36,6 +36,7 @@ export {
   CONFIG_FILE_NAMES,
   RuleSeverity,
   createConfig,
+  ResolvedApi,
 } from './config';
 
 export { RedoclyClient, isRedoclyRegistryURL } from './redocly';
@@ -50,7 +51,7 @@ export {
   makeDocumentFromString,
 } from './resolve';
 export { parseYaml, stringifyYaml } from './js-yaml';
-export { unescapePointer, isRef } from './ref-utils';
+export { unescapePointer, isRef, isAbsoluteUrl } from './ref-utils';
 export { detectOpenAPI, OasMajorVersion, openAPIMajor, OasVersion } from './oas-types';
 export { normalizeVisitors } from './visitors';
 


Test patch:
diff --git a/packages/cli/src/__tests__/commands/lint.test.ts b/packages/cli/src/__tests__/commands/lint.test.ts
index 859fd32c..13bfbf9a 100644
--- a/packages/cli/src/__tests__/commands/lint.test.ts
+++ b/packages/cli/src/__tests__/commands/lint.test.ts
@@ -1,6 +1,5 @@
 import { handleLint, LintOptions } from '../../commands/lint';
 import {
-  loadConfig,
   getMergedConfig,
   lint,
   getTotals,
@@ -13,6 +12,7 @@ import {
   printUnusedWarnings,
   handleError,
   exitWithError,
+  loadConfigAndHandleErrors,
 } from '../../utils';
 import { ConfigFixture } from '../fixtures/config';
 import { performance } from 'perf_hooks';
@@ -58,12 +58,12 @@ describe('handleLint', () => {
       expect(exitWithError).toHaveBeenCalledWith(
         'Please, provide valid path to the configuration file'
       );
-      expect(loadConfig).toHaveBeenCalledTimes(0);
+      expect(loadConfigAndHandleErrors).toHaveBeenCalledTimes(0);
     });
 
-    it('should call loadConfig and getFallbackApisOrExit', async () => {
+    it('should call loadConfigAndHandleErrors and getFallbackApisOrExit', async () => {
       await handleLint(argvMock, versionMock);
-      expect(loadConfig).toHaveBeenCalledWith({
+      expect(loadConfigAndHandleErrors).toHaveBeenCalledWith({
         configPath: undefined,
         customExtends: undefined,
         processRawConfig: undefined,
@@ -76,7 +76,7 @@ describe('handleLint', () => {
         { ...argvMock, config: 'redocly.yaml', extends: ['some/path'] },
         versionMock
       );
-      expect(loadConfig).toHaveBeenCalledWith({
+      expect(loadConfigAndHandleErrors).toHaveBeenCalledWith({
         configPath: 'redocly.yaml',
         customExtends: ['some/path'],
         processRawConfig: undefined,
diff --git a/packages/cli/src/__tests__/commands/push.test.ts b/packages/cli/src/__tests__/commands/push.test.ts
index f6cc4c5a..cbc08783 100644
--- a/packages/cli/src/__tests__/commands/push.test.ts
+++ b/packages/cli/src/__tests__/commands/push.test.ts
@@ -1,6 +1,6 @@
 import * as fs from 'fs';
-import { Config, getMergedConfig, loadConfig } from '@redocly/openapi-core';
-import { exitWithError } from '../../utils';
+import { Config, getMergedConfig } from '@redocly/openapi-core';
+import { exitWithError, loadConfigAndHandleErrors } from '../../utils';
 import { getApiRoot, getDestinationProps, handlePush, transformPush } from '../../commands/push';
 import { ConfigFixture } from '../fixtures/config';
 
@@ -79,7 +79,7 @@ describe('push', () => {
   });
 
   it('push with --files', async () => {
-    (loadConfig as jest.Mock).mockImplementation(({ files }) => {
+    (loadConfigAndHandleErrors as jest.Mock).mockImplementation(({ files }) => {
       return { ...ConfigFixture, files };
     });
 
diff --git a/packages/cli/src/__tests__/utils.test.ts b/packages/cli/src/__tests__/utils.test.ts
index a87d7384..0b67f62c 100644
--- a/packages/cli/src/__tests__/utils.test.ts
+++ b/packages/cli/src/__tests__/utils.test.ts
@@ -1,9 +1,12 @@
-import { Totals } from '@redocly/openapi-core';
 import { getFallbackApisOrExit, isSubdir, pathToFilename, printConfigLintTotals } from '../utils';
+import { ResolvedApi, Totals, isAbsoluteUrl } from '@redocly/openapi-core';
 import { red, yellow } from 'colorette';
+import { existsSync } from 'fs';
+import * as path from 'path';
 
 jest.mock('os');
 jest.mock('colorette');
+jest.mock('fs');
 
 describe('isSubdir', () => {
   it('can correctly determine if subdir', () => {
@@ -53,6 +56,7 @@ describe('pathToFilename', () => {
 
 describe('getFallbackApisOrExit', () => {
   it('should find alias by filename', async () => {
+    (existsSync as jest.Mock<any, any>).mockImplementationOnce(() => true);
     const entry = await getFallbackApisOrExit(['./test.yaml'], {
       apis: {
         main: {
@@ -108,3 +112,127 @@ describe('printConfigLintTotals', () => {
     expect(redColoretteMocks).toHaveBeenCalledTimes(0);
   });
 });
+
+describe('getFallbackApisOrExit', () => {
+  const redColoretteMocks = red as jest.Mock<any, any>;
+  const yellowColoretteMocks = yellow as jest.Mock<any, any>;
+
+  const apis: Record<string, ResolvedApi> = {
+    main: {
+      root: 'someFile.yaml',
+      styleguide: {},
+    },
+  };
+
+  const config = { apis };
+
+  beforeEach(() => {
+    yellowColoretteMocks.mockImplementation((text: string) => text);
+    redColoretteMocks.mockImplementation((text: string) => text);
+    jest.spyOn(process.stderr, 'write').mockImplementation(() => true);
+    jest.spyOn(process, 'exit').mockImplementation();
+  });
+
+  it('should exit with error because no path provided', async () => {
+    const apisConfig = {
+      apis: {},
+    };
+    await getFallbackApisOrExit([''], apisConfig);
+    expect(process.exit).toHaveBeenCalledWith(1);
+  });
+
+  it('should error if file from config do not exist', async () => {
+    (existsSync as jest.Mock<any, any>).mockImplementationOnce(() => false);
+    await getFallbackApisOrExit(undefined, config);
+
+    expect(process.stderr.write).toHaveBeenCalledWith(
+      '\n someFile.yaml does not exist or is invalid. Please provide a valid path. \n\n'
+    );
+    expect(process.exit).toHaveBeenCalledWith(1);
+  });
+
+  it('should return valid array with results if such file exist', async () => {
+    (existsSync as jest.Mock<any, any>).mockImplementationOnce(() => true);
+    jest.spyOn(path, 'resolve').mockImplementationOnce((_, path) => path);
+
+    const result = await getFallbackApisOrExit(undefined, config);
+    expect(process.stderr.write).toHaveBeenCalledTimes(0);
+    expect(process.exit).toHaveBeenCalledTimes(0);
+    expect(result).toStrictEqual([
+      {
+        alias: 'main',
+        path: 'someFile.yaml',
+      },
+    ]);
+  });
+
+  it('should exit with error in case if invalid path provided as args', async () => {
+    const apisConfig = {
+      apis: {},
+    };
+    (existsSync as jest.Mock<any, any>).mockImplementationOnce(() => false);
+    await getFallbackApisOrExit(['someFile.yaml'], apisConfig);
+
+    expect(process.stderr.write).toHaveBeenCalledWith(
+      '\n someFile.yaml does not exist or is invalid. Please provide a valid path. \n\n'
+    );
+    expect(process.exit).toHaveBeenCalledWith(1);
+  });
+
+  it('should exit with error in case if invalid 2 path provided as args', async () => {
+    const apisConfig = {
+      apis: {},
+    };
+    (existsSync as jest.Mock<any, any>).mockImplementationOnce(() => false);
+    await getFallbackApisOrExit(['someFile.yaml', 'someFile2.yaml'], apisConfig);
+
+    expect(process.stderr.write).lastCalledWith(
+      '\n someFile2.yaml does not exist or is invalid. Please provide a valid path. \n\n'
+    );
+    expect(process.exit).toHaveBeenCalledWith(1);
+  });
+
+  it('should exit with error if only one file exist ', async () => {
+    const apisStub = {
+      ...apis,
+      notExist: {
+        root: 'notExist.yaml',
+        styleguide: {},
+      },
+    };
+    const configStub = { apis: apisStub };
+
+    (existsSync as jest.Mock<any, any>).mockImplementationOnce((path) => path === 'someFile.yaml');
+
+    await getFallbackApisOrExit(undefined, configStub);
+
+    expect(process.stderr.write).toBeCalledWith(
+      '\n notExist.yaml does not exist or is invalid. Please provide a valid path. \n\n'
+    );
+    expect(process.exit).toHaveBeenCalledWith(1);
+  });
+
+  it('should work ok if it is url passed', async () => {
+    (existsSync as jest.Mock<any, any>).mockImplementationOnce(() => false);
+    (isAbsoluteUrl as jest.Mock<any, any>).mockImplementation(() => true);
+    const apisConfig = {
+      apis: {
+        main: {
+          root: 'https://someLinkt/petstore.yaml?main',
+          styleguide: {},
+        },
+      },
+    };
+
+    const result = await getFallbackApisOrExit(undefined, apisConfig);
+
+    expect(process.stderr.write).toHaveBeenCalledTimes(0);
+    expect(process.exit).toHaveBeenCalledTimes(0);
+    expect(result).toStrictEqual([
+      {
+        alias: 'main',
+        path: 'https://someLinkt/petstore.yaml?main',
+      },
+    ]);
+  });
+});