SKILL.md
唯讀
名稱
dart-build-cli-app
描述
進入點結構、結束碼、跨平台指令碼。適用於建置命令列工具、指令碼或應用程式。
建置 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)。 - 若有多個非同步接聽器(listener)需要循序存取標準輸入(stdin),請使用
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)驗證標準輸出(stdout)與錯誤串流(stderr)。 - 使用
await process.shouldExit(0)斷言最終的結束碼。 - 使用
await d.Descriptor.validate()驗證變更後的檔案系統狀態。
編譯與發布
根據你的發布需求選擇合適的編譯目標。
- 若在開發期間進行本機測試: 使用
dart run bin/cli.dart。這會使用 JIT 編譯器以支援快速迭代。 - 若要打包程式碼資產與動態函式庫: 使用
dart build cli。這會執行建置掛鉤(build hooks)並輸出至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。
搭配 dart compile exe 或 dart compile aot-snapshot 使用 --target-os 與 --target-arch 旗標。
--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_process或標準測試在test/目錄中為新指令建立測試。 - [ ] 執行驗證器 -> 執行
dart run bin/cli.dart help <command_name>以驗證說明文字的生成。 - [ ] 驗證最終 UX:使用
dart compile exe編譯應用程式並執行產出的可執行檔,以驗證目標使用者體驗(例如./bin/cli <command>)。
任務進度:編譯並發布原生可執行檔
- [ ] 執行驗證器 -> 執行
dart format . --set-exit-if-changed以確保程式碼格式正確。 - [ ] 執行驗證器 -> 執行
dart analyze以確保沒有靜態分析錯誤。 - [ ] 執行驗證器 -> 執行
dart test以通過所有整合測試。 - [ ] 為目前主機 OS 編譯:
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 = 'Record changes to the repository.';
CommitCommand() {
argParser.addFlag('all', abbr: 'a', help: 'Commit all changed files.');
}
@override
Future<void> run() async {
final commitAll = argResults?['all'] as bool? ?? false;
print('Committing... (All: $commitAll)');
}
}
void main(List<String> args) {
Chain.capture(() async {
final runner = CommandRunner('dgit', 'Distributed version control.')
..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('Fatal error: $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 formats output correctly and modifies filesystem', () 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. 驗證標準輸出串流
await expectLater(process.stdout, emitsThrough('Processing complete.'));
// 4. 驗證結束碼
await process.shouldExit(0);
// 5. 驗證檔案系統變更
await d.dir('project', [
d.file('config.json', '{"key": "value"}'),
d.file('output.log', 'Success')
]).validate();
});
}






