run function

Future<void> run(
  1. String name,
  2. List<String> args
)

Implementation

Future<void> run(String name, List<String> args) async {
  // If no YAML file exists and command is reserved, execute the original
  if (!hasYamlFile) {
    if (await ReservedCommands.isReserved(name)) {
      final exitCode = await _executeOriginal(name, args);
      exit(exitCode);
    } else {
      stderr.writeln('❌ No commands.yaml found');
      exit(1);
    }
  }

  final commands = loadCommandsFrom(yamlFile);

  // Check if this command has validation errors (was invalid during loading)
  if (commandValidationErrors.containsKey(name)) {
    final error = commandValidationErrors[name]!;
    stderr.writeln('❌ $error');
    exit(1);
  }

  final command = commands[name];
  if (command == null) {
    if (await ReservedCommands.isReserved(name)) {
      final exitCode = await _executeOriginal(name, args);
      exit(exitCode);
    } else {
      stderr.writeln('❌ Command: $bold$red$name$reset not found in commands.yaml');
      exit(1);
    }
  }

  if (await ReservedCommands.isReserved(name) && !command.override) {
    final exitCode = await _executeOriginal(name, args);
    exit(exitCode);
  }

  const helpFlags = ['--help', '-h'];

  // Check for help BEFORE resolving switches so we can show the switch options
  final isHelpRequested = helpFlags.any(args.contains);

  // For commands with switches, check if help is requested before resolving
  if (isHelpRequested && command.hasSwitches) {
    final paramFlags = [
      ...command.requiredParams.map((p) => p.flags).whereType<String>(),
      ...command.optionalParams.map((p) => p.flags).whereType<String>(),
    ];
    final paramOverridesHelp = paramFlags.any((f) => helpFlags.any((hf) => f.contains(hf)));
    final isAlias = command.script?.contains('...args') ?? false;

    if (!paramOverridesHelp && !isAlias) {
      print('$blue$name$reset${command.description != null ? ': $gray${command.description}$reset' : ''}');
      _printSwitchesHelp(command, '');
      exit(0);
    }
  }

  // Resolve switches recursively before processing params
  final resolvedData = await _resolveSwitches(command, args, name);
  final resolvedCommand = resolvedData.command;
  final resolvedArgs = resolvedData.args;

  final paramFlags = [
    ...resolvedCommand.requiredParams.map((p) => p.flags).whereType<String>(),
    ...resolvedCommand.optionalParams.map((p) => p.flags).whereType<String>(),
  ];

  final paramOverridesHelp = paramFlags.any((f) => helpFlags.any((hf) => f.contains(hf)));
  final isAlias = resolvedCommand.script?.contains('...args') ?? false;

  // Handle help for non-switch commands or resolved switch commands with params
  if (helpFlags.any(resolvedArgs.contains) && !paramOverridesHelp && !isAlias) {
    print(
      '$blue$name$reset${resolvedCommand.description != null ? ': $gray${resolvedCommand.description}$reset' : ''}',
    );

    // Display params if command has them (don't show switches here, they were already handled above)
    if (resolvedCommand.requiredParams.isNotEmpty || resolvedCommand.optionalParams.isNotEmpty) {
      print('params:');
      if (resolvedCommand.requiredParams.isNotEmpty) {
        print('  required:');
        for (final param in resolvedCommand.requiredParams) {
          _printParamHelp(param);
        }
      }
      if (resolvedCommand.optionalParams.isNotEmpty) {
        print('  optional:');
        for (final param in resolvedCommand.optionalParams) {
          _printParamHelp(param);
        }
      }
    }
    exit(0);
  }

  var commandText = resolvedCommand.script ?? '';

  final commandValues = <String, String?>{};
  final positionalParams = <String>[];
  final optionalPositionalParams = <String>[];
  final optionalParamAliases = <String, String>{};

  for (final param in resolvedCommand.requiredParams) {
    if (param.flags != null) {
      final aliases = param.flags!.split(',').map((s) => s.trim()).where((s) => s.isNotEmpty);
      for (final alias in aliases) {
        optionalParamAliases[alias] = param.name;
      }
    } else {
      positionalParams.add(param.name);
    }
    commandValues[param.name] = param.defaultValue;
  }

  for (final param in resolvedCommand.optionalParams) {
    if (param.flags != null) {
      final aliases = param.flags!.split(',').map((s) => s.trim()).where((s) => s.isNotEmpty);
      for (final alias in aliases) {
        optionalParamAliases[alias] = param.name;
      }
    } else {
      optionalPositionalParams.add(param.name);
    }
    commandValues[param.name] = param.defaultValue;
  }

  final positionalArgs = <String>[];
  final passthroughArgs = <String>[];
  final argsCopy = List<String>.from(resolvedArgs);

  // Helper to get param object by name
  Param getParamByName(String name) {
    try {
      return resolvedCommand.requiredParams.firstWhere((p) => p.name == name);
    } catch (_) {
      try {
        return resolvedCommand.optionalParams.firstWhere((p) => p.name == name);
      } catch (_) {
        // Fallback - should not happen in normal flow
        return Param(name: name);
      }
    }
  }

  while (argsCopy.isNotEmpty) {
    final arg = argsCopy.removeAt(0);

    if (optionalParamAliases.containsKey(arg)) {
      final paramName = optionalParamAliases[arg]!;
      final param = getParamByName(paramName);
      final isRequired = resolvedCommand.requiredParams.any((p) => p.name == paramName);

      // Handle boolean flags - if boolean type, flag presence toggles default
      if (param.isBoolean) {
        // Check if there's an explicit value like --verbose=true
        if (argsCopy.isNotEmpty && !argsCopy.first.startsWith('-')) {
          final nextArg = argsCopy.first;
          if (nextArg == 'true' || nextArg == 'false') {
            commandValues[paramName] = argsCopy.removeAt(0);
          } else {
            // Invalid boolean value provided
            stderr.writeln('❌ Parameter $bold$red$paramName$reset expects a $gray[boolean]$reset');
            stderr.writeln('   Got: "$nextArg" $gray[string]$reset');
            stderr.writeln('πŸ’‘ Example: $bgGreen$black$name $arg true$reset or $bgGreen$black$name $arg false$reset');
            exit(1);
          }
        } else {
          // Flag present without value = toggle the default value
          final currentValue = commandValues[paramName] ?? 'false';
          commandValues[paramName] = currentValue == 'true' ? 'false' : 'true';
        }
      } else {
        // Non-boolean parameter - requires a value
        if (argsCopy.isNotEmpty && !argsCopy.first.startsWith('-')) {
          final value = argsCopy.removeAt(0);

          // Validate enum values
          if (param.isEnum && !param.isValidValue(value)) {
            stderr.writeln('❌ Parameter $red$paramName$reset has invalid value: "$value"');
            final allowedValues = param.values!.map((v) => '$green$v$reset').join(', ');
            stderr.writeln('πŸ’‘ Must be one of: $allowedValues');
            exit(1);
          }

          // Validate and parse numeric types
          if (param.type == 'int') {
            if (int.tryParse(value) == null) {
              stderr.writeln('❌ Parameter $bold$red$paramName$reset expects an $gray[integer]$reset');
              stderr.writeln('   Got: "$value" $gray[string]$reset');
              stderr.writeln('πŸ’‘ Example: $bgGreen$black$name $arg 42$reset');
              exit(1);
            }
          } else if (param.type == 'double') {
            if (double.tryParse(value) == null) {
              stderr.writeln('❌ Parameter $bold$red$paramName$reset expects a $gray[number]$reset');
              stderr.writeln('   Got: "$value" $gray[string]$reset');
              stderr.writeln('πŸ’‘ Example: $bgGreen$black$name $arg 3.14$reset');
              exit(1);
            }
          }

          commandValues[paramName] = value;
        } else {
          if (isRequired) {
            stderr.writeln('❌ Missing value for param: $paramName');
            exit(1);
          }
        }
      }
    } else {
      positionalArgs.add(arg);
      passthroughArgs.add(arg);
    }
  }

  final missingPositional = <String>[];
  final missingNamed = <String>[];

  // Process positional parameters FIRST (before enum picker)
  // This ensures invalid values are caught and reported as errors
  // instead of triggering the interactive picker
  final allPositionalParams = positionalParams + optionalPositionalParams;
  for (var i = 0; i < allPositionalParams.length; i++) {
    final paramName = allPositionalParams[i];
    if (i < positionalArgs.length) {
      final value = positionalArgs[i];
      final param = getParamByName(paramName);

      // Validate enum values
      if (param.isEnum && !param.isValidValue(value)) {
        stderr.writeln('❌ Parameter $red$paramName$reset has invalid value: "$value"');
        final allowedValues = param.values!.map((v) => '$green$v$reset').join(', ');
        stderr.writeln('πŸ’‘ Must be one of: $allowedValues');
        exit(1);
      }

      // Validate boolean types
      if (param.type == 'boolean' && value != 'true' && value != 'false') {
        stderr.writeln('❌ Parameter $bold$red$paramName$reset expects a $gray[boolean]$reset');
        stderr.writeln('   Got: "$value" $gray[string]$reset');
        stderr.writeln('πŸ’‘ Example: $bgGreen$black$name true$reset or $bgGreen$black$name false$reset');
        exit(1);
      }

      // Validate numeric types
      if (param.type == 'int' && int.tryParse(value) == null) {
        stderr.writeln('❌ Parameter $bold$red$paramName$reset expects an $gray[integer]$reset');
        stderr.writeln('   Got: "$value" $gray[string]$reset');
        stderr.writeln('πŸ’‘ Example: $bgGreen$black$name 42$reset');
        exit(1);
      }

      if (param.type == 'double' && double.tryParse(value) == null) {
        stderr.writeln('❌ Parameter $bold$red$paramName$reset expects a $gray[number]$reset');
        stderr.writeln('   Got: "$value" $gray[string]$reset');
        stderr.writeln('πŸ’‘ Example: $bgGreen$black$name 3.14$reset');
        exit(1);
      }

      commandValues[paramName] = value;
    } else if (commandValues[paramName] == null && positionalParams.contains(paramName)) {
      // Don't add to missingPositional if it's an enum that requires a picker
      // The picker will handle it after this section
      final param = getParamByName(paramName);
      if (!param.requiresEnumPicker) {
        missingPositional.add(paramName);
      }
    }
  }

  // Handle enum pickers for parameters without defaults and without provided values
  // Check both required and optional params for enums that need picker
  // This runs AFTER positional processing to ensure invalid values are caught first
  final allParams = [...resolvedCommand.requiredParams, ...resolvedCommand.optionalParams];
  for (final param in allParams) {
    // Only show picker if:
    // 1. Parameter is an enum (has values)
    // 2. No default value exists
    // 3. No value has been provided yet
    if (param.requiresEnumPicker && commandValues[param.name] == null) {
      final selectedValue = EnumPicker.pick(param, param.name);

      if (selectedValue == null) {
        // User cancelled - exit gracefully
        exit(0);
      }

      commandValues[param.name] = selectedValue;
    }
  }

  for (final param in resolvedCommand.requiredParams) {
    if (param.flags != null) {
      if (commandValues[param.name] == null) {
        missingNamed.add(param.name);
      }
    }
  }

  if (missingPositional.isNotEmpty) {
    stderr.writeln(
      '❌ Missing required positional param${missingPositional.length > 1 ? 's' : ''}: ${missingPositional.map((p) => '$bold$red$p$reset').join(', ')}',
    );
    exit(1);
  }

  if (missingNamed.isNotEmpty) {
    stderr.writeln(
      '❌ Missing required named param${missingNamed.length > 1 ? 's' : ''}: ${missingNamed.map((p) => '$bold$red$p$reset').join(', ')}',
    );
    exit(1);
  }

  commandValues.forEach((k, v) {
    if (v != null) {
      commandText = commandText.replaceAll('{$k}', v);
    } else {
      commandText = commandText.replaceAll('{$k}', '');
    }
  });

  commandText = commandText.replaceAll('...args', passthroughArgs.join(' '));

  final process = await Process.start(
    Platform.isWindows ? 'cmd' : 'sh',
    Platform.isWindows ? ['/C', commandText] : ['-c', commandText],
    mode: ProcessStartMode.inheritStdio,
  );

  exit(await process.exitCode);
}