SKILL.md
只读
名称
dart-build-cli-app
描述
入口结构、退出码规范及跨平台脚本实现。适用于开发 Dart 命令行工具(CLI)、脚本或终端应用。
开发 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 依赖包,用于统一管理命令行参数、标志(flags)及子命令。
- 开发简单脚本:直接使用
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)。 - 当存在多个异步监听器需要按顺序读取标准输入(stdin)时,请使用
io包中的sharedStdIn。 - 使用
stack_trace包的Chain.capture()包裹应用执行逻辑,以精准追踪异步调用堆栈。 - 通过
Trace.terse或Chain.terse格式化输出堆栈信息,过滤掉核心库等杂音堆栈,呈现易读的错误报告。 - 切勿在底层逻辑或存储类中静默吞掉异常(除非可以自动恢复)。应让异常向上抛出或重新抛出(rethrow),以便上层命令知晓操作已失败。
- 快速失败并返回非零退出码:确保在操作失败时向
stderr输出明确的错误提示,并返回对应的非零退出码(例如使用exit(1),或在捕获UsageException后触发 64 退出码)。
CLI 应用自动化测试
[!IMPORTANT]
所有新命令和核心功能必须覆盖自动化测试。 仅靠手动验证无法保障逻辑的严谨性。不过,针对帮助文本和用户体验(UX)的手动验证依然是必要的,这能确保交互逻辑符合直觉且表述准确。
使用 test_process 与 test_descriptor 为 CLI 编写高保真的集成测试。
- 使用
test_descriptor(d.dir、d.file)定义预期的文件系统状态。 - 在测试执行前运行
await d.Descriptor.create()来创建 Mock 文件系统环境。 - 调用
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。这会触发构建 Hook 并将产物输出到build/cli/_/bundle/。 - 分发独立原生可执行文件: 使用
dart compile exe bin/cli.dart -o <output_path>。这会将 Dart 运行时与机器码打包为单一可执行文件。 - 分发多个应用且对磁盘空间有严格限制: 使用
dart compile aot-snapshot bin/cli.dart。生成的.aot文件需通过dartaotruntime运行。
<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/目录下编写对应新命令的测试用例(可使用test_process或标准测试)。 - [ ] 执行校验 -> 运行
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. Setup mock filesystem
await d.dir('project', [
d.file('config.json', '{"key": "value"}')
]).create();
// 2. Spawn the CLI process
final process = await TestProcess.start(
'dart',
['run', 'bin/cli.dart', 'process', '--path', '${d.sandbox}/project']
);
// 3. Validate stdout stream
await expectLater(process.stdout, emitsThrough('Processing complete.'));
// 4. Validate exit code
await process.shouldExit(0);
// 5. Validate filesystem mutations
await d.dir('project', [
d.file('config.json', '{"key": "value"}'),
d.file('output.log', 'Success')
]).validate();
});
}






