Communitygithub.com

photoeven2-netizen/astronusa

適用於 Dart 命令列介面 (CLI) 應用程式的架構模式、進入點結構、結束代碼、串流路由與子程序生成。當建置 CLI 工具、主控台公用程式、腳本、使用 `package:args` (ArgParser 或 CommandRunner) 處理引數、處理結束代碼、在 pubspec.yaml 中設定可執行檔、生成 Dart 子程序或編譯原生 CLI 二進位檔時使用。請勿用於 Flutter UI 小工具、網頁應用程式或獨立 HTTP 後端伺服器。

astronusa 是什麼?

此技能提供了一組全面的準則和範本,用於建立穩健、可維護的 Dart 命令列應用程式。它涵蓋了關鍵的架構決策,例如為什麼要避免使用 `exit(N)` 以及如何正確地讓應用程式終止,同時也涵蓋了將業務邏輯與簡潔的進入點分離的「精簡進入點模式」。此技能詳細說明了使用 `package:args` 進行引數解析、使用 `package:io` 管理標準的 POSIX 結束代碼,以及使用 `package:cli_util` 以 AOT 感知的方式產生子程序的規則。受支援的平台包括 Dart VM 以及透過 `dart compile exe` 和 `dart install` 編譯的原生二進位檔。此技能是根據其詳細的 SKILL.md 文件提煉而成,該文件明確定義了哪些內容應輸出到 stdout vs stderr、如何處理終端機功能 (ANSI、`NO_COLOR`) 以及如何以機器可讀的格式輸出。它還提供了針對 CLI 應用程式的特定測試策略,區分了快速的記憶體內單元測試與使用 `test_process` 的端對端子程序測試。最後,它概述了使用 `dart run`、`dart install` 和 `dart compile exe` 進行現代化編譯與分發的實務。

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

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

建置 Dart CLI 應用程式

目錄


1. 核心架構與程序生命週期

避免破壞性結束 (exit(N))

呼叫 dart:io 的 exit(int code) 會觸發 C++ 執行階段的 Platform::Exit(code)。它會立即終止作業系統程序,而不會 unwinding Dart 堆疊:

  • 除錯器中斷連線:當使用 --pause-isolates-on-exit 啟動時,VM 服務會在關機前暫停 isolates 以允許 IDE 檢查。exit() 在 VM 服務可以暫停或檢查狀態之前終止了作業系統程序。
  • 涵蓋率遺失:package:coverage 會在暫停結束狀態期間透過 VM 服務 RPC 查詢執行行數。exit() 在 RPC 提取前銷毀程序,導致 0% 涵蓋率。
  • 緩衝區截斷:stdout 和 stderr 是緩衝的非同步 IOSink 串流。exit() 會丟棄未刷新的位元組。
  • 資源洩漏:finally 區塊 (關閉鎖定、刪除暫存目錄) 會被繞過。

規則:避免在正常執行期間直接呼叫 exit(code);應設定 exitCode = code 或從 CommandRunner<int> (來自 package:args) 回傳整數結束代碼,並允許非同步的 main() 函數自然回傳。不要在未處理的錯誤上呼叫 exit();應擲出未處理的 Error 或例外,以便執行階段乾淨地 unwinding 並以非零狀態結束。

標準 POSIX 結束代碼 (/usr/include/sysexits.h):

  • 0:成功 (EX_OK / ExitCode.success.code)
  • 64:命令列使用錯誤 (EX_USAGE / ExitCode.usage.code)
  • 65:資料格式錯誤 (EX_DATAERR / ExitCode.data.code)
  • 70:內部軟體當機 (EX_SOFTWARE / ExitCode.software.code)
  • 78:設定錯誤 (EX_CONFIG / ExitCode.config.code)

注意:建議匯入 package:io/io.dart 並使用 ExitCode 常數 (例如,ExitCode.usage.code、ExitCode.software.code),而不是使用魔術整數字面值。對無套件相依性的最小獨立腳本,可以使用標準 POSIX 整數字面值 (0、64、70)。

import 'dart:io';
import 'package:args/command_runner.dart';
import 'package:io/io.dart' show ExitCode; // 提供標準 POSIX ExitCode 常數

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

精簡進入點模式 (bin/ vs. lib/src/)

保持 bin/*.dart 檔案嚴格作為精簡的進入點跳板 (實例化 runner、傳遞 args、等待結束代碼)。將所有命令定義、引數解析器、格式化程式和業務邏輯放在 lib/src/ 中。

  • 理由:bin/ 中的程式碼無法透過 package: URI 乾淨地匯入。將邏輯移到 lib/src/ 中,允許在毫秒內 (< 2ms) 於記憶體中對整個命令 runner、子命令階層和業務邏輯進行單元測試,而無需產生作業系統子程序。
// bin/my_cli.dart — 精簡進入點跳板
import 'dart:io';
import 'package:my_cli/src/cli.dart';

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

2. 輸出、診斷與格式化

  • 資料 vs. 診斷:將預期的程式結果和機器可讀資料僅寫入 stdout。將警告、錯誤訊息和除錯日誌僅寫入 stderr。
  • 錯誤使用規則:當發生引數解析或必要選項錯誤時 (FormatException、UsageException,或存取缺少的 mandatory: true 選項時透過 results.option(...) 拋出的 ArgumentError),錯誤訊息和使用說明文字都必須寫入 stderr,並且必須回傳結束代碼 64 (EX_USAGE / ExitCode.usage.code)。stdout 應該僅在使用者透過 --help 或 -h 明確請求時接收使用說明。
  • 錯誤處理程式中不要使用 print():print() 會導向 stdout。對所有失敗通知使用 stderr.writeln()。對於標準輸出,建議使用 stdout.writeln() 而不是 print(),以遵守 avoid_print lint 規則 (除非 analysis_options.yaml 明確設定了 avoid_print: false)。
  • 終端機功能偵測與 NO_COLOR:在發出 ANSI 色彩或游標跳脫碼之前,請驗證 stdout.hasTerminal、stdout.supportsAnsiEscapes 和 !Platform.environment.containsKey('NO_COLOR'):
    bool get useAnsi =>
        stdout.hasTerminal &&
        stdout.supportsAnsiEscapes &&
        !Platform.environment.containsKey('NO_COLOR');
    
  • 機器可讀模式:當傳遞 --json 或 --machine 標誌時,將資料格式化為 JSON 輸出到 stdout,並將日誌導向 stderr。

3. 專案設定與封裝

建構與 Pubspec 可執行檔對應 (executables:)

使用 dart create -t console <package_name> 建構新的命令列專案,這會初始化標準的 bin/ 和 lib/ 佈局。始終在 pubspec.yaml 的 executables: 下宣告可執行檔,將命令名稱對應到 bin/ 中的腳本,從而允許透過 dart run <command> 進行乾淨的呼叫 (無需指定 bin/...dart),並為 dart install 設定全域二進位符號連結:

name: my_cli
description: 高效能 CLI 工具。
version: 1.0.0

executables:
  my_cli: # 對應到 bin/my_cli.dart
  secondary_cmd: helper # 對應到 bin/helper.dart

單一來源版本控制 (package:build_version)

避免在 bin/*.dart 中硬編碼 --version 字串或手動同步常數檔案。使用 package:build_version 在建置期間從 pubspec.yaml 產生包含 const packageVersion = 'x.y.z'; 的 lib/src/version.dart 檔案。

快取慣例

將暫存快取檔案儲存在 .dart_tool/<package_name>/ 中。切勿將持久性快取檔案直接寫入專案根目錄。


4. 引數解析與命令路由

匯入 package:args 來管理命令列引數:

  • 簡單腳本:直接使用 ArgParser,搭配 addFlag() 和 addOption()。
  • 多命令工具:為每個子命令實作 CommandRunner<int> 並繼承 Command<int>,直接回傳 POSIX 結束代碼。
  • 型別安全的存取器:使用 results.flag('name')、results.option('name') 和 results.multiOption('name') (在 package:args 2.5+ 中可用) 來取代地圖索引 operator [],以消除手動型別轉換 (as bool、as String?)。
  • 複雜選項模型:對於具有大量標誌的應用程式,使用 package:build_cli 來產生強型別的選項類別。利用具名預設值覆寫 (例如 {String? hostDefaultOverride}) 來乾淨地合併設定檔與 CLI 標誌。

5. 原生非同步與現代堆疊追蹤

  • 避免 Chain.capture():Dart VM 原生地在 await 暫停點之間保留非同步堆疊幀。Chain.capture 將事件迴圈包裝在自訂的 Zone 中,會產生大量的分配開銷,並可能跨 Zone 邊界捕獲錯誤。
  • 使用 Trace.from(st).terse 清理:在未捕獲的錯誤上使用 package:stack_trace 中的靜態工具,而無需捕獲 zone:
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('致命錯誤:$e');
    if (args.contains('-v') || args.contains('--verbose')) {
      stderr.writeln(Trace.from(st).terse);
    }
    exitCode = ExitCode.software.code;
  }
}

6. 子程序生成與 AOT 韌性

當生成 Dart SDK 子程序或執行其他 Dart 工具時 (例如 dart format、dart test、build_runner):

  • 不要假設 Platform.resolvedExecutable 或 Platform.executable 指向 dart 命令列可執行檔:在獨立的 AOT 編譯二進位檔 (dart install / dart compile exe) 中,resolvedExecutable 指向已編譯的應用程式二進位檔本身,會導致遞迴的自我呼叫迴圈或標誌拒絕當機。
  • 使用 package:cli_util:使用 cli_util.dartExecutable 或 cli_util.sdkPath 解析 Dart SDK 可執行檔,而不是撰寫自訂的 PATH 或目錄搜尋器。
  • 請參閱 references/aot_sdk_discovery.md 中的版本要求和詳細技術指南。

7. 訊號處理與終端機清理

如果你的 CLI 會變更終端機模式、顯示旋轉動畫或開啟監聽 socket:

  • Windows 訊號防護:在 Windows 上,ProcessSignal.sigterm.watch() 會擲出 UnsupportedError。使用 if (!Platform.isWindows) 來保護 sigterm。
  • 回音與行模式清理:如果設定 stdin.echoMode = false 或 stdin.lineMode = false,請先檢查 if (!stdin.hasTerminal) return;,並安裝一個 SIGINT 監聽器和 finally 區塊來恢復它們,以便使用者在結束後仍能看到按鍵輸入。
  • 游標可見性:如果發出了 ANSI 隱藏游標 (\x1B[?25l),請務必在結束或取消時恢復游標可見性 (\x1B[?25h)。
  • Socket 清理:在終止訊號上明確關閉正在監聽的 HttpServer 或 ServerSocket 實例 (server.close(force: true)),以立即釋放作業系統連接埠。
  • 請參閱 references/signals_and_terminal.md 中的詳細模式。

8. 測試 CLI 應用程式

將測試結構化為兩個不同的層級:

  1. 單元測試 (記憶體內,< 5ms):透過在 test/ 中匯入 package:<pkg>/src/... 來直接測試命令類別、選項解析和業務邏輯。
  2. 整合測試 (子程序):使用 package:test_process 和 package:test_descriptor 來驗證端對端的二進位檔執行、程序 I/O 串流和作業系統結束代碼:
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 處理輸入並乾淨地結束', () 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('處理完成。'));
    await process.shouldExit(0);
  });
}

9. 現代化編譯與分發

Dart 3.12+ 將 CLI 分發標準化為 dart run 和 dart install (逐漸脫離 dart pub global activate):

  • 暫態執行 (JIT):dart run <package>@<version> [args] 按需下載並執行 CLI。
  • 全域安裝 (原生 AOT):dart install <package> 將套件進入點編譯為儲存在 ~/.dart/install/bin/ 中的快速原生獨立二進位檔。
  • 本地開發:使用 dart run <command> (透過 pubspec.yaml 中的 executables: 解析) 或 dart run bin/cli.dart。
  • 捆綁動態函式庫與程式碼資產:使用 dart build cli。輸出到 build/cli/_/bundle/。
  • 獨立可執行檔編譯:使用 dart compile exe bin/cli.dart -o <output_path>。

10. 工作流程與稽核檢查清單

實作工作流程

  • 在 pubspec.yaml 的 executables: 下宣告進入點。
  • 保持 bin/*.dart 為精簡進入點;將命令邏輯放在 lib/src/ 中。
  • 回傳整數結束代碼或設定 exitCode = N;避免原始的 exit(N)。
  • 將錯誤、警告和解析失敗的使用說明導向 stderr。
  • 使用 results.flag()、results.option() 和 results.multiOption() 以獲得型別安全性。
  • 在發出 ANSI 碼之前,驗證 useAnsi (檢查 stdout.hasTerminal、supportsAnsiEscapes 和 NO_COLOR)。
  • 使用 cli_util.dartExecutable 生成子工具,切勿使用 Platform.resolvedExecutable。
  • 在記憶體中對命令 runner 進行單元測試;使用 test_process 進行端對端二進位檔執行測試。

參考資料與範例

Individual skills in this repo

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

相關技能