runCli function
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;
}
}