fnds_cli 0.3.0 copy "fnds_cli: ^0.3.0" to clipboard
fnds_cli: ^0.3.0 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
140
points
41
downloads

Publisher

unverified uploader

Weekly Downloads

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

Documentation

API reference

License

MIT (license)

Dependencies

ansi_strip, args, logging

More

Packages that depend on fnds_cli