runCli function

Future<int> runCli(
  1. List<String> arguments, {
  2. StringSink? out,
  3. StringSink? err,
})

Runs the CLI and returns the exit code.

Implementation

Future<int> runCli(
  List<String> arguments, {
  StringSink? out,
  StringSink? err,
}) async {
  final stdoutSink = out ?? stdout;
  final stderrSink = err ?? stderr;
  final parser = buildParser();

  final ArgResults args;
  try {
    args = parser.parse(arguments);
  } on FormatException catch (e) {
    stderrSink
      ..writeln(e.message)
      ..writeln()
      ..writeln(_usage(parser));
    return ExitCodes.usage;
  }
  if (args.flag('help')) {
    stdoutSink.writeln(_usage(parser));
    return ExitCodes.ok;
  }
  if (args.flag('version')) {
    stdoutSink.writeln('api_contract $packageVersion');
    return ExitCodes.ok;
  }

  final root = p.normalize(p.absolute(args.option('project')!));
  final configPath = p.join(root, args.option('config')!);
  final configExplicit = args.wasParsed('config');

  try {
    final config = ContractConfig.load(configPath, optional: !configExplicit);
    final specSource = args.option('spec') ?? config.spec;
    if (specSource == null) {
      stderrSink.writeln(
        'No spec given. Pass --spec <file-or-url> or set "spec:" in '
        '${p.relative(configPath)}.',
      );
      return ExitCodes.usage;
    }
    final resolvedSpec = _isUrl(specSource) || p.isAbsolute(specSource)
        ? specSource
        : (args.option('spec') != null ? specSource : p.join(root, specSource));

    final headers = {...config.headers};
    for (final h in args.multiOption('header')) {
      final i = h.indexOf(':');
      if (i <= 0) {
        stderrSink.writeln('Bad --header "$h": expected "Name: value".');
        return ExitCodes.usage;
      }
      headers[h.substring(0, i).trim()] = h.substring(i + 1).trim();
    }

    final spec = await loadSpec(resolvedSpec, headers: headers);
    final project = extractProject(
      root,
      paths: config.paths,
      exclude: config.exclude,
    );
    final checker = ContractChecker(
      project: project,
      spec: spec,
      config: config,
    );
    var findings = checker.check();

    final baselineOption = args.option('baseline') ?? config.baseline;
    final baselinePath = baselineOption == null
        ? null
        : (p.isAbsolute(baselineOption)
              ? baselineOption
              : p.join(root, baselineOption));
    var baselined = 0;
    var stale = const <String>[];
    if (args.flag('update-baseline')) {
      if (baselinePath == null) {
        stderrSink.writeln('--update-baseline needs --baseline <file>.');
        return ExitCodes.usage;
      }
      final exists = File(baselinePath).existsSync();
      final current = Baseline.load(baselinePath);
      final next = Baseline.next(current, findings, exists: exists);
      next.save(baselinePath);
      final added = findings
          .where((f) => !next.entries.contains(f.fingerprint))
          .length;
      stderrSink.writeln(
        exists
            ? 'Baseline ${p.relative(baselinePath)}: '
                  '${current.entries.length} → ${next.entries.length} entries'
                  '${added > 0 ? ' ($added new finding(s) not added: the '
                            'baseline only shrinks)' : ''}.'
            : 'Baseline ${p.relative(baselinePath)} created with '
                  '${next.entries.length} entries.',
      );
    }
    if (baselinePath != null) {
      final result = Baseline.load(baselinePath).apply(findings);
      findings = result.fresh;
      baselined = result.accepted.length;
      stale = result.stale;
    }

    final format = ReportFormat.values.byName(args.option('format')!);
    final color =
        (args['color'] as bool?) ??
        (out == null &&
            stdout.hasTerminal &&
            stdout.supportsAnsiEscapes &&
            !Platform.environment.containsKey('NO_COLOR'));
    final report = Report(
      specLabel: spec.label,
      specSource: _isUrl(specSource) ? specSource : p.basename(specSource),
      schemaCount: spec.schemas.length,
      match: checker.match,
      findings: findings,
      baselined: baselined,
      staleBaseline: stale,
    );
    stdoutSink.write(report.render(format, color: color));

    final failOn = args.option('fail-on')!;
    final failing = switch (failOn) {
      'never' => false,
      'warning' => findings.any((f) => f.severity != Severity.info),
      _ => findings.any((f) => f.severity == Severity.error),
    };
    return failing ? ExitCodes.drift : ExitCodes.ok;
  } on SpecException catch (e) {
    stderrSink.writeln(e.message);
    return ExitCodes.usage;
  } on ConfigException catch (e) {
    stderrSink.writeln(e.message);
    return ExitCodes.usage;
  } on FormatException catch (e) {
    stderrSink.writeln(e.message);
    return ExitCodes.usage;
  }
}