run method

  1. @override
Future<void> run()
override

Runs this command.

The return value is wrapped in a Future if necessary and returned by CommandRunner.runCommand.

Implementation

@override
Future<void> run() async {
  final force = argResults!['force'] as bool;
  final flavorsArg = argResults!['flavors'] as String?;
  final projectTypeArg = argResults!['project-type'] as String?;
  final cwd = Directory.current.path;

  // Guard against spaces after commas splitting the --flavors value across args
  // e.g. --flavors a, b  →  shell passes 'a,' and 'b' as separate tokens
  if (argResults!.rest.isNotEmpty) {
    AppLogger.error(
      'Unexpected argument(s): ${argResults!.rest.join(' ')}\n'
      '  Did you add spaces after commas in --flavors?\n'
      '  Correct usage (no spaces): --flavors dev.myapp.stage,dev.myapp.qa,dev.myapp',
    );
    exit(ExitCodes.usage);
  }

  // 1. Determine project type
  final projectType = projectTypeArg ?? _promptProjectType();

  // 2. Validate project root + parse project info
  late final String projectRoot;

  if (projectType == 'flutter') {
    final root = FlutterDetector.findProjectRoot(cwd);
    if (root == null) {
      AppLogger.error('Not a Flutter project — pubspec.yaml not found.');
      exit(ExitCodes.usage);
    }
    projectRoot = root;
    final pubspec = FlutterDetector.parsePubspec(projectRoot);
    AppLogger.info('Project : ${pubspec['name']} (v${pubspec['version']})');
  } else {
    if (!File(p.join(cwd, 'package.json')).existsSync()) {
      AppLogger.error('Not a React Native project — package.json not found.');
      exit(ExitCodes.usage);
    }
    final gradlePath = GradleParser.resolvePath(cwd);
    if (gradlePath == null) {
      AppLogger.error(
        'android/app/build.gradle(.kts) not found.\n'
        '  Run appflight init from your React Native project root.',
      );
      exit(ExitCodes.usage);
    }
    projectRoot = cwd;
    final pkgJson = jsonDecode(
      File(p.join(cwd, 'package.json')).readAsStringSync(),
    ) as Map<String, dynamic>;
    final projectName = pkgJson['name'] as String? ?? '';
    final gradleContent = File(gradlePath).readAsStringSync();
    final gradleFileName = p.basename(gradlePath);
    final versionName = GradleParser.parseVersionName(gradleContent);
    final versionCode = GradleParser.parseVersionCode(gradleContent);
    AppLogger.info('Project : $projectName');
    AppLogger.info(
      'Version : $versionName+$versionCode  (from android/app/$gradleFileName)',
    );
  }
  stdout.writeln('');

  // 3. Guard against overwriting without --force
  if (AppConfig.load(projectRoot) != null && !force) {
    AppLogger.warn(
      'appflight.json already exists. Use --force to overwrite.',
    );
    exit(ExitCodes.usage);
  }

  // 4. Tip for unflavored
  if (flavorsArg == null) {
    AppLogger.info('Tip: For apps with multiple flavors run:');
    AppLogger.info(
      '  appflight init --flavors stage:dev.myapp.stage,prod:dev.myapp',
    );
    stdout.writeln('');
  }

  final apps = <String, AppEntry>{};
  String? defaultFlavor;

  if (flavorsArg != null) {
    // --- Multi-flavor: package names passed directly via --flavors ---
    final entries = flavorsArg
        .split(',')
        .map((s) => s.trim())
        .where((s) => s.isNotEmpty)
        .toList();

    if (entries.isEmpty) {
      AppLogger.error(
        '--flavors is empty.\n'
        '  Example: --flavors stage:dev.myapp.stage,qa:dev.myapp.qa,prod:dev.myapp',
      );
      exit(ExitCodes.usage);
    }

    final usedPackageNames = <String>{};
    final usedFlavorKeys = <String>{};

    for (final entry in entries) {
      final parts = entry.split(':');
      if (parts.length != 2 ||
          parts[0].trim().isEmpty ||
          parts[1].trim().isEmpty) {
        AppLogger.error(
          'Invalid format: "$entry"\n'
          '  Each entry must be name:packageName.\n'
          '  Example: stage:dev.myapp.stage',
        );
        exit(ExitCodes.usage);
      }

      final flavorKey = parts[0].trim();
      final packageName = parts[1].trim();

      if (usedFlavorKeys.contains(flavorKey)) {
        AppLogger.error('Duplicate flavor name: "$flavorKey"');
        exit(ExitCodes.usage);
      }

      final error = validatePackageName(packageName, usedPackageNames);
      if (error != null) {
        AppLogger.error(error);
        exit(ExitCodes.usage);
      }

      usedPackageNames.add(packageName);
      usedFlavorKeys.add(flavorKey);
      apps[flavorKey] = AppEntry(
        appflightAppId: packageName,
        packageName: packageName,
        apkPath: projectType == 'react-native'
            ? p.join(
                'android',
                'app',
                'build',
                'outputs',
                'apk',
                flavorKey,
                'release',
                'app-$flavorKey-release.apk',
              )
            : FlutterDetector.defaultApkPath(flavorKey),
      );
    }

    if (apps.length > 1) {
      final keys = apps.keys.toList();
      final def = _prompt('Default flavor [${keys.first}]: ');
      defaultFlavor = def.isEmpty ? keys.first : def;
      if (!apps.containsKey(defaultFlavor)) {
        AppLogger.error('Unknown flavor: $defaultFlavor');
        exit(ExitCodes.usage);
      }
    }
  } else {
    // --- No-flavor: single package name, prompted interactively ---
    final packageName = _promptPackageName(
      'Package name (applicationId): ',
      {},
    );
    apps['default'] = AppEntry(
      appflightAppId: packageName,
      packageName: packageName,
      apkPath: projectType == 'react-native'
          ? p.join(
              'android',
              'app',
              'build',
              'outputs',
              'apk',
              'release',
              'app-release.apk',
            )
          : FlutterDetector.defaultSingleApkPath(),
    );
  }

  // 5. Write config
  AppConfig(
    version: 1,
    projectType: projectType,
    apps: apps,
    defaultFlavor: defaultFlavor,
  ).save(projectRoot);

  stdout.writeln('');
  AppLogger.success('appflight.json written.');
  CliAnalytics.ins.logInitCompleted(projectType: projectType);
  stdout.writeln('');
  AppLogger.info('Next steps:');
  AppLogger.info('  1. appflight login');
  final flavor = defaultFlavor ?? (apps.length == 1 ? null : apps.keys.first);
  if (projectType == 'react-native') {
    final gradleTask = flavor != null
        ? 'assemble${_capitalize(flavor)}Release'
        : 'assembleRelease';
    AppLogger.info('  2. cd android && ./gradlew $gradleTask');
  } else {
    final buildArg = flavor != null ? ' --flavor $flavor' : '';
    AppLogger.info('  2. flutter build apk$buildArg --release');
  }
  AppLogger.info(
    '  3. appflight upload${flavor != null ? ' --flavor $flavor' : ''}',
  );
}