Communitygithub.com

photoeven2-netizen/astronusa

Architekturmuster, Einstiegspunktstruktur, Exit-Codes, Stream-Routing und Subprozess-Erzeugung für Dart-Kommandozeilenanwendungen. Verwenden Sie dies beim Erstellen von CLI-Tools, Konsolenprogrammen, Skripten, Argument-Parsing mit `package:args` (ArgParser oder CommandRunner), Behandlung von Exit-Codes, Konfiguration von ausführbaren Dateien in pubspec.yaml, Erzeugung von Dart-Subprozessen oder Kompilierung von nativen CLI-Binärdateien. Nicht geeignet für Flutter-UI-Widgets, Webanwendungen oder eigenständige HTTP-Backend-Server.

Was ist astronusa?

Diese Skill bietet eine umfassende Anleitung zur Entwicklung von Dart-Kommandozeilenanwendungen (CLI) und behandelt wichtige Architekturmuster wie den schlanken Einstiegspunkt, die korrekte Verwendung von Exit-Codes und das Stream-Routing. Sie erklärt ausführlich, wie man `exit()` vermeidet und stattdessen `exitCode` setzt oder Exit-Codes aus `CommandRunner<int>` zurückgibt, um Debugging, Codeabdeckung und Ressourcenbereinigung nicht zu beeinträchtigen. Die Skill unterstützt die Erstellung von Single-Command- und Multi-Command-Tools mit `package:args`, die effiziente Argumentanalyse und typsichere Zugriffsmethoden (`results.flag()`, `results.option()`, `results.multiOption()`) umfasst. Sie geht auf die Erzeugung von Subprozessen mit `package:cli_util` ein, um Probleme mit AOT-kompilierten Binärdateien zu vermeiden, und beschreibt Best Practices für Signalverarbeitung, Terminalbereinigung und das Testen von CLI-Anwendungen mit `package:test_process`. Die Skill richtet sich an Entwickler, die robuste, gut testbare und wartbare Dart-CLI-Tools erstellen möchten, und bietet praktische Codebeispiele, eine Checkliste und Verweise auf detaillierte Referenzdokumente.

Funktioniert mit~Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/photoeven2-netizen/astronusa/tree/HEAD/.agents/skills/dart-build-cli-app

In Ihrer bevorzugten KI fragen

Öffnet einen neuen Chat, in dem dieser Agent-Skill bereits geladen ist.

Dokumentation

Erstellen von Dart-CLI-Anwendungen

Inhaltsverzeichnis


1. Kernarchitektur & Prozesslebenszyklus

Vermeiden Sie destruktive Beendigungen (exit(N))

Der Aufruf von dart:ios exit(int code) ruft Platform::Exit(code) in der C++-Laufzeit auf. Es beendet den Betriebssystemprozess sofort, ohne den Dart-Stack zu entrollen:

  • Debugger-Trennung: Wenn mit --pause-isolates-on-exit gestartet, pausiert der VM-Service die Isolate vor dem Herunterfahren, um eine IDE-Inspektion zu ermöglichen. exit() beendet den Betriebssystemprozess, bevor der VM-Service den Zustand pausieren oder inspizieren kann.
  • Codeabdeckungsverlust: package:coverage fragt Ausführungszeilen über VM-Service-RPCs während des pausierten Zustands ab. exit() zerstört den Prozess vor der RPC-Extraktion, was zu 0% Codeabdeckung führt.
  • Pufferabschneidung: stdout und stderr sind gepufferte asynchrone IOSink-Streams. exit() verwirft ungeschriebene Bytes.
  • Ressourcenlecks: finally-Blöcke (Schließen von Locks, Löschen temporärer Verzeichnisse) werden umgangen.

Regel: Vermeiden Sie den direkten Aufruf von exit(code) während der normalen Ausführung; setzen Sie exitCode = code oder geben Sie einen ganzzahligen Exit-Code von CommandRunner<int> (aus package:args) zurück und lassen Sie die asynchrone main()-Funktion natürlich zurückkehren. Rufen Sie exit() nicht bei unbehandelten Fehlern auf; werfen Sie eine unbehandelte Error- oder Exception, damit die Laufzeit sauber entrollt und mit einem Status ungleich Null beendet wird.

Standard-POSIX-Exit-Codes (/usr/include/sysexits.h):

  • 0: Erfolg (EX_OK / ExitCode.success.code)
  • 64: Befehlszeilen-Nutzungsfehler (EX_USAGE / ExitCode.usage.code)
  • 65: Datenformatfehler (EX_DATAERR / ExitCode.data.code)
  • 70: Interner Softwareabsturz (EX_SOFTWARE / ExitCode.software.code)
  • 78: Konfigurationsfehler (EX_CONFIG / ExitCode.config.code)

Hinweis: Bevorzugen Sie den Import von package:io/io.dart und die Verwendung von ExitCode-Konstanten (z. B. ExitCode.usage.code, ExitCode.software.code) anstelle von magischen Integer-Literalen. Für minimale eigenständige Skripte ohne Paketabhängigkeiten können standardmäßige POSIX-Integer-Literale (0, 64, 70) verwendet werden.

import 'dart:io';
import 'package:args/command_runner.dart';
import 'package:io/io.dart' show ExitCode; // Stellt standardmäßige POSIX ExitCode-Konstanten bereit

Future<void> main(List<String> args) async {
  final runner = CommandRunner<int>('tool', 'CLI-Tool-Beschreibung.');
  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;
  }
}

Das schlanke Einstiegspunktmuster (bin/ vs. lib/src/)

Halten Sie bin/*.dart-Dateien strikt als minimale Einstiegspunkt-Trampoline (Instanziieren des Runners, Übergeben von args, Abwarten des Exit-Codes). Platzieren Sie alle Befehlsdefinitionen, Argumentparser, Formatierer und Geschäftslogik innerhalb von lib/src/.

  • Begründung: Code in bin/ kann nicht sauber über package:-URIs importiert werden. Das Verschieben der Logik in lib/src/ ermöglicht es, den gesamten Befehlsrunner, die Unterbefehlshierarchie und die Geschäftslogik im Speicher in Millisekunden (< 2ms) zu testen, ohne Betriebssystem-Subprozesse zu erzeugen.
// bin/my_cli.dart — Schlankes Einstiegspunkt-Trampolin
import 'dart:io';
import 'package:my_cli/src/cli.dart';

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

2. Ausgabe, Diagnose & Formatierung

  • Daten vs. Diagnose: Schreiben Sie beabsichtigte Programmergebnisse und maschinenlesbare Daten ausschließlich auf stdout. Schreiben Sie Warnungen, Fehlermeldungen und Debug-Logs ausschließlich auf stderr.
  • Die Fehler-Nutzungsregel: Wenn ein Argument-Parsing- oder Pflichtoptionenfehler auftritt (FormatException, UsageException oder ArgumentError, das beim Zugriff auf eine fehlende mandatory: true-Option über results.option(...) ausgelöst wird), müssen sowohl die Fehlermeldung als auch der Nutzungstext auf stderr geschrieben werden, und der Exit-Code 64 (EX_USAGE / ExitCode.usage.code) muss zurückgegeben werden. stdout sollte NUR dann die Nutzungshilfe erhalten, wenn der Benutzer sie explizit über --help oder -h anfordert.
  • Kein print() in Fehlerbehandlern: print() leitet auf stdout um. Verwenden Sie stderr.writeln() für alle Fehlermeldungen. Für die Standardausgabe bevorzugen Sie stdout.writeln() gegenüber print(), um der avoid_print-Lint-Regel zu entsprechen (es sei denn, analysis_options.yaml konfiguriert explizit avoid_print: false).
  • Terminalerkennung & NO_COLOR: Überprüfen Sie stdout.hasTerminal, stdout.supportsAnsiEscapes und !Platform.environment.containsKey('NO_COLOR'), bevor Sie ANSI-Farb- oder Cursor-Steuercodes ausgeben:
    bool get useAnsi =>
        stdout.hasTerminal &&
        stdout.supportsAnsiEscapes &&
        !Platform.environment.containsKey('NO_COLOR');
    
  • Maschinenlesbare Modi: Wenn --json- oder --machine-Flags übergeben werden, formatieren Sie Daten als JSON auf stdout und leiten Sie Logs auf stderr um.

3. Projektkonfiguration & Paketierung

Gerüstbau & Pubspec-Ausführbare-Zuordnung (executables:)

Erstellen Sie neue Befehlszeilenprojekte mit dart create -t console <paketname>, das das standardmäßige bin/- und lib/-Layout initialisiert. Deklarieren Sie ausführbare Dateien immer in pubspec.yaml unter executables:, um Befehlsnamen Skripten in bin/ zuzuordnen, was einen sauberen Aufruf über dart run <befehl> (ohne Angabe von bin/...dart) ermöglicht und globale binäre Symlinks für dart install konfiguriert:

name: my_cli
description: Hochleistungs-CLI-Tool.
version: 1.0.0

executables:
  my_cli: # Kartiert auf bin/my_cli.dart
  secondary_cmd: helper # Kartiert auf bin/helper.dart

Single-Source-Versionierung (package:build_version)

Vermeiden Sie das Hartcodieren von --version-Strings in bin/*.dart oder das manuelle Synchronisieren von Konstantendateien. Verwenden Sie package:build_version, um lib/src/version.dart zu generieren, das const packageVersion = 'x.y.z'; direkt aus pubspec.yaml während der Builds enthält.

Caching-Konventionen

Speichern Sie temporäre Cache-Dateien in .dart_tool/<paketname>/. Schreiben Sie niemals persistente Cache-Dateien direkt in das Projektwurzelverzeichnis.


4. Argumentanalyse & Befehlsrouting

Importieren Sie package:args, um Befehlszeilenargumente zu verwalten:

  • Einfache Skripte: Verwenden Sie ArgParser direkt mit addFlag() und addOption().
  • Multi-Befehls-Tools: Implementieren Sie CommandRunner<int> und erweitern Sie Command<int> für jeden Unterbefehl, wobei Sie direkt POSIX-Exit-Codes zurückgeben.
  • Typsichere Zugriffsmethoden: Verwenden Sie results.flag('name'), results.option('name') und results.multiOption('name') (verfügbar in package:args 2.5+) anstelle der Map-Indizierung operator [], um manuelle Typumwandlungen (as bool, as String?) zu eliminieren.
  • Komplexe Optionsmodelle: Für Anwendungen mit umfangreichen Flags verwenden Sie package:build_cli, um stark typisierte Optionsklassen zu generieren. Nutzen Sie benannte Standard-Überschreibungen (z. B. {String? hostDefaultOverride}), um Konfigurationsdateien sauber mit CLI-Flags zusammenzuführen.

5. Natives Async & Moderne Stacktraces

  • Vermeiden Sie Chain.capture(): Die Dart-VM bewahrt nativ asynchrone Stackframes über await-Unterbrechungspunkte hinweg auf. Chain.capture umschließt die Ereignisschleife in benutzerdefinierten Zonen, was zu erheblichem Allokationsaufwand führt und Fehler über Zonengrenzen hinweg einfängt.
  • Bereinigen Sie mit Trace.from(st).terse: Verwenden Sie statische Hilfsprogramme aus package:stack_trace bei unbehandelten Fehlern, ohne Zonen zu erfassen:
import 'dart:io';
import 'package:io/io.dart' show ExitCode;
import 'package:stack_trace/stack_trace.dart';

Future<void> runMain(List<String> args) async {
  try {
    await executeLogic(args);
    exitCode = ExitCode.success.code;
  } catch (e, st) {
    stderr.writeln('Schwerwiegender Fehler: $e');
    if (args.contains('-v') || args.contains('--verbose')) {
      stderr.writeln(Trace.from(st).terse);
    }
    exitCode = ExitCode.software.code;
  }
}

6. Subprozess-Erzeugung & AOT-Resilienz

Beim Erzeugen von Dart-SDK-Subprozessen oder Ausführen anderer Dart-Tools (z. B. dart format, dart test, build_runner):

  • Gehen Sie nicht davon aus, dass Platform.resolvedExecutable oder Platform.executable auf das ausführbare dart-Befehlszeilenprogramm zeigt: In eigenständigen AOT-kompilierten Binärdateien (dart install / dart compile exe) zeigt resolvedExecutable auf die kompilierte Anwendungsbinärdatei selbst, was zu rekursiven Selbstaufrufschleifen oder Abstürzen aufgrund von Flag-Ablehnungen führt.
  • Verwenden Sie package:cli_util: Lösen Sie das ausführbare Dart-SDK-Programm mit cli_util.dartExecutable oder cli_util.sdkPath auf, anstatt benutzerdefinierte PATH- oder Verzeichnisscanner zu schreiben.
  • Siehe Versionsanforderungen und detaillierte technische Anleitung in references/aot_sdk_discovery.md.

7. Signalbehandlung & Terminalbereinigung

Wenn Ihr CLI Terminalmodi ändert, Spinner anzeigt oder Listening-Sockets öffnet:

  • Windows-Signalwächter: Unter Windows löst ProcessSignal.sigterm.watch() einen UnsupportedError aus. Schützen Sie sigterm mit if (!Platform.isWindows).
  • Echo- & Zeilenmodus-Bereinigung: Wenn Sie stdin.echoMode = false oder stdin.lineMode = false setzen, überprüfen Sie zuerst if (!stdin.hasTerminal) return; und installieren Sie einen SIGINT-Listener und einen finally-Block, um sie wiederherzustellen, sodass Benutzertastatureingaben nach dem Beenden sichtbar bleiben.
  • Cursorsichtbarkeit: Wenn Sie ANSI-Cursor-verstecken (\x1B[?25l) ausgeben, stellen Sie die Cursorsichtbarkeit immer bei Beendigung oder Abbruch wieder her (\x1B[?25h).
  • Socket-Bereinigung: Schließen Sie explizit lauschende HttpServer- oder ServerSocket-Instanzen (server.close(force: true)) bei Beendigungssignalen, um sofort Betriebssystem-Ports freizugeben.
  • Siehe detaillierte Muster in references/signals_and_terminal.md.

8. Testen von CLI-Anwendungen

Strukturieren Sie Tests über zwei verschiedene Ebenen:

  1. Unit-Tests (In-Memory, < 5ms): Testen Sie Befehlsklassen, Optionsparsing und Geschäftslogik direkt, indem Sie package:<pkg>/src/... in test/ importieren.
  2. Integrationstests (Subprozesse): Verwenden Sie package:test_process und package:test_descriptor, um die End-to-End-Binärausführung, Prozess-I/O-Streaming und Betriebssystem-Exit-Codes zu überprüfen:
import 'package:test/test.dart';
import 'package:test_descriptor/test_descriptor.dart' as d;
import 'package:test_process/test_process.dart';

void main() {
  test('CLI verarbeitet Eingabe und beendet sich sauber', () async {
    await d.file('input.txt', 'hallo').create();

    final process = await TestProcess.start('dart', [
      'run',
      'bin/my_cli.dart',
      '--input',
      d.path('input.txt'),
    ]);

    await expectLater(process.stdout, emitsThrough('Verarbeitung abgeschlossen.'));
    await process.shouldExit(0);
  });
}

9. Moderne Kompilierung & Distribution

Dart 3.12+ standardisiert die CLI-Distribution um dart run und dart install (weg von dart pub global activate):

  • Flüchtige Ausführung (JIT): dart run <paket>@<version> [args] lädt das CLI herunter und führt es bei Bedarf aus.
  • Globale Installation (Native AOT): dart install <paket> kompiliert den Paketeinstiegspunkt zu einer schnellen nativen eigenständigen Binärdatei in ~/.dart/install/bin/.
  • Lokale Entwicklung: Verwenden Sie dart run <befehl> (wird über executables: in pubspec.yaml aufgelöst) oder dart run bin/cli.dart.
  • Bündeln dynamischer Bibliotheken & Code-Assets: Verwenden Sie dart build cli. Gibt das Bundle nach build/cli/_/bundle/ aus.
  • Kompilierung eigenständiger ausführbarer Dateien: Verwenden Sie dart compile exe bin/cli.dart -o <ausgabepfad>.

10. Workflows & Prüfliste

Implementierungs-Workflow

  • Einstiegspunkte in pubspec.yaml unter executables: deklarieren.
  • bin/*.dart als schlanken Einstiegspunkt halten; Befehlslogik in lib/src/ platzieren.
  • Ganzzahlige Exit-Codes zurückgeben oder exitCode = N setzen; rohes exit(N) vermeiden.
  • Fehler, Warnungen und Nutzungshilfe bei Parsing-Fehlern auf stderr leiten.
  • results.flag(), results.option() und results.multiOption() für Typsicherheit verwenden.
  • useAnsi validieren (Überprüfung von stdout.hasTerminal, supportsAnsiEscapes und NO_COLOR), bevor ANSI-Codes ausgegeben werden.
  • Kind-Tools mit cli_util.dartExecutable erzeugen, niemals mit Platform.resolvedExecutable.
  • Befehlsrunner im Speicher per Unit-Test testen; End-to-End-Binärausführung mit test_process testen.

Referenzen & Beispiele

Individual skills in this repo

This repo contains 3 individual skills — each has its own dedicated page.

Verwandte Skills