Communitygithub.com

Construção de Aplicações CLI em Dart

Padrões arquiteturais, estrutura de ponto de entrada, códigos de saída, roteamento de streams e criação de subprocessos para aplicações de linha de comando (CLI) em Dart.

O que é Construção de Aplicações CLI em Dart?

A habilidade 'Construção de Aplicações CLI em Dart' (photoeven2-netizen/astronusa) é um guia abrangente para desenvolvedores que desejam criar ferramentas de linha de comando robustas e profissionais utilizando Dart. Ela abrange desde padrões arquiteturais, como o padrão de ponto de entrada `Thin Entrypoint` e a separação clara entre lógica de negócios em `lib/src/` e scripts de inicialização em `bin/`, até boas práticas para o ciclo de vida de processos. A habilidade enfatiza a importância de evitar chamadas destrutivas como `exit(N)`, promovendo o uso de `exitCode` ou códigos de retorno inteiros para garantir um desligamento limpo, preservando buffers e recursos. Além disso, o guia detalha o uso de bibliotecas essenciais do ecossistema Dart, como `package:args` para análise de argumentos e roteamento de comandos, e `package:cli_util` para descoberta robusta do SDK Dart, especialmente crítica em binários compilados AOT. Tópicos avançados incluem manipulação de sinais do SO (como SIGINT), detecção de capacidades terminais (como suporte a ANSI) e o padrão `NO_COLOR`, formatação de saída e diagnóstico em streams separadas (stdout para dados, stderr para logs), e criação de subprocessos sem incorrer em loops de auto-invocação. A habilidade é voltada exclusivamente para aplicações de linha de comando Dart, não se aplicando a interfaces Flutter, aplicações web ou servidores HTTP. Ela suporta as plataformas de desenvolvimento onde Dart é executado (Linux, macOS, Windows) e fornece modelos de código e guias de referência para acelerar o desenvolvimento, incluindo padrões para testes unitários e de integração, compilação moderna (JIT, AOT via `dart install` ou `dart compile exe`) e distribuição. Em suma, é um recurso indispensável para qualquer desenvolvedor que busca construir CLIs Dart com qualidade de produção.

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

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Documentação

O que Construção de Aplicações CLI em Dart faz?

Conteúdo


1. Arquitetura Central e Ciclo de Vida do Processo

Evite Saídas Destrutivas (exit(N))

Chamar exit(int code) de dart:io invoca Platform::Exit(code) no runtime C++. Ele termina o processo do SO imediatamente sem desenrolar a pilha Dart:

  • Desconexão do Depurador: Quando iniciado com --pause-isolates-on-exit, o VM Service pausa os isolates antes do encerramento para permitir a inspeção na IDE. exit() termina o processo do SO antes que o VM Service possa pausar ou inspecionar o estado.
  • Perda de Cobertura: package:coverage consulta linhas de execução através de RPCs do VM Service durante o estado pausado no exit. exit() destrói o processo antes da extração das RPCs, resultando em cobertura de 0%.
  • Truncamento de Buffer: stdout e stderr são streams IOSink assíncronas com buffer. exit() descarta bytes não descarregados (flushed).
  • Vazamento de Recursos: Blocos finally (fechando locks, deletando diretórios temporários) são ignorados.

Regra: Evite chamar exit(code) diretamente durante a execução normal; defina exitCode = code ou retorne um código de saída inteiro de CommandRunner<int> (de package:args) e permita que a função main() assíncrona retorne naturalmente. Não chame exit() em erros não tratados; lance um Error ou exceção não tratada para que o runtime desenrole de forma limpa e saia com um status diferente de zero.

Códigos de saída POSIX padrão (/usr/include/sysexits.h):

  • 0: Sucesso (EX_OK / ExitCode.success.code)
  • 64: Erro de uso da linha de comando (EX_USAGE / ExitCode.usage.code)
  • 65: Erro de formato de dados (EX_DATAERR / ExitCode.data.code)
  • 70: Falha interna de software (EX_SOFTWARE / ExitCode.software.code)
  • 78: Erro de configuração (EX_CONFIG / ExitCode.config.code)

Nota: Prefira importar package:io/io.dart e usar as constantes ExitCode (ex., ExitCode.usage.code, ExitCode.software.code) em vez de números inteiros mágicos. Para scripts independentes mínimos sem dependências de pacotes, os inteiros POSIX padrão (0, 64, 70) podem ser usados.

import 'dart:io';
import 'package:args/command_runner.dart';
import 'package:io/io.dart' show ExitCode; // Fornece constantes POSIX padrão ExitCode

Future<void> main(List<String> args) async {
  final runner = CommandRunner<int>('tool', 'Descrição da ferramenta 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;
  }
}

O Padrão de Ponto de Entrada Enxuto (bin/ vs. lib/src/)

Mantenha os arquivos bin/*.dart estritamente como trampolins de ponto de entrada mínimos (instanciar runner, passar args, aguardar código de saída). Coloque todas as definições de comando, analisadores de argumentos, formatadores e lógica de negócios dentro de lib/src/.

  • Justificativa: Código em bin/ não pode ser importado de forma limpa via URIs package:. Mover a lógica para lib/src/ permite testar unitariamente todo o runner de comandos, hierarquia de subcomandos e lógica de negócios em memória em milissegundos (< 2ms) sem gerar subprocessos do SO.
// bin/my_cli.dart — Trampolim de ponto de entrada enxuto
import 'dart:io';
import 'package:my_cli/src/cli.dart';

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

2. Saída, Diagnósticos e Formatação

  • Dados vs. Diagnósticos: Escreva resultados pretendidos do programa e dados legíveis por máquina exclusivamente no stdout. Escreva avisos, mensagens de erro e logs de depuração exclusivamente no stderr.
  • A Regra de Erro de Uso: Quando ocorrer um erro de análise de argumentos ou de opção obrigatória (FormatException, UsageException, ou ArgumentError lançado ao acessar uma opção mandatory: true ausente via results.option(...)), tanto a mensagem de erro quanto o texto de uso devem escrever para stderr, e o código de saída 64 (EX_USAGE / ExitCode.usage.code) deve ser retornado. stdout só deve receber ajuda de uso quando o usuário a solicitar explicitamente via --help ou -h.
  • Sem print() em Manipuladores de Erro: print() roteia para stdout. Use stderr.writeln() para todas as notificações de falha. Para saída padrão, prefira stdout.writeln() em vez de print() para cumprir a regra de lint avoid_print (a menos que analysis_options.yaml configure explicitamente avoid_print: false).
  • Detecção de Capacidade do Terminal e NO_COLOR: Verifique stdout.hasTerminal, stdout.supportsAnsiEscapes, e !Platform.environment.containsKey('NO_COLOR') antes de emitir códigos de escape ANSI de cor ou cursor:
    bool get useAnsi =>
        stdout.hasTerminal &&
        stdout.supportsAnsiEscapes &&
        !Platform.environment.containsKey('NO_COLOR');
    
  • Modos Legíveis por Máquina: Quando os sinalizadores --json ou --machine forem passados, formate os dados como JSON para stdout e direcione os logs para stderr.

3. Configuração e Empacotamento do Projeto

Scaffolding e Mapeamento de Executáveis no Pubspec (executáveis:)

Crie o scaffolding de novos projetos de linha de comando usando dart create -t console <package_name>, que inicializa o layout padrão bin/ e lib/. Sempre declare executáveis em pubspec.yaml sob executables: para mapear nomes de comandos para scripts em bin/, permitindo invocação limpa via dart run <command> (sem especificar bin/...dart) e configurando links simbólicos binários globais para dart install:

name: my_cli
description: Ferramenta CLI de alto desempenho.
version: 1.0.0

executables:
  my_cli: # Mapeia para bin/my_cli.dart
  secondary_cmd: helper # Mapeia para bin/helper.dart

Versionamento de Fonte Única (package:build_version)

Evite codificar strings --version em bin/*.dart ou sincronizar manualmente arquivos de constantes. Use package:build_version para gerar lib/src/version.dart contendo const packageVersion = 'x.y.z'; diretamente de pubspec.yaml durante as compilações.

Convenções de Cache

Armazene arquivos de cache transitórios em .dart_tool/<package_name>/. Nunca escreva arquivos de cache persistentes diretamente na raiz do projeto.


4. Análise de Argumentos e Roteamento de Comandos

Importe package:args para gerenciar argumentos de linha de comando:

  • Scripts Simples: Use ArgParser diretamente com addFlag() e addOption().
  • Ferramentas com Múltiplos Comandos: Implemente CommandRunner<int> e estenda Command<int> para cada subcomando, retornando códigos de saída POSIX diretamente.
  • Acessadores Type-Safe: Use results.flag('name'), results.option('name') e results.multiOption('name') (disponíveis no package:args 2.5+) em vez da indexação por mapa operator [] para eliminar casts de tipo manuais (as bool, as String?).
  • Modelos de Opções Complexas: Para aplicações com muitas flags, use package:build_cli para gerar classes de opções fortemente tipadas. Aproveite substituições de padrão nomeadas (ex. {String? hostDefaultOverride}) para mesclar limpa e eficientemente arquivos de configuração com flags da CLI.

5. Async Nativo e Stack Traces Modernos

  • Evite Chain.capture(): A VM Dart preserva nativamente os quadros de pilha assíncronos entre pontos de suspensão await. Chain.capture envolve o loop de eventos em Zonas personalizadas, incorrendo em sobrecarga substancial de alocação e capturando erros através de limites de Zona.
  • Sanitize com Trace.from(st).terse: Use utilitários estáticos de package:stack_trace em erros não capturados sem capturar zonas:
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('Erro fatal: $e');
    if (args.contains('-v') || args.contains('--verbose')) {
      stderr.writeln(Trace.from(st).terse);
    }
    exitCode = ExitCode.software.code;
  }
}

6. Criação de Subprocessos e Resiliência AOT

Ao gerar subprocessos do Dart SDK ou executar outras ferramentas Dart (ex., dart format, dart test, build_runner):

  • Não presuma que Platform.resolvedExecutable ou Platform.executable aponte para o executável de linha de comando dart: Em binários autônomos compilados AOT (dart install / dart compile exe), resolvedExecutable aponta para o binário da aplicação compilada em si, causando loops de auto-invocação recursiva ou falhas de rejeição de flag.
  • Use package:cli_util: Resolva o executável do SDK Dart usando cli_util.dartExecutable ou cli_util.sdkPath em vez de escrever scrapers de diretório ou PATH personalizados.
  • Consulte os requisitos de versão e o guia técnico detalhado em referências/aot_sdk_discovery.md.

7. Manipulação de Sinais e Desligamento do Terminal

Se sua CLI alterar modos de terminal, exibir spinners ou abrir sockets de escuta:

  • Proteção para Windows: No Windows, ProcessSignal.sigterm.watch() lança UnsupportedError. Proteja sigterm com if (!Platform.isWindows).
  • Desligamento de Eco e Modo de Linha: Se estiver definindo stdin.echoMode = false ou stdin.lineMode = false, verifique if (!stdin.hasTerminal) return; primeiro e instale um listener SIGINT e um bloco finally para restaurá-los, para que as teclas do usuário permaneçam visíveis após a saída.
  • Visibilidade do Cursor: Se emitir ANSI para esconder cursor (\x1B[?25l), sempre restaure a visibilidade do cursor (\x1B[?25h) ao sair ou cancelar.
  • Limpeza de Socket: Feche explicitamente instâncias de HttpServer ou ServerSocket em escuta (server.close(force: true)) em sinais de término para liberar portas do SO imediatamente.
  • Consulte padrões detalhados em referências/sinais_e_terminal.md.

8. Testando Aplicações CLI

Estruture os testes em duas camadas distintas:

  1. Testes Unitários (Em Memória, < 5ms): Teste classes de comando, análise de opções e lógica de negócios diretamente importando package:<pkg>/src/... em test/.
  2. Testes de Integração (Subprocessos): Use package:test_process e package:test_descriptor para verificar a execução binária de ponta a ponta, streaming de E/S do processo e códigos de saída do 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('CLI processa entrada e sai de forma limpa', () async {
    await d.file('input.txt', 'olá').create();

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

    await expectLater(process.stdout, emitsThrough('Processamento concluído.'));
    await process.shouldExit(0);
  });
}

9. Compilação e Distribuição Modernas

Dart 3.12+ padroniza a distribuição de CLI em torno de dart run e dart install (afastando-se de dart pub global activate):

  • Execução Efêmera (JIT): dart run <package>@<version> [args] baixa e executa a CLI sob demanda.
  • Instalação Global (AOT Nativo): dart install <package> compila o ponto de entrada do pacote em um binário nativo autônomo rápido em ~/.dart/install/bin/.
  • Desenvolvimento Local: Use dart run <command> (resolve via executables: em pubspec.yaml) ou dart run bin/cli.dart.
  • Agrupando Bibliotecas Dinâmicas e Assets de Código: Use dart build cli. Saída do pacote para build/cli/_/bundle/.
  • Compilação de Executável Autônomo: Use dart compile exe bin/cli.dart -o <caminho_saida>.

10. Fluxos de Trabalho e Lista de Verificação

Fluxo de Trabalho de Implementação

  • Declare pontos de entrada em pubspec.yaml sob executables:.
  • Mantenha bin/*.dart como um ponto de entrada enxuto; coloque a lógica do comando em lib/src/.
  • Retorne códigos de saída inteiros ou defina exitCode = N; evite exit(N) bruto.
  • Direcione erros, avisos e uso em falha de análise para stderr.
  • Use results.flag(), results.option() e results.multiOption() para segurança de tipo.
  • Valide useAnsi (verificando stdout.hasTerminal, supportsAnsiEscapes e NO_COLOR) antes de emitir códigos ANSI.
  • Gere ferramentas filhas usando cli_util.dartExecutable, nunca Platform.resolvedExecutable.
  • Teste unitariamente os runners de comando em memória; teste a execução binária de ponta a ponta com test_process.

Referências e Exemplos

Individual skills in this repo

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

Habilidades Relacionadas