fnds_cli 0.3.1 copy "fnds_cli: ^0.3.1" to clipboard
fnds_cli: ^0.3.1 copied to clipboard

Another starting point for interactive CLI using Dart, inspired by AstroJS CLI.

example/fnds_cli_example.dart

import 'package:args/args.dart';
import 'package:fnds_cli/fnds_cli.dart';

void main(List<String> args) async {
  // Create a command runner for a culinary adventure CLI
  final runner = CliCommandRunner(
    'food-adventure',
    'A culinary adventure CLI application using fnds_cli framework',
    // Enable logging by default
    enableLogging: true,
    // Enable interactive fallback by default (use -c to disable)
    useInteractiveFallback: true,
  );

  // Add first-level commands
  runner.addBaseCommand(EatCommand());
  runner.addBaseCommand(DrinkCommand());
  runner.addBaseCommand(TalkCommand());
  runner.addBaseCommand(SeeCommand());

  // Add a custom option to disable interactive mode with -c flag
  runner.argParser.addFlag(
    'console-only',
    abbr: 'c',
    help: 'Disable interactive fallback mode',
    negatable: false,
    callback: (value) {
      if (value) {
        // Override the interactive flag if -c is provided
        cliStateManager.addMember(SingleCLIState<bool>('interactive', false));
      }
    },
  );

  // Run the command
  await runner.run(args);
}

/// Second-level command under Talk
class ChefCommand extends BaseCommand {
  @override
  String get description => 'Talk to the chef';

  @override
  String get name => 'chef';

  @override
  Future<int> execute() async {
    final compliment = getArg<String>('compliment') ?? '';
    final feedback = getArg<String>('feedback') ?? '';

    logger.info('Complimenting chef: $compliment');
    logger.info('Feedback: $feedback');

    return 0;
  }

  @override
  void setupArgs(ArgParser argParser) {
    super.setupArgs(argParser);

    // Add compliment option
    argParser.addOption('compliment', help: 'Compliment for the chef');
    // Configure interactive fallback for compliment
    setInteractiveFallback(
      'compliment',
      InteractiveFallback<String>(
        inputType: InteractiveInputType.ask,
        question: 'What would you like to compliment the chef on?',
        defaultValue: 'Excellent meal',
      ),
    );

    // Add feedback option
    argParser.addOption('feedback', help: 'Specific feedback for the chef');
    // Configure interactive fallback for feedback with validation logic
    // (The validation happens in the CommandRunner before it reaches here)
    setInteractiveFallback(
      'feedback',
      InteractiveFallback<String>(
        inputType: InteractiveInputType.ask,
        question: 'Any specific feedback for the chef?',
        defaultValue: 'The meal was perfect',
      ),
    );
  }
}

/// Second-level command under Drink
class ColdDrinkCommand extends BaseCommand {
  @override
  String get description => 'Order a cold drink';

  @override
  String get name => 'cold';

  @override
  Future<int> execute() async {
    // Remove defaults here to ensure interactive prompts appear
    final drinkChoice = getArg<String>('drink-choice');
    final ice = getArg<String>('ice');
    final withStraw = getArg<bool>('with-straw');
    final discountCode = getArg<String>('discount-code') ?? '';

    logger.info(
      'Ordering ${drinkChoice ?? "Unknown drink"} with ${ice ?? "no"} ice',
    );
    logger.info('With straw: ${withStraw ?? false}');
    if (discountCode.isNotEmpty) {
      logger.info('Discount code applied');
    }

    return 0;
  }

  @override
  void setupArgs(ArgParser argParser) {
    super.setupArgs(argParser);

    // Add drink-choice option - remove defaultsTo
    argParser.addOption(
      'drink-choice',
      help: 'Type of cold drink',
      allowed: ['Cola', 'Lemonade', 'Iced tea', 'Water'],
      // Remove defaultsTo to force interactive prompt
    );
    // Configure interactive fallback for drink-choice
    setInteractiveFallback(
      'drink-choice',
      InteractiveFallback<String>(
        inputType: InteractiveInputType.select,
        question: 'Select a cold drink:',
        options: <String>['Cola', 'Lemonade', 'Iced tea', 'Water'],
        defaultValue: 'Water',
        label: 'Drink',
      ),
    );

    // Add ice option - remove defaultsTo
    argParser.addOption(
      'ice',
      help: 'Amount of ice',
      allowed: ['none', 'light', 'normal', 'extra'],
      // Remove defaultsTo to force interactive prompt
    );
    // Add interactive fallback for ice
    setInteractiveFallback(
      'ice',
      InteractiveFallback<String>(
        inputType: InteractiveInputType.select,
        question: 'How much ice would you like?',
        options: <String>['none', 'light', 'normal', 'extra'],
        defaultValue: 'normal',
        label: 'Ice Amount',
      ),
    );

    // Add with-straw flag - remove defaultsTo
    argParser.addFlag('with-straw', help: 'Include a straw');
    // Add interactive fallback for with-straw
    setInteractiveFallback(
      'with-straw',
      InteractiveFallback<bool>(
        inputType: InteractiveInputType.confirm,
        question: 'Would you like a straw?',
        defaultValue: true,
      ),
    );

    // Add discount-code option
    argParser.addOption('discount-code', help: 'Discount code (if any)');
    // Configure interactive fallback for discount-code
    // Using ask with secretive=true for password-like input
    setInteractiveFallback(
      'discount-code',
      InteractiveFallback<String>(
        inputType: InteractiveInputType.ask,
        question: 'Enter discount code (if any):',
        defaultValue: '',
      ),
    );
  }
}

/// First-level command: Drink
class DrinkCommand extends NestedCommand {
  @override
  String get description => 'Commands related to drinks';

  @override
  String get name => 'drink';

  @override
  void addSubcommands() {
    addSubcommand(HotDrinkCommand());
    addSubcommand(ColdDrinkCommand());
  }
}

/// First-level command: Eat
class EatCommand extends NestedCommand {
  @override
  String get description => 'Commands related to eating food';

  @override
  String get name => 'eat';

  @override
  void addSubcommands() {
    addSubcommand(MealCommand());
    addSubcommand(SnackCommand());
  }
}

/// Second-level command under Drink
class HotDrinkCommand extends BaseCommand {
  @override
  String get description => 'Order a hot drink';

  @override
  String get name => 'hot';

  @override
  Future<int> execute() async {
    final drinkName = getArg<String>('drink-name') ?? 'coffee';
    final temperature = getArg<String>('temperature') ?? 'hot';
    final toGo = getArg<bool>('to-go') ?? false;
    final confirm = getArg<bool>('confirm') ?? true;

    if (confirm) {
      logger.info('Ordering $temperature $drinkName');
      logger.info('To go: $toGo');
    } else {
      logger.info('Order cancelled');
    }

    return 0;
  }

  @override
  void setupArgs(ArgParser argParser) {
    super.setupArgs(argParser);

    // Add drink-name option
    argParser.addOption('drink-name', help: 'Type of hot drink');
    // Configure interactive fallback for drink-name
    setInteractiveFallback(
      'drink-name',
      InteractiveFallback<String>(
        inputType: InteractiveInputType.ask,
        question: 'What hot drink would you like?',
        defaultValue: 'coffee',
        label: 'Drink',
      ),
    );

    // Add temperature option
    argParser.addOption(
      'temperature',
      help: 'Drink temperature',
      allowed: ['hot', 'very-hot', 'warm'],
      defaultsTo: 'hot',
    );

    // Add to-go flag
    argParser.addFlag('to-go', help: 'Get drink to go', defaultsTo: false);

    // Add confirm flag
    argParser.addFlag('confirm', help: 'Confirm your order', defaultsTo: true);
    // Configure interactive fallback for confirm
    setInteractiveFallback(
      'confirm',
      InteractiveFallback<bool>(
        inputType: InteractiveInputType.confirm,
        question: 'Confirm your order?',
        defaultValue: true,
      ),
    );
  }
}

/// Second-level command under See
class LocationsCommand extends BaseCommand {
  @override
  String get description => 'View restaurant locations';

  @override
  String get name => 'locations';

  @override
  Future<int> execute() async {
    final nearby = getArg<bool>('nearby') ?? false;
    final zipCode = getArg<String>('zip-code') ?? '10001';
    final confirmSearch = getArg<bool>('confirm-search') ?? true;

    if (confirmSearch) {
      logger.info(
        'Searching for ${nearby ? "nearby " : ""}locations near $zipCode',
      );
    } else {
      logger.info('Location search cancelled');
    }

    return 0;
  }

  @override
  void setupArgs(ArgParser argParser) {
    super.setupArgs(argParser);

    // Add nearby flag
    argParser.addFlag(
      'nearby',
      help: 'Show only nearby locations',
      defaultsTo: false,
    );

    // Add zip-code option
    argParser.addOption('zip-code', help: 'ZIP code to search near');
    // Configure interactive fallback for zip-code
    // Using ask for potentially sensitive data
    setInteractiveFallback(
      'zip-code',
      InteractiveFallback<String>(
        inputType: InteractiveInputType.ask,
        question: 'Enter your zip code to find nearby locations:',
        defaultValue: '10001',
      ),
    );

    // Add confirm-search flag
    argParser.addFlag(
      'confirm-search',
      help: 'Confirm location search',
      defaultsTo: true,
    );
    // Configure interactive fallback for confirm-search
    setInteractiveFallback(
      'confirm-search',
      InteractiveFallback<bool>(
        inputType: InteractiveInputType.confirm,
        question: 'Search for locations?',
        defaultValue: true,
      ),
    );
  }
}

/// Second-level command under Eat
class MealCommand extends BaseCommand {
  @override
  String get description => 'Order a complete meal';

  @override
  String get name => 'meal';

  @override
  Future<int> execute() async {
    final cuisine = getArg<String>('cuisine') ?? 'italian';
    final mealType = getArg<String>('meal-type') ?? 'dinner';
    final sides = getArg<List<String>>('sides') ?? ['salad', 'bread'];
    final specialRequests = getArg<String>('special-requests') ?? 'None';
    final drink = getArg<String>('drink') ?? 'Water';
    final dessert = getArg<bool>('dessert') ?? false;

    logger.info('Ordering $cuisine $mealType:');
    logger.info('- Side dishes: ${sides.join(", ")}');
    logger.info('- Drink pairing: $drink');
    logger.info('- Dessert: ${dessert ? "Yes" : "No"}');
    logger.info('- Special requests: $specialRequests');

    return 0;
  }

  @override
  void setupArgs(ArgParser argParser) {
    super.setupArgs(argParser);

    // Add cuisine option
    argParser.addOption(
      'cuisine',
      abbr: 'u',
      help: 'Type of cuisine',
      allowed: ['italian', 'mexican', 'japanese', 'indian'],
    );
    // Configure interactive fallback for cuisine
    setInteractiveFallback(
      'cuisine',
      InteractiveFallback<String>(
        inputType: InteractiveInputType.select,
        question: 'What type of cuisine would you like?',
        options: <String>['italian', 'mexican', 'japanese', 'indian'],
        label: 'Cuisine',
      ),
    );

    // Add meal-type option
    argParser.addOption(
      'meal-type',
      help: 'Type of meal',
      allowed: ['breakfast', 'lunch', 'dinner'],
    );
    // Configure interactive fallback for meal-type
    setInteractiveFallback(
      'meal-type',
      InteractiveFallback<String>(
        inputType: InteractiveInputType.select,
        question: 'Which meal are you ordering?',
        options: <String>['breakfast', 'lunch', 'dinner'],
        label: 'Meal Type',
      ),
    );

    // Add sides option
    argParser.addMultiOption(
      'sides',
      help: 'Side dishes to include',
      defaultsTo: ['salad', 'bread'],
    );

    // Add special requests option
    argParser.addOption(
      'special-requests',
      help: 'Any special requests for your meal',
    );
    // Configure interactive fallback for special-requests
    setInteractiveFallback(
      'special-requests',
      InteractiveFallback<String>(
        inputType: InteractiveInputType.ask,
        question: 'Any special requests for your meal?',
        defaultValue: 'None',
      ),
    );

    // Add drink option
    argParser.addOption(
      'drink',
      help: 'Drink pairing for your meal',
      allowed: ['Wine', 'Beer', 'Water', 'Juice', 'Coffee'],
      defaultsTo: 'Water',
    );
    // Configure interactive fallback for drink
    setInteractiveFallback(
      'drink',
      InteractiveFallback<String>(
        inputType: InteractiveInputType.select,
        question: 'Choose a drink pairing:',
        options: <String>['Wine', 'Beer', 'Water', 'Juice', 'Coffee'],
        defaultValue: 'Water',
        label: 'Drink',
      ),
    );

    // Add dessert flag
    argParser.addFlag(
      'dessert',
      help: 'Include dessert with your meal',
      defaultsTo: false,
    );
    // Configure interactive fallback for dessert
    setInteractiveFallback(
      'dessert',
      InteractiveFallback<bool>(
        inputType: InteractiveInputType.confirm,
        question: 'Would you like dessert with your meal?',
        defaultValue: false,
      ),
    );
  }
}

/// Second-level command under See
class MenuCommand extends BaseCommand {
  @override
  String get description => 'View the menu';

  @override
  String get name => 'menu';

  @override
  Future<int> execute() async {
    final category = getArg<String>('category') ?? 'all';
    final showPrices = getArg<bool>('prices') ?? true;

    logger.info('Showing $category menu');
    if (showPrices) {
      logger.info('Prices included');
    }

    return 0;
  }

  @override
  void setupArgs(ArgParser argParser) {
    super.setupArgs(argParser);

    // Add category option
    argParser.addOption(
      'category',
      help: 'Menu category to view',
      allowed: ['appetizers', 'mains', 'desserts', 'drinks', 'all'],
      defaultsTo: 'all',
    );

    // Add prices flag
    argParser.addFlag('prices', help: 'Show prices', defaultsTo: true);
  }
}

/// First-level command: See
class SeeCommand extends NestedCommand {
  @override
  String get description =>
      'Commands related to viewing restaurant information';

  @override
  String get name => 'see';

  @override
  void addSubcommands() {
    addSubcommand(MenuCommand());
    addSubcommand(LocationsCommand());
  }
}

/// Second-level command under Eat
class SnackCommand extends BaseCommand {
  @override
  String get description => 'Get a quick snack';

  @override
  String get name => 'snack';

  @override
  Future<int> execute() async {
    final snackType = getArg<String>('snack-type') ?? 'sweet';
    final toGo = getArg<bool>('to-go') ?? true;
    final toppings = getArg<List<String>>('toppings') ?? <String>[];

    logger.info('Getting a $snackType snack');
    logger.info('To go: $toGo');
    if (toppings.isNotEmpty) {
      logger.info('Toppings: ${toppings.join(", ")}');
    }

    return 0;
  }

  @override
  void setupArgs(ArgParser argParser) {
    super.setupArgs(argParser);

    // Add snack-type option
    argParser.addOption(
      'snack-type',
      help: 'Type of snack',
      allowed: ['sweet', 'savory', 'healthy', 'indulgent'],
    );
    // Configure interactive fallback for snack-type
    setInteractiveFallback(
      'snack-type',
      InteractiveFallback<String>(
        inputType: InteractiveInputType.select,
        question: 'What type of snack would you like?',
        options: <String>['sweet', 'savory', 'healthy', 'indulgent'],
        label: 'Snack Type',
      ),
    );

    // Add to-go flag
    argParser.addFlag('to-go', help: 'Get snack to go', defaultsTo: true);

    // Add toppings option
    argParser.addMultiOption(
      'toppings',
      help: 'Toppings for your snack',
      defaultsTo: [],
    );
    // Configure interactive fallback for toppings
    setInteractiveFallback(
      'toppings',
      InteractiveFallback<List<String>>(
        inputType: InteractiveInputType.multipleSelect,
        question: 'Choose toppings for your snack:',
        options: <List<String>>[
          ['Chocolate'],
          ['Nuts'],
          ['Fruit'],
          ['Caramel'],
          ['Sprinkles'],
        ],
      ),
    );
  }
}

/// First-level command: Talk
class TalkCommand extends NestedCommand {
  @override
  String get description => 'Commands related to talking to staff';

  @override
  String get name => 'talk';

  @override
  void addSubcommands() {
    addSubcommand(WaiterCommand());
    addSubcommand(ChefCommand());
  }
}

/// Second-level command under Talk
class WaiterCommand extends BaseCommand {
  @override
  String get description => 'Talk to a waiter';

  @override
  String get name => 'waiter';

  @override
  Future<int> execute() async {
    final urgency = getArg<String>('urgency') ?? 'low';
    final topics = getArg<List<String>>('topics') ?? <String>[];

    logger.info('Calling waiter with $urgency urgency');
    logger.info('Topics to discuss: ${topics.join(", ")}');

    return 0;
  }

  @override
  void setupArgs(ArgParser argParser) {
    super.setupArgs(argParser);

    // Add urgency option
    argParser.addOption(
      'urgency',
      help: 'Level of urgency',
      allowed: ['low', 'medium', 'high'],
      defaultsTo: 'low',
    );

    // Add topics option
    argParser.addMultiOption('topics', help: 'Topics to discuss with waiter');
    // Configure interactive fallback for topics
    setInteractiveFallback(
      'topics',
      InteractiveFallback<List<String>>(
        inputType: InteractiveInputType.multipleSelect,
        question: 'What would you like to talk about?',
        options: <List<String>>[
          ['Menu recommendations'],
          ['Special dietary requests'],
          ['Wine pairing suggestions'],
          ['Dessert options'],
        ],
        label: 'Topics',
      ),
    );
  }
}
0
likes
150
points
16
downloads

Publisher

unverified uploader

Weekly Downloads

Another starting point for interactive CLI using Dart, inspired by AstroJS CLI.

Repository (GitHub)

Documentation

API reference

Funding

Consider supporting this project:

ko-fi.com

License

MIT (license)

Dependencies

ansi_strip, args, logging

More

Packages that depend on fnds_cli