async function runLighthouse(url, flags, config) {
  /** @param {any} reason */
  function handleTheUnhandled(reason) {
    process.stderr.write(`Unhandled Rejection. Reason: ${reason}\n`);
    launchedChrome?.kill();
    process.exit(1);
  }
  process.on('unhandledRejection', handleTheUnhandled);

  /** @type {ChromeLauncher.LaunchedChrome|undefined} */
  let launchedChrome;

  try {
    if (url && flags.auditMode && !flags.gatherMode) {
      log.warn('CLI', 'URL parameter is ignored if -A flag is used without -G flag');
    }

    const shouldGather = flags.gatherMode || flags.gatherMode === flags.auditMode;
    const shouldUseLocalChrome = UrlUtils.isLikeLocalhost(flags.hostname);
    if (shouldGather && shouldUseLocalChrome) {
      launchedChrome = await getDebuggableChrome(flags);
      flags.port = launchedChrome.port;
    }

    flags.channel = 'cli';

    const runnerResult = await lighthouse(url, flags, config);

    // If in gatherMode only, there will be no runnerResult.
    if (runnerResult) {
      await saveResults(runnerResult, flags);
    }

    launchedChrome?.kill();
    process.removeListener('unhandledRejection', handleTheUnhandled);

    // Runtime errors indicate something was *very* wrong with the page result.
    // We don't want the user to have to parse the report to figure it out, so we'll still exit
    // with an error code after we saved the results.
    if (runnerResult?.lhr.runtimeError) {
      const {runtimeError} = runnerResult.lhr;
      return printErrorAndExit({
        name: 'LighthouseError',
        friendlyMessage: runtimeError.message,
        code: runtimeError.code,
        message: runtimeError.message,
      });
    }

    return runnerResult;
  } catch (err) {
    launchedChrome?.kill();
    return printErrorAndExit(err);
  }
}
@param {string} url
@param {LH.CliFlags} flags
@param {LH.Config|undefined} config
@return {Promise<LH.RunnerResult|undefined>}