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