|
| 1 | +import 'dart:convert'; |
1 | 2 | import 'dart:io'; |
2 | 3 | import '/metro/constants/strings.dart'; |
3 | 4 | import '/metro/metro_console.dart'; |
@@ -37,7 +38,6 @@ class MetroService { |
37 | 38 | } |
38 | 39 |
|
39 | 40 | argumentsForAction.removeAt(0); |
40 | | - |
41 | 41 | await nyCommand.action!(argumentsForAction); |
42 | 42 | } |
43 | 43 |
|
@@ -327,19 +327,49 @@ import '/resources/themes/${nameReCase.snakeCase}_theme.dart';"""; |
327 | 327 | } |
328 | 328 |
|
329 | 329 | /// Runs a process |
330 | | - static runProcess(String command) async { |
| 330 | + static Future<int> runProcess(String command) async { |
331 | 331 | List<String> commands = command.split(" "); |
332 | 332 |
|
333 | 333 | final processArguments = commands.getRange(1, commands.length).toList(); |
334 | | - final process = await Process.start(commands.first, processArguments, |
335 | | - runInShell: false); |
336 | | - await stdout.addStream(process.stdout); |
337 | | - await stderr.addStream(process.stderr); |
| 334 | + |
| 335 | + final process = |
| 336 | + await Process.start(commands.first, processArguments, runInShell: true); |
| 337 | + |
| 338 | + // Connect all streams |
| 339 | + process.stdout.pipe(stdout); |
| 340 | + process.stderr.pipe(stderr); |
| 341 | + stdin.pipe(process.stdin); // This pipes stdin to the child process |
| 342 | + |
| 343 | + final exitCode = await process.exitCode; |
| 344 | + |
| 345 | + if (exitCode != 0) { |
| 346 | + MetroConsole.writeInRed("Error: $exitCode"); |
| 347 | + } |
| 348 | + |
| 349 | + return exitCode; |
338 | 350 | } |
339 | 351 |
|
340 | 352 | /// Add a package to your pubspec.yaml file. |
341 | | - static addPackage(String package) async { |
342 | | - await runProcess("dart pub add $package"); |
| 353 | + static addPackage(String package, {String? version, bool dev = false}) async { |
| 354 | + String command = "dart pub add"; |
| 355 | + if (dev) { |
| 356 | + command += " --dev"; |
| 357 | + } |
| 358 | + command += " $package"; |
| 359 | + if (version != null) { |
| 360 | + command += ":$version"; |
| 361 | + } |
| 362 | + await runProcess(command); |
| 363 | + } |
| 364 | + |
| 365 | + /// Add a packages to your pubspec.yaml file. |
| 366 | + static addPackages(List<String> packages, {bool dev = false}) async { |
| 367 | + String command = "dart pub add"; |
| 368 | + if (dev) { |
| 369 | + command += " --dev"; |
| 370 | + } |
| 371 | + command += " ${packages.join(" ")}"; |
| 372 | + await runProcess(command); |
343 | 373 | } |
344 | 374 |
|
345 | 375 | /// Creates a new Model. |
@@ -444,6 +474,63 @@ final Map<Type, dynamic> modelDecoders = {${reg.allMatches(file).map((e) => e.gr |
444 | 474 | }); |
445 | 475 | } |
446 | 476 |
|
| 477 | + /// Creates a new command file. |
| 478 | + static makeCommand(String commandName, String value, |
| 479 | + {String folderPath = commandsFolder, |
| 480 | + bool forceCreate = false, |
| 481 | + String? creationPath, |
| 482 | + String? category}) async { |
| 483 | + String name = commandName.replaceAll(RegExp(r'(_?command)'), ""); |
| 484 | + ReCase nameReCase = ReCase(name); |
| 485 | + |
| 486 | + // create missing directories in the project |
| 487 | + await _makeDirectory(folderPath); |
| 488 | + await createDirectoriesFromCreationPath(creationPath, folderPath); |
| 489 | + |
| 490 | + // Check custom_commands.json file exists |
| 491 | + String customCommandsFilePath = "$folderPath/custom_commands.json"; |
| 492 | + if (!await hasFile(customCommandsFilePath)) { |
| 493 | + await _createNewFile(customCommandsFilePath, "[]"); |
| 494 | + } |
| 495 | + |
| 496 | + // create file path |
| 497 | + String filePath = createPathForDartFile( |
| 498 | + folderPath: folderPath, className: name, creationPath: creationPath); |
| 499 | + await _checkIfFileExists(filePath, shouldForceCreate: forceCreate); |
| 500 | + await _createNewFile(filePath, value, onSuccess: () async { |
| 501 | + MetroConsole.writeInGreen('[Command] ${name.snakeCase} created 🎉'); |
| 502 | + }); |
| 503 | + |
| 504 | + // Add to custom_commands.json |
| 505 | + String commandJson = jsonEncode({ |
| 506 | + "name": nameReCase.snakeCase, |
| 507 | + "category": category ?? "app", |
| 508 | + "script": "${nameReCase.snakeCase}.dart" |
| 509 | + }); |
| 510 | + |
| 511 | + try { |
| 512 | + File file = File(customCommandsFilePath); |
| 513 | + |
| 514 | + String customCommandsFile = await loadAsset(customCommandsFilePath); |
| 515 | + |
| 516 | + List<dynamic> commands = jsonDecode(customCommandsFile); |
| 517 | + if (!commands.any((command) => command["name"] == nameReCase.snakeCase)) { |
| 518 | + commands.add(jsonDecode(commandJson)); |
| 519 | + } |
| 520 | + String updatedCommands = jsonEncode(commands); |
| 521 | + |
| 522 | + // format json file |
| 523 | + String formattedJson = const JsonEncoder.withIndent(' ') |
| 524 | + .convert(jsonDecode(updatedCommands)); |
| 525 | + |
| 526 | + await file.writeAsString(formattedJson); |
| 527 | + } catch (e) { |
| 528 | + MetroConsole.writeInRed( |
| 529 | + '[Command] ${name.snakeCase} failed to create command: $e'); |
| 530 | + return; |
| 531 | + } |
| 532 | + } |
| 533 | + |
447 | 534 | /// Creates a new Stateful Widget. |
448 | 535 | static makeStatefulWidget(String className, String value, |
449 | 536 | {String folderPath = widgetsFolder, |
@@ -919,13 +1006,88 @@ final Map<Type, NyApiService> apiDecoders = {${reg.allMatches(file).map((e) => e |
919 | 1006 | forceCreate: (hasForceFlag ?? false)); |
920 | 1007 | break; |
921 | 1008 | } |
| 1009 | + case commandsFolder: |
| 1010 | + { |
| 1011 | + if (templateName.contains("_command")) { |
| 1012 | + templateName = templateName.replaceAll("_command", ""); |
| 1013 | + } |
| 1014 | + String category = 'app'; |
| 1015 | + if (template.options.containsKey('category')) { |
| 1016 | + category = template.options['category']; |
| 1017 | + } |
| 1018 | + await makeCommand(templateName, template.stub, |
| 1019 | + forceCreate: (hasForceFlag ?? false), category: category); |
| 1020 | + break; |
| 1021 | + } |
922 | 1022 | default: |
923 | 1023 | { |
924 | 1024 | continue; |
925 | 1025 | } |
926 | 1026 | } |
927 | 1027 | } |
928 | 1028 | } |
| 1029 | + |
| 1030 | + /// Discovers custom commands from a JSON file. |
| 1031 | + static Future<List<NyCommand>> discoverCustomCommands() async { |
| 1032 | + try { |
| 1033 | + final configFile = File('lib/app/commands/custom_commands.json'); |
| 1034 | + if (await configFile.exists()) { |
| 1035 | + final jsonStr = await configFile.readAsString(); |
| 1036 | + final List<dynamic> commandConfigs = jsonDecode(jsonStr); |
| 1037 | + |
| 1038 | + // Remove any duplicate commands from commandConfigs |
| 1039 | + final Set<String> commandNames = {}; |
| 1040 | + commandConfigs.removeWhere((config) { |
| 1041 | + final name = config['name']; |
| 1042 | + if (commandNames.contains(name)) { |
| 1043 | + return true; // Remove duplicate |
| 1044 | + } else { |
| 1045 | + commandNames.add(name); |
| 1046 | + return false; // Keep unique |
| 1047 | + } |
| 1048 | + }); |
| 1049 | + |
| 1050 | + // List of commands |
| 1051 | + List<NyCommand> allCommands = commandConfigs.map<NyCommand>((config) { |
| 1052 | + assert(config['name'] != null, |
| 1053 | + 'Command "name" is required in custom_commands.json'); |
| 1054 | + assert(config['script'] != null, |
| 1055 | + 'Command "script" is required in custom_commands.json'); |
| 1056 | + return NyCommand( |
| 1057 | + name: config['name'], |
| 1058 | + category: |
| 1059 | + config.containsKey('category') ? config['category'] : "app", |
| 1060 | + action: (args) => _executeCommandScript(config['script'], args), |
| 1061 | + ); |
| 1062 | + }).toList(); |
| 1063 | + |
| 1064 | + // Sort commands by category |
| 1065 | + allCommands.sort((a, b) { |
| 1066 | + if (a.category == b.category) { |
| 1067 | + return (a.name ?? "").compareTo(b.name ?? ""); |
| 1068 | + } |
| 1069 | + return (a.category ?? "").compareTo(b.category ?? ""); |
| 1070 | + }); |
| 1071 | + |
| 1072 | + return allCommands; |
| 1073 | + } |
| 1074 | + } catch (e) { |
| 1075 | + MetroConsole.writeInRed( |
| 1076 | + 'Error loading custom commands: $e\n\nMake sure to create a custom_commands.json file in the lib/app/commands directory.'); |
| 1077 | + } |
| 1078 | + return []; |
| 1079 | + } |
| 1080 | + |
| 1081 | + /// Executes a command script. |
| 1082 | + static Future<void> _executeCommandScript( |
| 1083 | + String scriptPath, List<String> args) async { |
| 1084 | + final script = File('lib/app/commands/$scriptPath'); |
| 1085 | + if (await script.exists()) { |
| 1086 | + await runProcess('dart run ${script.path} ${args.join(' ')}'); |
| 1087 | + } else { |
| 1088 | + MetroConsole.writeInRed('Command script not found: $scriptPath'); |
| 1089 | + } |
| 1090 | + } |
929 | 1091 | } |
930 | 1092 |
|
931 | 1093 | /// IterableExtension |
|
0 commit comments