mcpField
D
dart-cli-creator

dart-cli-creator

github.com/mono0926開発286 回インストール2 週間前に更新
リポジトリ

dart-cli-creator について

Dart のコマンドラインツールを作成・変更するエージェント向けのスキルです。プロジェクト構成、コマンド処理、ログ、エラー管理、静的解析、リリースの指針を示します。

主な機能

  1. 01Dart CLI のプロジェクト構成
  2. 02サブコマンドとヘルプ文
  3. 03ログと対話型プロンプト
  4. 04エラー処理と終了コード
  5. 05静的解析とリリースの指針

活用シーン

  1. 01Dart CLI のひな形作成
  2. 02コマンドとオプションの追加
  3. 03CLI のエラー処理と出力の改善
  4. 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 ツールを作成して」などと依頼された場合:

  1. このスキルの内容に従い、args と mason_logger を中心にプロジェクトを構成します。pubspec.yaml では、常に最新の安定版 Dart SDK バージョン(パッチバージョンが 0 のもの。例:^3.11.0)を指定してください。
  2. ディレクトリ構成を決定し、要件に基づいてコマンド群を編成または提案します。
  3. pedantic_mono を導入し、厳格な静的解析に準拠した高品質なコードを生成します。
  4. (必要に応じて)開発完了後、README.md に使用方法を記載し、dart pub global activate --source path . を使用するなどのインストール手順をユーザーに提示します。

コメント