SKILL.md
readonlyread-only
name
dart-build-cli-app
description
進入點結構、退出碼、跨平台腳本。用於建構命令列工具、腳本或應用程式。
建構 Dart CLI 應用程式
目錄
專案設定與架構
使用官方 Dart 範本初始化新的 CLI 專案,以確保標準目錄結構。
- 執行
dart create -t cli <project_name>來建立一個包含基本引數解析的主控台應用程式。 - 將可執行進入點(包含
main()的檔案)僅放在bin/目錄中。 - 將內部實作邏輯放在
lib/src/中,並透過lib/<project_name>.dart公開公開 API。 - 在 CI 環境中強制執行格式化,執行
dart format . --set-exit-if-changed。如果存在格式違規,則會回傳退出碼 1。
引數解析與命令路由
匯入 args 套件來管理命令列引數、旗標和子命令。
- 如果建構簡單腳本:直接使用
ArgParser定義旗標(addFlag)和選項(addOption)。 - 如果建構複雜的多命令 CLI(如
git):實作CommandRunner並為每個子命令擴充Command。 - 在
CommandRunner.argParser上定義全域引數,在個別Command.argParser上定義命令專屬引數。 - 捕捉
UsageException以優雅地處理無效引數並顯示自動產生的說明文字。 - 驗證說明文字的正確性:確保說明文字提供執行工具所需的所有資訊。如果說明文字參考了已編譯的可執行檔名稱,且使用者需要將其加入 PATH 才能以該方式執行,請在說明文字或描述中提供明確的指示。
執行與錯誤處理
利用 io 和 stack_trace 套件來建構穩健、可上線的 CLI 工具。
- 使用
io套件的ExitCode列舉來回傳標準 POSIX 退出碼(例如ExitCode.success.code、ExitCode.usage.code)。 - 如果多個非同步監聽器需要依序存取標準輸入,請使用
io套件的sharedStdIn。 - 使用
stack_trace套件的Chain.capture()包裝應用程式執行,以追蹤非同步堆疊鏈。 - 使用
Trace.terse或Chain.terse格式化輸出堆疊追蹤,以去除雜亂的核心函式庫框架,並向使用者呈現可讀的錯誤。 - 除非可以復原,否則不要在底層邏輯或儲存類別中吞掉例外。讓它們向上傳遞或重新拋出,以便上層命令知道操作失敗。
- 快速失敗並使用非零退出碼:確保操作失敗會向
stderr輸出描述性錯誤訊息,並使用適當的非零退出碼(例如,使用exit(1)或在捕捉到UsageException後觸發退出碼 64)。
測試 CLI 應用程式
[!IMPORTANT]
所有新命令和重要功能都必須有自動化測試覆蓋。 手動驗證不足以測試邏輯。不過,仍然需要手動驗證說明文字和使用者體驗(UX),以確保介面直觀且正確。
使用 test_process 和 test_descriptor 為 CLI 編寫高保真整合測試。
- 使用
test_descriptor(d.dir、d.file)定義預期的檔案系統狀態。 - 在執行前使用
await d.Descriptor.create()建立模擬檔案系統。 - 使用
TestProcess.start('dart', ['run', 'bin/cli.dart', ...args])啟動 CLI 程序。 - 使用
StreamQueue匹配器(例如emitsThrough、emits)驗證標準輸出和錯誤串流。 - 使用
await process.shouldExit(0)斷言最終退出碼。 - 使用
await d.Descriptor.validate()驗證結果的檔案系統變更。
編譯與發布
根據發布需求選擇適當的編譯目標。
- 如果在開發期間進行本地測試: 使用
dart run bin/cli.dart。這會使用 JIT 編譯器以快速迭代。 - 如果需要打包程式碼資產和動態函式庫: 使用
dart build cli。這會執行建構鉤子並輸出到build/cli/_/bundle/。 - 如果需要發布獨立的原生可執行檔: 使用
dart compile exe bin/cli.dart -o <output_path>。這會將 Dart 執行時期和機器碼打包到單一檔案中。 - 如果需要發布多個應用程式且磁碟空間嚴格限制: 使用
dart compile aot-snapshot bin/cli.dart。使用dartaotruntime執行產生的.aot檔案。
<details>
<summary>交叉編譯目標(僅限 Linux)</summary>
Dart 支援從 macOS、Windows 或 Linux 主機交叉編譯到 Linux。
使用 --target-os 和 --target-arch 旗標搭配 dart compile exe 或 dart compile aot-snapshot。
--target-os=linux(目前僅支援 Linux 作為交叉編譯目標)--target-arch=arm64(64 位元 ARM)--target-arch=x64(x86-64)--target-arch=arm(32 位元 ARM)--target-arch=riscv64(64 位元 RISC-V)
範例:dart compile exe --target-os=linux --target-arch=arm64 bin/cli.dart
</details>
工作流程
任務進度:實作新的 CLI 命令
- [ ] 在
lib/src/commands/中建立一個擴充Command的新類別。 - [ ] 定義
name和description屬性。 - [ ] 在建構子中使用
argParser.addFlag()或argParser.addOption()註冊命令專屬旗標。 - [ ] 使用核心邏輯實作
run()方法。 - [ ] 在
bin/cli.dart的CommandRunner實例中使用addCommand()註冊新命令。 - [ ] 在
test/目錄中使用test_process或標準測試為新命令建立測試。 - [ ] 執行驗證器 -> 執行
dart run bin/cli.dart help <command_name>以驗證說明文字產生。 - [ ] 驗證最終使用者體驗:使用
dart compile exe編譯應用程式,並執行產生的可執行檔以驗證目標使用者體驗(例如./bin/cli <command>)。
任務進度:編譯並發布原生可執行檔
- [ ] 執行驗證器 -> 執行
dart format . --set-exit-if-changed以確保程式碼格式。 - [ ] 執行驗證器 -> 執行
dart analyze以確保沒有靜態分析錯誤。 - [ ] 執行驗證器 -> 執行
dart test以通過所有整合測試。 - [ ] 為主機作業系統編譯:
dart compile exe bin/cli.dart -o build/cli-host - [ ] 為 Linux 編譯(如果主機是 macOS/Windows):
dart compile exe --target-os=linux --target-arch=x64 bin/cli.dart -o build/cli-linux-x64
範例
範例:CommandRunner 實作
import 'dart:io';
import 'package:args/command_runner.dart';
import 'package:stack_trace/stack_trace.dart';
class CommitCommand extends Command {
@override
final String name = 'commit';
@override
final String description = '記錄對儲存庫的變更。';
CommitCommand() {
argParser.addFlag('all', abbr: 'a', help: '提交所有已變更的檔案。');
}
@override
Future<void> run() async {
final commitAll = argResults?['all'] as bool? ?? false;
print('正在提交...(全部:$commitAll)');
}
}
void main(List<String> args) {
Chain.capture(() async {
final runner = CommandRunner('dgit', '分散式版本控制。')
..addCommand(CommitCommand());
await runner.run(args);
}, onError: (error, chain) {
if (error is UsageException) {
stderr.writeln(error.message);
stderr.writeln(error.usage);
exit(64); // ExitCode.usage.code
} else {
stderr.writeln('致命錯誤:$error');
stderr.writeln(chain.terse);
exit(1);
}
});
}
範例:使用子程序的整合測試
import 'package:test/test.dart';
import 'package:test_process/test_process.dart';
import 'package:test_descriptor/test_descriptor.dart' as d;
void main() {
test('CLI 正確格式化輸出並修改檔案系統', () async {
// 1. 設定模擬檔案系統
await d.dir('project', [
d.file('config.json', '{"key": "value"}')
]).create();
// 2. 啟動 CLI 程序
final process = await TestProcess.start(
'dart',
['run', 'bin/cli.dart', 'process', '--path', '${d.sandbox}/project']
);
// 3. 驗證 stdout 串流
await expectLater(process.stdout, emitsThrough('處理完成。'));
// 4. 驗證退出碼
await process.shouldExit(0);
// 5. 驗證檔案系統變更
await d.dir('project', [
d.file('config.json', '{"key": "value"}'),
d.file('output.log', '成功')
]).validate();
});
}






