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);
}