Skip to content
Development
Skill

/dart-build-cli-app

Architectural patterns, entrypoint structure, exit codes, stream routing, and subprocess spawning for Dart command-line interface (CLI) applications. Use when building CLI tools, console utilities, scripts, argument parsing with `package:args` (ArgParser or CommandRunner),

BOOST
From plugin
dart-lang-skills
50515 skills
Install
$ npx -y skills add dart-lang/skills --skill dart-build-cli-app --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/dart-build-cli-app

Context preview

The summary Claude sees to decide when to auto-load this skill.

Architectural patterns, entrypoint structure, exit codes, stream routing, and subprocess spawning for Dart command-line interface (CLI) applications. Use when building CLI tools, console utilities, scripts, argument parsing with `package:args` (ArgParser or CommandRunner),

SKILL.md

dart-build-cli-app.SKILL.md
name: dart-build-cli-app
description: >-
  Architectural patterns, entrypoint structure, exit codes, stream routing, and subprocess spawning for Dart command-line interface (CLI) applications. Use when building CLI tools, console utilities, scripts, argument parsing with `package:args` (ArgParser or CommandRunner), handling exit codes, configuring executables in pubspec.yaml, spawning Dart subprocesses, or compiling native CLI binaries. Don't use for Flutter UI widgets, web applications, or standalone HTTP backend servers.

Building Dart CLI Applications

Contents

  • [1. Core Architecture & Process Lifecycle](#1-core-architecture--process-lifecycle)
  • [2. Streams, Diagnostics & Formatting](#2-streams-diagnostics--formatting)
  • [3. Project Configuration & Packaging](#3-project-configuration--packaging)
  • [4. Argument Parsing & Command Routing](#4-argument-parsing--command-routing)
  • [5. Native Async & Modern Stack Traces](#5-native-async--modern-stack-traces)
  • [6. Subprocess Spawning & AOT Resilience](#6-subprocess-spawning--aot-resilience)
  • [7. Signal Handling & Terminal Teardown](#7-signal-handling--terminal-teardown)
  • [8. Testing CLI Applications](#8-testing-cli-applications)
  • [9. Modern Compilation & Distribution](#9-modern-compilation--distribution)
  • [10. Workflows & Audit Checklist](#10-workflows--audit-checklist)
  • [References & Examples](#references--examples)

---

1. Core Architecture & Process Lifecycle

Avoid Destructive Exits (`exit(N)`)

Calling `dart:io`'s `exit(int code)` invokes `Platform::Exit(code)` in the C++ runtime. It immediately terminates the OS process without unwinding the Dart stack:

  • **Debugger Disconnect**: When launched with `--pause-isolates-on-exit`, the VM Service pauses isolates before shutdown to allow IDE inspection. `exit()` terminates the OS process before the VM Service can pause or inspect state.
  • **Coverage Loss**: `package:coverage` queries execution lines over VM Service RPCs during the paused-on-exit state. `exit()` destroys the process before RPC extraction, yielding 0% coverage.
  • **Buffer Truncation**: `stdout` and `stderr` are buffered asynchronous `IOSink` streams. `exit()` drops unflushed bytes.
  • **Resource Leaks**: `finally` blocks (closing locks, deleting temp directories) are bypassed.

**Rule**: Avoid calling `exit(code)` directly during normal execution; set `exitCode = code` or return an integer exit code from `CommandRunner<int>` (from `package:args`) and allow the asynchronous `main()` function to return naturally. Do not call `exit()` on unhandled errors; throw an unhandled `Error` or exception so the runtime unwinds cleanly and exits with a non-zero status.

Standard POSIX exit codes (`/usr/include/sysexits.h`):

  • `0`: Success (`EX_OK` / `ExitCode.success.code`)
  • `64`: Command-line usage error (`EX_USAGE` / `ExitCode.usage.code`)
  • `65`: Data format error (`EX_DATAERR` / `ExitCode.data.code`)
  • `70`: Internal software crash (`EX_SOFTWARE` / `ExitCode.software.code`)
  • `78`: Configuration error (`EX_CONFIG` / `ExitCode.config.code`)

*Note*: Prefer importing `package:io/io.dart` and using `ExitCode` constants (e.g., `ExitCode.usage.code`, `ExitCode.software.code`) rather than magic integer literals. For minimal standalone scripts without package dependencies, standard POSIX integer literals (`0`, `64`, `70`) may be used.

import 'dart:io';
import 'package:args/command_runner.dart';
import 'package:io/io.dart' show ExitCode; // Provides standard POSIX ExitCode constants

Future<void> main(List<String> args) async {
  final runner = CommandRunner<int>('tool', 'CLI tool description.');
  try {
    final status = await runner.run(args);
    exitCode = status ?? ExitCode.success.code;
  } on UsageException catch (e) {
    stderr
      ..writeln(e.message)
      ..writeln(e.usage);
    exitCode = ExitCode.usage.code;
  }
}

The Thin Entrypoint Pattern (`bin/` vs. `lib/src/`)

Keep `bin/*.dart` files strictly as minimal entrypoint trampolines (instantiate runner, pass `args`, await exit code). Place all command definitions, argument parsers, formatters, and business logic inside `lib/src/`.

  • **Rationale**: Code in `bin/` cannot be cleanly imported via `package:` URIs. Moving logic into `lib/src/` allows the entire command runner, subcommand hierarchy, and business logic to be unit-tested in-memory in milliseconds (`< 2ms`) without spawning OS subprocesses.
// bin/my_cli.dart — Thin entrypoint trampoline
import 'dart:io';
import 'package:my_cli/src/cli.dart';

Future<void> main(List<String> args) async {
  exitCode = await runCli(args);
}

---

2. Output, Diagnostics & Formatting

  • **Data vs. Diagnostics**: Write intended program results and machine-readable data exclusively to `stdout`. Write warnings, error messages, and debug logs exclusively to `stderr`.
  • **The Error Usage Rule**: When an argument parsing or mandatory option error

occurs (`FormatException`, `UsageException`, or `ArgumentError` thrown when accessing a missing `mandatory: true` option via `results.option(...)`), **both the error message and the usage text must write to `stderr`**, and exit code `64` (`EX_USAGE` / `ExitCode.usage.code`) must be returned. `stdout` should ONLY receive usage help when the user explicitly requests it via `--help` or `-h`.

  • **No `print()` in Error Handlers**: `print()` routes to `stdout`. Use `stderr.writeln()` for all failure notifications. For standard output, prefer `stdout.writeln()` over `print()` to comply with the [`avoid_print`](https://dart.dev/tools/linter-rules/avoid_print) lint rule (unless `analysis_options.yaml` explicitly configures `avoid_print: false`).
  • **Terminal Capability Detection & `NO_COLOR`**: Verify `stdout.hasTerminal`, `stdout.supportsAnsiEscapes`, and `!Platform.environment.containsKey('NO_COLOR')` before emitting ANSI color or cursor escape codes:
  bool get useAnsi =>
      stdout.hasTerminal &&
      stdout.supportsAnsiEscape
Read more
Ships withdart-lang-skills

Agent skills for Dart, maintained by the Dart team. A collection of skills providing tailored instructions for common Dart development workflows.

Get the whole plugin

Other skills on dart-lang-skills.