Communitygithub.com

photoeven2-netizen/astronusa

Patrones arquitectónicos, estructura de puntos de entrada, códigos de salida, enrutamiento de flujos y lanzamiento de subprocesos para aplicaciones de línea de comandos (CLI) en Dart.

¿Qué es astronusa?

Esta habilidad proporciona una guía completa y detallada para construir aplicaciones CLI robustas y profesionales en Dart. Cubre desde la arquitectura central y el ciclo de vida del proceso, evitando llamadas destructivas a `exit()`, hasta el manejo adecuado de flujos de salida y diagnósticos, donde se enfatiza la separación entre datos estándar y mensajes de error. Además, incluye secciones sobre configuración de proyectos con `pubspec.yaml`, análisis de argumentos con el paquete `args`, manejo moderno de asincronía y trazas de pila, lanzamiento de subprocesos (subprocess) con resiliencia en compilaciones AOT, y el manejo de señales del sistema operativo para una correcta limpieza de terminal. También se detallan estrategias de prueba en dos capas (unitarias y de integración) y las opciones modernas de compilación y distribución con Dart 3.12+. Es útil para cualquier desarrollador que construya herramientas de consola, utilidades de línea de comandos, scripts o que necesite analizar argumentos con `package:args` y manejar códigos de salida POSIX. No es apropiado para interfaces de usuario Flutter, aplicaciones web o servidores backend HTTP independientes.

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

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

Construcción de Aplicaciones CLI en Dart

Contenidos


1. Arquitectura Central y Ciclo de Vida del Proceso

Evitar Salidas Destructivas (exit(N))

Llamar a exit(int code) de dart:io invoca Platform::Exit(code) en el tiempo de ejecución de C++. Termina inmediatamente el proceso del sistema operativo sin desenrollar la pila de Dart:

  • Desconexión del Depurador: Cuando se inicia con --pause-isolates-on-exit, el Servicio VM pausa los aislamientos antes del apagado para permitir la inspección del IDE. exit() termina el proceso del SO antes de que el Servicio VM pueda pausar o inspeccionar el estado.
  • Pérdida de Cobertura: package:coverage consulta las líneas de ejecución a través de RPC del Servicio VM durante el estado de pausa en la salida. exit() destruye el proceso antes de la extracción de RPC, dando como resultado una cobertura del 0%.
  • Truncamiento de Búfer: stdout y stderr son flujos IOSink asíncronos almacenados en búfer. exit() descarta los bytes no vaciados.
  • Fugas de Recursos: Los bloques finally (cerrar bloqueos, eliminar directorios temporales) se omiten.

Regla: Evita llamar a exit(code) directamente durante la ejecución normal; establece exitCode = code o devuelve un código de salida entero desde CommandRunner<int> (de package:args) y permite que la función main() asíncrona retorne de forma natural. No llames a exit() en errores no manejados; lanza un Error o excepción no manejado para que el tiempo de ejecución se desenrolle limpiamente y salga con un estado distinto de cero.

Códigos de salida POSIX estándar (/usr/include/sysexits.h):

  • 0: Éxito (EX_OK / ExitCode.success.code)
  • 64: Error de uso de línea de comandos (EX_USAGE / ExitCode.usage.code)
  • 65: Error de formato de datos (EX_DATAERR / ExitCode.data.code)
  • 70: Fallo interno de software (EX_SOFTWARE / ExitCode.software.code)
  • 78: Error de configuración (EX_CONFIG / ExitCode.config.code)

Nota: Prefiere importar package:io/io.dart y usar las constantes ExitCode (por ejemplo, ExitCode.usage.code, ExitCode.software.code) en lugar de literales enteros mágicos. Para scripts independientes mínimos sin dependencias de paquetes, se pueden usar literales enteros POSIX estándar (0, 64, 70).

import 'dart:io';
import 'package:args/command_runner.dart';
import 'package:io/io.dart' show ExitCode; // Proporciona constantes POSIX estándar de ExitCode

Future<void> main(List<String> args) async {
  final runner = CommandRunner<int>('tool', 'Descripción de la herramienta 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;
  }
}

El Patrón de Punto de Entrada Delgado (bin/ vs. lib/src/)

Mantén los archivos bin/*.dart estrictamente como trampolines de punto de entrada mínimos (instanciar el runner, pasar args, esperar el código de salida). Coloca todas las definiciones de comandos, analizadores de argumentos, formateadores y lógica de negocio dentro de lib/src/.

  • Justificación: El código en bin/ no se puede importar limpiamente mediante URIs package:. Mover la lógica a lib/src/ permite que todo el runner de comandos, la jerarquía de subcomandos y la lógica de negocio se prueben unitariamente en memoria en milisegundos (< 2ms) sin lanzar subprocesos del SO.
// bin/my_cli.dart — Trampolín de punto de entrada delgado
import 'dart:io';
import 'package:my_cli/src/cli.dart';

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

2. Flujos, Diagnósticos y Formateo

  • Datos vs. Diagnósticos: Escribe los resultados previstos del programa y los datos legibles por máquina exclusivamente en stdout. Escribe advertencias, mensajes de error y registros de depuración exclusivamente en stderr.
  • La Regla de Error de Uso: Cuando ocurre un error de análisis de argumentos o de opción obligatoria (FormatException, UsageException o ArgumentError lanzado al acceder a una opción mandatory: true faltante a través de results.option(...)), tanto el mensaje de error como el texto de uso deben escribirse en stderr, y se debe devolver el código de salida 64 (EX_USAGE / ExitCode.usage.code). stdout SOLO debe recibir ayuda de uso cuando el usuario la solicite explícitamente a través de --help o -h.
  • Sin print() en Manejadores de Error: print() dirige a stdout. Usa stderr.writeln() para todas las notificaciones de fallo. Para la salida estándar, prefiere stdout.writeln() sobre print() para cumplir con la regla lint avoid_print (a menos que analysis_options.yaml configure explícitamente avoid_print: false).
  • Detección de Capacidad del Terminal y NO_COLOR: Verifica stdout.hasTerminal, stdout.supportsAnsiEscapes y !Platform.environment.containsKey('NO_COLOR') antes de emitir códigos de escape ANSI de color o cursor:
    bool get useAnsi =>
        stdout.hasTerminal &&
        stdout.supportsAnsiEscapes &&
        !Platform.environment.containsKey('NO_COLOR');
    
  • Modos Legibles por Máquina: Cuando se pasan las banderas --json o --machine, formatea los datos como JSON a stdout y dirige los registros a stderr.

3. Configuración del Proyecto y Empaquetado

Andamiaje y Mapeo de Ejecutables de Pubspec (executables:)

Andamia nuevos proyectos de línea de comandos usando dart create -t console <nombre_paquete>, que inicializa el diseño estándar bin/ y lib/. Siempre declara los ejecutables en pubspec.yaml bajo executables: para mapear nombres de comandos a scripts en bin/, lo que permite una invocación limpia a través de dart run <comando> (sin especificar bin/archivo.dart) y configura enlaces simbólicos binarios globales para dart install:

name: my_cli
description: Herramienta CLI de alto rendimiento.
version: 1.0.0

executables:
  my_cli: # Mapea a bin/my_cli.dart
  secondary_cmd: helper # Mapea a bin/helper.dart

Versionado de Fuente Única (package:build_version)

Evita codificar cadenas --version en bin/*.dart o sincronizar manualmente archivos de constantes. Usa package:build_version para generar lib/src/version.dart que contenga const packageVersion = 'x.y.z'; directamente desde pubspec.yaml durante las compilaciones.

Convenciones de Caché

Almacena archivos de caché transitorios en .dart_tool/<nombre_paquete>/. Nunca escribas archivos de caché persistentes directamente en la raíz del proyecto.


4. Análisis de Argumentos y Enrutamiento de Comandos

Importa package:args para gestionar los argumentos de línea de comandos:

  • Scripts Simples: Usa ArgParser directamente con addFlag() y addOption().
  • Herramientas de Múltiples Comandos: Implementa CommandRunner<int> y extiende Command<int> para cada subcomando, devolviendo códigos de salida POSIX directamente.
  • Accesores Seguros en Tipo: Usa results.flag('name'), results.option('name') y results.multiOption('name') (disponibles en package:args 2.5+) en lugar del indexado de mapas operator [] para eliminar conversiones de tipo manuales (as bool, as String?).
  • Modelos de Opciones Complejos: Para aplicaciones con banderas extensas, usa package:build_cli para generar clases de opciones fuertemente tipadas. Aprovecha las anulaciones predeterminadas con nombre (por ejemplo, {String? hostDefaultOverride}) para fusionar limpiamente los archivos de configuración con las banderas CLI.

5. Asincronía Nativa y Trazas de Pila Modernas

  • Evita Chain.capture(): La VM de Dart preserva de forma nativa los marcos de pila asíncronos a través de los puntos de suspensión await. Chain.capture envuelve el bucle de eventos en Zones personalizadas, incurriendo en una sobrecarga de asignación sustancial y atrapando errores a través de los límites de Zone.
  • Sanitiza con Trace.from(st).terse: Usa utilidades estáticas de package:stack_trace en errores no capturados sin capturar 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('Error fatal: $e');
    if (args.contains('-v') || args.contains('--verbose')) {
      stderr.writeln(Trace.from(st).terse);
    }
    exitCode = ExitCode.software.code;
  }
}

6. Lanzamiento de Subprocesos y Resiliencia AOT

Al lanzar subprocesos del SDK de Dart o ejecutar otras herramientas de Dart (por ejemplo, dart format, dart test, build_runner):

  • No asumas que Platform.resolvedExecutable o Platform.executable apunta al ejecutable de línea de comandos dart: En binarios compilados AOT independientes (dart install / dart compile exe), resolvedExecutable apunta al binario de la aplicación compilada en sí, causando bucles de auto-invocación recursiva o fallos de rechazo de banderas.
  • Usa package:cli_util: Resuelve el ejecutable del SDK de Dart usando cli_util.dartExecutable o cli_util.sdkPath en lugar de escribir scripts personalizados de búsqueda en PATH o directorios.
  • Consulta los requisitos de versión y la guía técnica detallada en references/aot_sdk_discovery.md.

7. Manejo de Señales y Desmontaje de Terminal

Si tu CLI altera los modos del terminal, muestra indicadores de progreso o abre sockets de escucha:

  • Protección de Señal en Windows: En Windows, ProcessSignal.sigterm.watch() lanza UnsupportedError. Protege sigterm con if (!Platform.isWindows).
  • Desmontaje de Eco y Modo Línea: Si estableces stdin.echoMode = false o stdin.lineMode = false, verifica primero if (!stdin.hasTerminal) return;, e instala un listener SIGINT y un bloque finally para restaurarlos, de modo que las pulsaciones de teclas del usuario permanezcan visibles después de la salida.
  • Visibilidad del Cursor: Si emites ocultación de cursor ANSI (\x1B[?25l), siempre restaura la visibilidad del cursor (\x1B[?25h) al salir o cancelar.
  • Limpieza de Sockets: Cierra explícitamente las instancias de HttpServer o ServerSocket en escucha (server.close(force: true)) en las señales de terminación para liberar inmediatamente los puertos del SO.
  • Consulta los patrones detallados en references/signals_and_terminal.md.

8. Pruebas de Aplicaciones CLI

Estructura las pruebas en dos capas distintas:

  1. Pruebas Unitarias (En Memoria, < 5ms): Prueba las clases de comando, el análisis de opciones y la lógica de negocio directamente importando package:<pkg>/src/... en test/.
  2. Pruebas de Integración (Subprocesos): Usa package:test_process y package:test_descriptor para verificar la ejecución binaria de extremo a extremo, la transmisión de E/S del proceso y los códigos de salida del SO:
import 'package:test/test.dart';
import 'package:test_descriptor/test_descriptor.dart' as d;
import 'package:test_process/test_process.dart';

void main() {
  test('La CLI procesa la entrada y sale limpiamente', () async {
    await d.file('input.txt', 'hola').create();

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

    await expectLater(process.stdout, emitsThrough('Procesamiento completo.'));
    await process.shouldExit(0);
  });
}

9. Compilación y Distribución Modernas

Dart 3.12+ estandariza la distribución CLI en torno a dart run y dart install (alejándose de dart pub global activate):

  • Ejecución Efímera (JIT): dart run <paquete>@<version> [args] descarga y ejecuta la CLI bajo demanda.
  • Instalación Global (AOT Nativa): dart install <paquete> compila el punto de entrada del paquete en un binario independiente nativo rápido en ~/.dart/install/bin/.
  • Desarrollo Local: Usa dart run <comando> (se resuelve a través de executables: en pubspec.yaml) o dart run bin/cli.dart.
  • Agrupación de Bibliotecas Dinámicas y Activos de Código: Usa dart build cli. Envía el paquete a build/cli/_/bundle/.
  • Compilación de Ejecutable Independiente: Usa dart compile exe bin/cli.dart -o <ruta_salida>.

10. Flujos de Trabajo y Lista de Verificación de Auditoría

Flujo de Trabajo de Implementación

  • Declarar puntos de entrada en pubspec.yaml bajo executables:.
  • Mantener bin/*.dart como un punto de entrada delgado; colocar la lógica de comandos en lib/src/.
  • Devolver códigos de salida enteros o establecer exitCode = N; evitar exit(N) directo.
  • Dirigir errores, advertencias y el uso en caso de fallo de análisis a stderr.
  • Usar results.flag(), results.option() y results.multiOption() para seguridad de tipos.
  • Validar useAnsi (verificando stdout.hasTerminal, supportsAnsiEscapes y NO_COLOR) antes de emitir códigos ANSI.
  • Lanzar herramientas hijas usando cli_util.dartExecutable, nunca Platform.resolvedExecutable.
  • Probar unitariamente los runners de comandos en memoria; probar la ejecución binaria de extremo a extremo con test_process.

Referencias y Ejemplos

Individual skills in this repo

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

Skills relacionados