execute method

Future<void> execute()

Implementation

Future<void> execute() async {
  _log.info('šŸš€ Welcome to Flavor CLI! Let\'s set up your environment.');

  // 1. Choose flavors
  var flavorSelection = _log.chooseOne(
    'šŸ‘‰ Which flavor setup do you need ?',
    choices: ['dev, prod', 'dev, stage, prod', 'Enter manually'],
  );

  List<String> flavors;
  while (true) {
    if (flavorSelection == 'Enter manually') {
      final input = _log.prompt(
        'šŸ‘‰ List your flavors (comma separated)',
        defaultValue: 'dev, stage, prod',
      );
      flavors = input
          .split(',')
          .map((e) => e.trim().toLowerCase())
          .where((e) => e.isNotEmpty)
          .toList();
    } else {
      flavors = flavorSelection
          .split(',')
          .map((e) => e.trim().toLowerCase())
          .toList();
    }

    bool allFlavorsValid = true;
    for (final flavor in flavors) {
      if (!ValidationUtils.isValidIdentifier(flavor)) {
        _log.error(
          'āŒ Invalid flavor name: "$flavor". Must be a valid Dart identifier (start with letter, no spaces, no special characters).',
        );
        allFlavorsValid = false;
      }
    }

    if (flavors.length < 2) {
      _log.error(
        'āŒ Error: You need at least 2 flavors to use this tool (e.g., dev and prod).',
      );
      allFlavorsValid = false;
    }

    if (allFlavorsValid) break;

    if (flavorSelection != 'Enter manually') {
      flavorSelection = 'Enter manually';
    }
    _log.info('Please try again.');
  }

  // 3. Choose fields
  final fields = <String, String>{};
  while (true) {
    final fieldInput = _log.prompt(
      'šŸ‘‰ What variables should your AppConfig have ?',
      defaultValue: 'String baseUrl, bool debug',
    );

    final parts = fieldInput.split(',').map((e) => e.trim()).toList();
    bool allValid = true;

    for (var part in parts) {
      if (part.isEmpty) continue;
      final entry = part.split(' ');
      if (entry.length != 2) {
        _log.error('āŒ Invalid format: "$part". Use "Type Name"');
        allValid = false;
        break;
      }
      final type = entry[0];
      final name = entry[1];

      const validTypes = ['String', 'int', 'bool', 'double'];
      if (!validTypes.contains(type)) {
        _log.error('āŒ Invalid type: "$type". Use: String, int, bool, double');
        allValid = false;
        break;
      }

      if (!ValidationUtils.isValidIdentifier(name)) {
        _log.error(
          'āŒ Invalid variable name: "$name". Must be a valid Dart identifier.',
        );
        allValid = false;
        break;
      }

      fields[name] = type;
    }

    if (allValid && fields.isNotEmpty) break;
    _log.info('Please try again.');
  }

  // 4. Choose AppConfig path
  var appConfigPath = _log.prompt(
    'šŸ‘‰ Where should AppConfig be created ?',
    defaultValue: 'lib/core/config/app_config.dart',
  );

  appConfigPath = appConfigPath.trim();
  if (appConfigPath.startsWith('Example: ')) {
    appConfigPath = appConfigPath.replaceFirst('Example: ', '');
  }
  if (!appConfigPath.endsWith('.dart')) {
    appConfigPath = p.join(appConfigPath, 'app_config.dart');
  }

  // 5. Choose Main strategy
  final strategy = _log.chooseOne(
    'šŸ‘‰ Which main strategy do you prefer ?',
    choices: [
      'Separate main files per flavor (e.g., main_dev.dart)',
      'Single main file for all flavors',
    ],
  );
  final useSeparateMains = strategy.startsWith('Separate');

  // 6. App Name
  final detectedName = _detectAppName();
  final appName = _log.prompt(
    'šŸ‘‰ What is your App Name?',
    defaultValue: detectedName,
  );

  // 7. Identify Production Flavor
  String productionFlavor;
  if (flavors.contains('prod')) {
    productionFlavor = 'prod';
  } else if (flavors.contains('production')) {
    productionFlavor = 'production';
  } else {
    productionFlavor = _log.chooseOne(
      'šŸ‘‰ Which one is the production flavor?',
      choices: flavors,
    );
  }

  // 7.5 Custom App Names
  final flavorAppNames = <String, String>{};
  final useCustomNames = _log.confirm(
    'šŸ‘‰ Do you want to set custom app names for your flavors? (e.g. "$appName Dev")',
    defaultValue: false,
  );
  if (useCustomNames) {
    for (final flavor in flavors) {
      final defaultName = flavor == productionFlavor
          ? appName
          : '$appName-${flavor[0].toUpperCase()}${flavor.substring(1)}';
      final name = _log.prompt(
        '   → App name for $flavor:',
        defaultValue: defaultName,
      );
      flavorAppNames[flavor] = name;
    }
  } else {
    // Auto-generate default app names so they are always persisted in the config
    for (final flavor in flavors) {
      flavorAppNames[flavor] =
          flavor == productionFlavor ? appName : '$appName-$flavor';
    }
  }

  // 8. Base Package ID
  final detectedId = _detectPackageId();
  final packageId = _log.prompt(
    'šŸ‘‰ What is your Production Package ID? (Your unique App ID, e.g., com.example.app)',
    defaultValue: detectedId,
  );

  // 9. ID strategy
  final idStrategy = _log.chooseOne(
    'šŸ‘‰ Which package ID strategy do you prefer?',
    choices: [
      'Unique IDs per flavor (recommended) — appends .flavorName to non-production flavors',
      'Shared ID — all flavors use the same package ID',
    ],
  );
  final useSuffix = idStrategy.startsWith('Unique');

  // 10. Firebase
  FirebaseConfig? firebaseConfig;
  bool enableFirebase = ConfigService.hasFirebase();
  if (enableFirebase) {
    _log.info('✨ Firebase detected in project, enabling support.');
  } else {
    enableFirebase = _log.confirm(
      'šŸ‘‰ Enable Firebase support?',
      defaultValue: false,
    );
  }

  if (enableFirebase) {
    // Delegate to FirebaseCommand to avoid duplicating the strategy/project-ID prompt logic
    final tempConfig = FlavorConfig(
      flavors: flavors,
      appName: '',
      fields: const {},
      flavorValues: const {},
      appConfigPath: '',
      useSeparateMains: false,
      useSuffix: useSuffix,
      android: AndroidConfig(applicationId: ''),
      ios: IosConfig(bundleId: ''),
      platforms: const [],
      productionFlavor: '',
    );
    firebaseConfig = FirebaseCommand.promptForFirebaseConfig(
      _log,
      tempConfig,
    );
  }

  // 11. Per-flavor field values
  final flavorValues = <String, Map<String, dynamic>>{};
  _log.info('\nšŸ“ Now let\'s set the values for your variables per flavor:');

  for (final fieldName in fields.keys) {
    final type = fields[fieldName]!;
    _log.info('Variable: $fieldName ($type)');
    for (final flavor in flavors) {
      final defaultValue = TypeUtils.getDefaultValueForType(type);
      final input = _log
          .prompt(
            '   → Value for $fieldName ($flavor):',
            defaultValue: defaultValue,
          )
          .trim();
      final typedVal = TypeUtils.parseToType(type, input);
      flavorValues.putIfAbsent(flavor, () => {})[fieldName] = typedVal;
    }
  }
  _log.info('');

  // 12. Gitignore env
  final gitignoreEnv = _log.confirm(
    'šŸ‘‰ Do you want to add .env files to .gitignore?',
    defaultValue: true,
  );

  // 13. Select Platforms
  final root = ConfigService.root;
  final detectedPlatforms = <String>[];
  if (Directory(p.join(root, 'android')).existsSync())
    detectedPlatforms.add('android');
  if (Directory(p.join(root, 'ios')).existsSync())
    detectedPlatforms.add('ios');
  if (Directory(p.join(root, 'macos')).existsSync())
    detectedPlatforms.add('macos');
  if (Directory(p.join(root, 'web')).existsSync())
    detectedPlatforms.add('web');

  List<String> selectedPlatforms = detectedPlatforms;
  if (detectedPlatforms.isNotEmpty) {
    final onlyMobile =
        detectedPlatforms.every((p) => p == 'android' || p == 'ios');
    if (!onlyMobile) {
      selectedPlatforms = _log.chooseAny(
        'šŸ‘‰ Which platforms do you want to generate flavors for?',
        choices: detectedPlatforms,
        defaultValues: detectedPlatforms,
      );
      if (selectedPlatforms.isEmpty) {
        _log.warn(
          'āš ļø No platforms selected. Proceeding anyway, but no platform-specific setup will run.',
        );
      }
    }
  }

  // Create FlavorConfig using collected data
  final config = FlavorConfig(
    flavors: flavors,
    appName: appName,
    fields: fields,
    flavorValues: flavorValues,
    appConfigPath: appConfigPath,
    useSeparateMains: useSeparateMains,
    useSuffix: useSuffix,
    android: AndroidConfig(applicationId: packageId),
    ios: IosConfig(bundleId: packageId),
    platforms: selectedPlatforms,
    productionFlavor: productionFlavor,
    flavorAppNames: flavorAppNames.isNotEmpty ? flavorAppNames : null,
    firebase: firebaseConfig,
    gitignoreEnv: gitignoreEnv,
  );
  await SetupRunner(logger: _log).run(config);
}