dart-cli-creator について
Dart のコマンドラインツールを作成・変更するエージェント向けのスキルです。プロジェクト構成、コマンド処理、ログ、エラー管理、静的解析、リリースの指針を示します。
主な機能
- 01Dart CLI のプロジェクト構成
- 02サブコマンドとヘルプ文
- 03ログと対話型プロンプト
- 04エラー処理と終了コード
- 05静的解析とリリースの指針
活用シーン
- 01Dart CLI のひな形作成
- 02コマンドとオプションの追加
- 03CLI のエラー処理と出力の改善
- 04CLI の配布準備
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ mono0926/ skills --skill dart-cli-creatorインストール後、Claude Code で/dart-cli-creatorを入力して読み込まれたか確認します。
詳細
Dart CLI 作成スキル
このスキルでは、Dart を使用して高品質なコマンドラインインターフェース(CLI)ツールを作成、改善するためのガイドラインと手順を説明します。 エージェントとして、ユーザーから Dart CLI の作成または変更を依頼された場合は、これらのベストプラクティスに常に従う必要があります。
🎯 ベストプラクティスの基本
1. プロジェクト構成と設計
bin/ディレクトリ:CLI のエントリーポイントとなる実行可能ファイル(例:bin/my_cli.dart)を配置します。ここは、lib/またはsrc/のトップレベル関数やクラスを呼び出すだけの薄いラッパーにし、複雑なロジックは置かないでください。lib/ディレクトリ:ビジネスロジック、コマンドの実装、再利用可能なユーティリティを配置します(例:lib/src/commands/、lib/src/utils/)。- 関心の分離:テスト容易性を最大限に高めるため、CLI の入出力処理(引数の解析、UI の描画)とコアのビジネスロジックは必ず分離します。
2. コマンドライン引数とルーティング
argsパッケージを活用する:標準のargsパッケージにあるCommandRunnerクラスとCommandクラスを使用して、サブコマンドを備えた Git のような CLI を構築します。- 自己説明的にする:各コマンド、フラグ、オプションの
descriptionとhelpを丁寧に記述し、--helpで使用方法を十分に説明できるようにします。
3. 豊かな UX(ユーザー体験)とログ出力
mason_loggerを積極的に使用する:標準のprint文は避け、代わりにmason_loggerパッケージを使用します。logger.info()、logger.success()、logger.err()、logger.warn()を使って、色分けされたメッセージを出力します。- 時間のかかるタスクには、
logger.progress('Doing something...')を使ってスピナーを表示します。 - ユーザーへの確認(y/n)やテキスト入力には、
logger.confirm()またはlogger.prompt()を利用します。より高度なメニュー選択やスピナーが必要な場合は、interactパッケージと組み合わせてください。 - ヒント: 実用的な構成にするには、
lib/logger.dartのトップレベルでfinal logger = Logger();を定義し、プロジェクト全体でインポートします。これにより定型コードを減らせて、とても便利です(DI が必須ではない、小規模から中規模の CLI ツールに最適です)。
cli_completion:CompletionCommandRunnerを拡張してシェル補完機能を提供するために、cli_completionパッケージの使用を検討してください。
4. 適切なエラー処理と終了方法
- 終了コード:プロセスの結果は常に適切な終了コードとして扱います(成功時は
0、エラー時は1またはsysexitsに準拠したエラーコード)。 exit()の直接呼び出しを避ける:exit()を直接呼び出すと Dart VM が直ちに終了し、リソースのクリーンアップ(finallyブロックなど)が実行されない可能性があります。代わりに、CommandRunner.run()からintを返し、それをmain関数に渡してexitCodeプロパティに設定し、プロセスが自然に終了するようにします(例:Process.stdout.close()を待機した後)。- カスタム例外クラスと処理:ドメイン固有のカスタム例外クラス(例:
AppException)を定義し、呼び出しスタックの深い位置からスローします。 - グローバルな例外捕捉:
CommandRunnerの実行全体をtry-catchブロックで囲み、AppException、UsageException、未捕捉の例外を処理します。スタックトレースをユーザーに直接表示するのではなく、logger.err(e.message)でエラーを適切に出力し、適切な終了コードを返します。
5. 静的解析とコードフォーマット
pedantic_monoを適用する:analysis_options.yamlでpedantic_monoを設定し、厳格な静的解析ルールに従います。- [ZERO TOLERANCE]:コードを変更した後は、エラー、警告、info レベルの lint を必ずすべて直ちに解消します。プロジェクトが健全な状態と見なされるには、解析結果が「クリーン」であることが必須です。
6. 配布と実行の設定
- シバン:
bin/直下にある実行可能ファイルの先頭行には、必ず#!/usr/bin/env dartを記述します。 - Pubspec の設定:
dart pub global activateで簡単にインストールできるよう、pubspec.yamlのexecutables:セクションで CLI コマンド名を定義します。
7. 自動更新チェック機能
pub_updaterの活用:pub_updaterを使用して、コマンド実行ごと、または定期的にpub.devやその他のレジストリを確認し、新しいバージョンがリリースされているかチェックします。- 新しいバージョンが利用可能な場合は、
logger.warn('Update available!')またはinteractベースのプロンプトを使用して、ユーザーに更新を促す対話型 UI を表示します。
8. バージョン管理とリリースプロセス
- 堅牢なバージョン操作:SemVer バージョンを操作する際は、可能な限りバージョンを手動で解析しないでください。プレリリースタグやビルドメタデータなどのエッジケースによる解析エラーを防ぐため、常に
pub_semverパッケージ(例:Version.parse、nextMajor、nextMinor)を使用してください。 - 自動リリース:バージョンの更新、CHANGELOG の生成、Git へのコミット、GitHub または pub.dev への公開には、
release-pubスキルを利用してください。高度な Dart CLI を作成する場合は、ユーザーにこれらの手順を手動で実行するよう指示するのではなく、このエージェント型スキルを使用して複雑な CI/CD とリリースパイプラインを安全に調整することを推奨します。
🛠️ プロジェクトのひな形(基本的な実装例)
CLI を作成する際は、基盤として以下のような構成と実装を使用してください。
pubspec.yaml の抜粋
environment:
sdk: ^3.11.0 # Always specify the latest stable version
dependencies:
args: ^2.5.0
cli_completion: ^0.4.0
mason_logger: ^0.2.16
pub_updater: ^0.4.0
dev_dependencies:
pedantic_mono: any
test: ^1.24.0
executables:
my_cli:
bin/my_cli.dart
#!/usr/bin/env dart
import 'dart:io';
import 'package:my_cli/command_runner.dart';
Future<void> main(List<String> arguments) async {
final exitCode = await MyCliCommandRunner().run(arguments);
await flushThenExit(exitCode ?? 0);
}
/// Helper method to set the [status] to exitCode, and wait for the standard output/error to flush before exiting
Future<void> flushThenExit(int status) async {
exitCode = status;
await Future.wait<void>([
stdout.close(),
stderr.close(),
]);
}
lib/logger.dart
import 'package:mason_logger/mason_logger.dart';
/// The [Logger] shared across the application.
final logger = Logger();
lib/command_runner.dart
import 'package:args/args.dart';
import 'package:cli_completion/cli_completion.dart';
import 'package:mason_logger/mason_logger.dart';
import 'package:my_cli/logger.dart';
import 'package:pub_updater/pub_updater.dart';
class MyCliCommandRunner extends CompletionCommandRunner<int> {
MyCliCommandRunner({PubUpdater? pubUpdater})
: _pubUpdater = pubUpdater ?? PubUpdater(),
super('my_cli', 'A highly robust Dart CLI tool.') {
argParser
..addFlag(
'version',
abbr: 'v',
negatable: false,
help: 'Print the current version.',
)
..addFlag(
'verbose',
help: 'Enable verbose logging.',
);
// TODO: addCommand(MyCustomCommand());
}
final PubUpdater _pubUpdater;
Future<void> _checkForUpdates() async {
try {
final isUpToDate = await _pubUpdater.isUpToDate(
packageName: 'my_cli',
currentVersion: '1.0.0', // Optionally link to a packageVersion constant
);
if (!isUpToDate) {
final latestVersion = await _pubUpdater.getLatestVersion('my_cli');
logger.info('\nUpdate available: $latestVersion');
}
} catch (_) {}
}
@override
Future<int> run(Iterable<String> args) async {
try {
final argResults = parse(args);
if (argResults['verbose'] == true) {
logger.level = Level.verbose;
}
// Check for version updates (consider asynchronous background execution)
await _checkForUpdates();
return await runCommand(argResults) ?? 0;
} on FormatException catch (e) {
logger
..err(e.message)
..info('')
..info(usage);
return 64; // usage error
} on UsageException catch (e) {
logger
..err(e.message)
..info('')
..info(usage);
return 64;
} on AppException catch (e) {
// Gracefully output custom domain exceptions
logger.err(e.message);
return 1;
} catch (e, stackTrace) {
logger
..err('An unexpected error occurred: $e')
..err('$stackTrace');
return 1;
}
}
@override
Future<int?> runCommand(ArgResults topLevelResults) async {
if (topLevelResults['version'] == true) {
logger.info('my_cli version: 1.0.0');
return 0;
}
return super.runCommand(topLevelResults);
}
}
/// A custom exception specific to the domain.
class AppException implements Exception {
const AppException(this.message);
final String message;
@override
String toString() => message;
}
lib/src/commands/my_custom_command.dart
import 'package:args/command_runner.dart';
import 'package:my_cli/logger.dart';
class MyCustomCommand extends Command<int> {
MyCustomCommand() {
argParser.addOption(
'name',
abbr: 'n',
help: 'Your name.',
mandatory: true,
);
}
@override
String get description => 'A custom command example.';
@override
String get name => 'hello';
@override
Future<int> run() async {
final name = argResults?['name'] as String?;
final progress = logger.progress('Saying hello to $name...');
// Simulate a time-consuming task
await Future<void>.delayed(const Duration(seconds: 1));
progress.complete('Hello, $name!');
return 0; // Success
}
}
🤖 エージェントへの指示
ユーザーから「X を実行する CLI ツールを作成して」などと依頼された場合:
- このスキルの内容に従い、
argsとmason_loggerを中心にプロジェクトを構成します。pubspec.yamlでは、常に最新の安定版 Dart SDK バージョン(パッチバージョンが 0 のもの。例:^3.11.0)を指定してください。 - ディレクトリ構成を決定し、要件に基づいてコマンド群を編成または提案します。
pedantic_monoを導入し、厳格な静的解析に準拠した高品質なコードを生成します。- (必要に応じて)開発完了後、
README.mdに使用方法を記載し、dart pub global activate --source path .を使用するなどのインストール手順をユーザーに提示します。
コメント
Markdown 対応