Communitygithub.com

photoeven2-netizen/astronusa

Modèles architecturaux, structure des points d'entrée, codes de sortie, routage des flux et lancement de sous-processus pour les applications en ligne de commande (CLI) en Dart. À utiliser lors de la construction d'outils CLI, d'utilitaires console, de scripts, d'analyse d'arguments avec `package:args` (ArgParser ou CommandRunner), de gestion des codes de sortie, de configuration des exécutables dans pubspec.yaml, de lancement de sous-processus Dart ou de compilation de binaires CLI natifs. Ne pas utiliser pour les widgets UI Flutter, les applications web ou les serveurs backend HTTP autonomes.

Qu'est-ce que astronusa ?

Cette compétence fournit un guide complet pour la construction d'applications en ligne de commande (CLI) robustes et maintenables en Dart. Elle couvre l'architecture de base et le cycle de vie des processus, en mettant l'accent sur l'évitement des appels destructeurs à `exit()` au profit de l'utilisation de `exitCode` ou du retour de codes de sortie POSIX. L'accent est mis sur les structures de code modulaires, avec le motif « point d'entrée mince » (`bin/`) et la logique métier dans `lib/src/`, facilitant les tests unitaires. La compétence détaille également la gestion des flux (`stdout` pour les données, `stderr` pour les diagnostics), le formatage avec détection des capacités du terminal, l'analyse d'arguments avec `package:args`, la gestion des signaux et le nettoyage des ressources, ainsi que le lancement de sous-processus de manière résiliente, notamment dans les binaires compilés AOT. Enfin, elle couvre le test, la compilation et la distribution modernes des CLI Dart.

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

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

Construction d'applications CLI en Dart

Table des matières


1. Architecture cœur et cycle de vie des processus

Éviter les sorties destructrices (exit(N))

Appeler exit(int code) de dart:io invoque Platform::Exit(code) dans le runtime C++. Il termine immédiatement le processus OS sans dérouler la pile Dart :

  • Déconnexion du débogueur : Lorsqu'il est lancé avec --pause-isolates-on-exit, le service VM met en pause les isolates avant l'arrêt pour permettre l'inspection par l'IDE. exit() termine le processus OS avant que le service VM ne puisse mettre en pause ou inspecter l'état.
  • Perte de couverture : package:coverage interroge les lignes d'exécution via les RPC du service VM pendant l'état de pause. exit() détruit le processus avant l'extraction RPC, donnant une couverture de 0%.
  • Troncature des tampons : stdout et stderr sont des flux IOSink asynchrones tamponnés. exit() perd les octets non vidés.
  • Fuite de ressources : Les blocs finally (fermeture de verrous, suppression de répertoires temporaires) sont contournés.

Règle : Évitez d'appeler exit(code) directement pendant l'exécution normale ; définissez exitCode = code ou retournez un code de sortie entier depuis CommandRunner<int> (de package:args) et laissez la fonction main() asynchrone se terminer naturellement. N'appelez pas exit() sur des erreurs non gérées ; levez une Error ou une exception non gérée pour que le runtime se déroule proprement et se termine avec un statut non nul.

Codes de sortie POSIX standard (/usr/include/sysexits.h) :

  • 0 : Succès (EX_OK / ExitCode.success.code)
  • 64 : Erreur d'utilisation de la ligne de commande (EX_USAGE / ExitCode.usage.code)
  • 65 : Erreur de format de données (EX_DATAERR / ExitCode.data.code)
  • 70 : Crash logiciel interne (EX_SOFTWARE / ExitCode.software.code)
  • 78 : Erreur de configuration (EX_CONFIG / ExitCode.config.code)

Remarque : Préférez importer package:io/io.dart et utiliser les constantes ExitCode (par exemple, ExitCode.usage.code, ExitCode.software.code) plutôt que des littéraux entiers magiques. Pour les scripts autonomes minimaux sans dépendances de package, les littéraux entiers POSIX standard (0, 64, 70) peuvent être utilisés.

import 'dart:io';
import 'package:args/command_runner.dart';
import 'package:io/io.dart' show ExitCode; // Fournit les constantes ExitCode POSIX standard

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

Le motif du point d'entrée mince (bin/ vs. lib/src/)

Gardez les fichiers bin/*.dart strictement comme des trampolines de point d'entrée minimaux (instancier le runner, passer args, attendre le code de sortie). Placez toutes les définitions de commandes, analyseurs d'arguments, formateurs et logique métier dans lib/src/.

  • Raison : Le code dans bin/ ne peut pas être importé proprement via les URI package:. Déplacer la logique dans lib/src/ permet de tester unitairement l'ensemble du runner de commandes, la hiérarchie des sous-commandes et la logique métier en mémoire en quelques millisecondes (< 2ms) sans lancer de sous-processus OS.
// bin/my_cli.dart — Trampoline de point d'entrée mince
import 'dart:io';
import 'package:my_cli/src/cli.dart';

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

2. Sortie, diagnostics et formatage

  • Données vs. Diagnostics : Écrivez les résultats du programme prévus et les données lisibles par machine exclusivement sur stdout. Écrivez les avertissements, messages d'erreur et journaux de débogage exclusivement sur stderr.
  • La règle d'utilisation des erreurs : Lorsqu'une erreur d'analyse d'argument ou d'option obligatoire se produit (FormatException, UsageException, ou ArgumentError levée lors de l'accès à une option mandatory: true via results.option(...)), le message d'erreur et le texte d'utilisation doivent être écrits sur stderr, et le code de sortie 64 (EX_USAGE / ExitCode.usage.code) doit être retourné. stdout ne doit recevoir l'aide d'utilisation que lorsque l'utilisateur la demande explicitement via --help ou -h.
  • Pas de print() dans les gestionnaires d'erreurs : print() route vers stdout. Utilisez stderr.writeln() pour toutes les notifications d'échec. Pour la sortie standard, préférez stdout.writeln() à print() pour se conformer à la règle de lint avoid_print (sauf si analysis_options.yaml configure explicitement avoid_print: false).
  • Détection des capacités du terminal et NO_COLOR : Vérifiez stdout.hasTerminal, stdout.supportsAnsiEscapes et !Platform.environment.containsKey('NO_COLOR') avant d'émettre des codes d'échappement ANSI de couleur ou de curseur :
    bool get useAnsi =>
        stdout.hasTerminal &&
        stdout.supportsAnsiEscapes &&
        !Platform.environment.containsKey('NO_COLOR');
    
  • Modes lisibles par machine : Lorsque les drapeaux --json ou --machine sont passés, formatez les données en JSON sur stdout et acheminez les journaux vers stderr.

3. Configuration du projet et empaquetage

Échafaudage et mappage des exécutables (executables:)

Échafaudez de nouveaux projets en ligne de commande en utilisant dart create -t console <package_name>, qui initialise la disposition standard bin/ et lib/. Déclarez toujours les exécutables dans pubspec.yaml sous executables: pour mapper les noms de commandes aux scripts dans bin/, permettant une invocation propre via dart run <command> (sans spécifier bin/...dart) et configurant les liens symboliques binaires globaux pour dart install :

name: my_cli
description: Outil CLI haute performance.
version: 1.0.0

executables:
  my_cli: # Mappe vers bin/my_cli.dart
  secondary_cmd: helper # Mappe vers bin/helper.dart

Versionnage à source unique (package:build_version)

Évitez de coder en dur les chaînes --version dans bin/*.dart ou de synchroniser manuellement les fichiers de constantes. Utilisez package:build_version pour générer lib/src/version.dart contenant const packageVersion = 'x.y.z'; directement à partir de pubspec.yaml pendant les builds.

Conventions de mise en cache

Stockez les fichiers de cache transitoires dans .dart_tool/<package_name>/. N'écrivez jamais de fichiers de cache persistants directement à la racine du projet.


4. Analyse des arguments et routage des commandes

Importez package:args pour gérer les arguments de ligne de commande :

  • Scripts simples : Utilisez ArgParser directement avec addFlag() et addOption().
  • Outils multi-commandes : Implémentez CommandRunner<int> et étendez Command<int> pour chaque sous-commande, en retournant directement les codes de sortie POSIX.
  • Accesseurs typés : Utilisez results.flag('name'), results.option('name') et results.multiOption('name') (disponibles dans package:args 2.5+) au lieu de l'indexation de carte operator [] pour éliminer les casts de type manuels (as bool, as String?).
  • Modèles d'options complexes : Pour les applications avec de nombreux drapeaux, utilisez package:build_cli pour générer des classes d'options fortement typées. Tirez parti des remplacements par défaut nommés (par exemple {String? hostDefaultOverride}) pour fusionner proprement les fichiers de configuration avec les drapeaux CLI.

5. Async natif et traces de pile modernes

  • Éviter Chain.capture() : La VM Dart préserve nativement les trames de pile asynchrones à travers les points de suspension await. Chain.capture enveloppe la boucle d'événements dans des Zones personnalisées, entraînant une surcharge d'allocation substantielle et piégeant les erreurs à travers les limites de Zone.
  • Assainir avec Trace.from(st).terse : Utilisez les utilitaires statiques de package:stack_trace sur les erreurs non capturées sans capturer de zones :
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('Erreur fatale : $e');
    if (args.contains('-v') || args.contains('--verbose')) {
      stderr.writeln(Trace.from(st).terse);
    }
    exitCode = ExitCode.software.code;
  }
}

6. Lancement de sous-processus et résilience AOT

Lors du lancement de sous-processus du SDK Dart ou de l'exécution d'autres outils Dart (par exemple, dart format, dart test, build_runner) :

  • Ne présumez pas que Platform.resolvedExecutable ou Platform.executable pointe vers l'exécutable en ligne de commande dart : Dans les binaires compilés AOT autonomes (dart install / dart compile exe), resolvedExecutable pointe vers le binaire de l'application compilée elle-même, provoquant des boucles d'auto-invocation récursives ou des crashes de rejet de drapeau.
  • Utilisez package:cli_util : Résolvez l'exécutable du SDK Dart en utilisant cli_util.dartExecutable ou cli_util.sdkPath au lieu d'écrire des scrapers de PATH ou de répertoire personnalisés.
  • Voir les exigences de version et le guide technique détaillé dans references/aot_sdk_discovery.md.

7. Gestion des signaux et nettoyage du terminal

Si votre CLI modifie les modes du terminal, affiche des spinners ou ouvre des sockets d'écoute :

  • Garde des signaux Windows : Sur Windows, ProcessSignal.sigterm.watch() lève UnsupportedError. Protégez sigterm avec if (!Platform.isWindows).
  • Nettoyage de l'écho et du mode ligne : Si vous définissez stdin.echoMode = false ou stdin.lineMode = false, vérifiez d'abord if (!stdin.hasTerminal) return;, et installez un écouteur SIGINT et un bloc finally pour les restaurer afin que les frappes utilisateur restent visibles après la sortie.
  • Visibilité du curseur : Si vous émettez un curseur masqué ANSI (\x1B[?25l), restaurez toujours la visibilité du curseur (\x1B[?25h) à la sortie ou à l'annulation.
  • Nettoyage des sockets : Fermez explicitement les instances HttpServer ou ServerSocket en écoute (server.close(force: true)) sur les signaux de terminaison pour libérer immédiatement les ports OS.
  • Voir les motifs détaillés dans references/signals_and_terminal.md.

8. Test des applications CLI

Structurez les tests à travers deux couches distinctes :

  1. Tests unitaires (en mémoire, < 5ms) : Testez les classes de commandes, l'analyse des options et la logique métier directement en important package:<pkg>/src/... dans test/.
  2. Tests d'intégration (sous-processus) : Utilisez package:test_process et package:test_descriptor pour vérifier l'exécution de bout en bout du binaire, le flux I/O du processus et les codes de sortie OS :
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 traite l\'entrée et se termine proprement', () async {
    await d.file('input.txt', 'hello').create();

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

    await expectLater(process.stdout, emitsThrough('Traitement terminé.'));
    await process.shouldExit(0);
  });
}

9. Compilation et distribution modernes

Dart 3.12+ standardise la distribution CLI autour de dart run et dart install (en s'éloignant de dart pub global activate) :

  • Exécution éphémère (JIT) : dart run <package>@<version> [args] télécharge et exécute la CLI à la demande.
  • Installation globale (AOT natif) : dart install <package> compile le point d'entrée du package en un binaire natif autonome rapide dans ~/.dart/install/bin/.
  • Développement local : Utilisez dart run <command> (résout via executables: dans pubspec.yaml) ou dart run bin/cli.dart.
  • Regroupement de bibliothèques dynamiques et d'actifs de code : Utilisez dart build cli. La sortie du bundle est dans build/cli/_/bundle/.
  • Compilation d'exécutable autonome : Utilisez dart compile exe bin/cli.dart -o <output_path>.

10. Flux de travail et liste de contrôle d'audit

Flux de travail d'implémentation

  • Déclarez les points d'entrée dans pubspec.yaml sous executables:.
  • Gardez bin/*.dart comme un point d'entrée mince ; placez la logique de commande dans lib/src/.
  • Retournez des codes de sortie entiers ou définissez exitCode = N ; évitez exit(N) brut.
  • Dirigez les erreurs, avertissements et l'utilisation en cas d'échec d'analyse vers stderr.
  • Utilisez results.flag(), results.option() et results.multiOption() pour la sécurité de type.
  • Validez useAnsi (en vérifiant stdout.hasTerminal, supportsAnsiEscapes et NO_COLOR) avant d'émettre des codes ANSI.
  • Lancez les outils enfants en utilisant cli_util.dartExecutable, jamais Platform.resolvedExecutable.
  • Testez unitairement les runners de commandes en mémoire ; testez l'exécution de bout en bout du binaire avec test_process.

Références et exemples

Individual skills in this repo

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

Skills associés