From b8ce1529332941c7e24898325256e3f4936fa01d Mon Sep 17 00:00:00 2001 From: Elijah Luckey Date: Fri, 18 Apr 2025 21:46:23 -0400 Subject: [PATCH 01/20] feat: Integrate mason_logger for improved logging and user prompts --- lib/commands/create_command.dart | 7 ++ lib/utils.dart | 116 +++++-------------------------- pubspec.lock | 10 +-- pubspec.yaml | 2 +- 4 files changed, 27 insertions(+), 108 deletions(-) diff --git a/lib/commands/create_command.dart b/lib/commands/create_command.dart index 5eb6851..baba160 100644 --- a/lib/commands/create_command.dart +++ b/lib/commands/create_command.dart @@ -11,6 +11,7 @@ import 'package:process_run/process_run.dart'; Future createCommand(ArgResults command) async { final interactive = command['interactive'] != 'false'; + final logger = Logger(); if (interactive) { stdout.write('\nWelcome to ${ansi.red.wrap('Ignite CLI')}! 🔥\n'); @@ -19,6 +20,7 @@ Future createCommand(ArgResults command) async { final name = getString( isInteractive: interactive, + logger: logger, command, 'name', 'Choose a name for your project: ', @@ -34,6 +36,7 @@ Future createCommand(ArgResults command) async { ); final org = getString( + logger: logger, isInteractive: interactive, command, 'org', @@ -51,6 +54,7 @@ Future createCommand(ArgResults command) async { final versions = FlameVersionManager.singleton.versions; final flameVersions = versions[Package.flame]!; final flameVersion = getOption( + logger: logger, isInteractive: interactive, command, 'flame-version', @@ -65,6 +69,7 @@ Future createCommand(ArgResults command) async { .map((key) => key.name) .toList(); final extraPackages = getMultiOption( + logger: logger, isInteractive: interactive, isRequired: false, command, @@ -86,6 +91,7 @@ Future createCommand(ArgResults command) async { print('\nYour current directory is: $currentDir'); final createFolder = getOption( + logger: logger, isInteractive: interactive, command, 'create-folder', @@ -100,6 +106,7 @@ Future createCommand(ArgResults command) async { print('\n'); final template = getOption( + logger: logger, isInteractive: interactive, command, 'template', diff --git a/lib/utils.dart b/lib/utils.dart index 3ab2d06..310251b 100644 --- a/lib/utils.dart +++ b/lib/utils.dart @@ -1,10 +1,9 @@ import 'dart:io'; import 'package:args/args.dart'; -import 'package:charcode/ascii.dart' as ascii; import 'package:io/ansi.dart' as ansi; +import 'package:mason_logger/mason_logger.dart' as logger; import 'package:path/path.dart' as p; -import 'package:prompts/prompts.dart' as prompts; String getBundledFile(String name) { final binFolder = p.dirname(p.fromUri(Platform.script)); @@ -16,6 +15,7 @@ String getString( String name, String message, { required bool isInteractive, + required logger.Logger logger, String? desc, String? Function(String)? validate, }) { @@ -35,21 +35,10 @@ String getString( if (desc != null) { stdout.write(ansi.darkGray.wrap('\n$desc\u{1B}[1A\r')); } - value = prompts.get( - message, - validate: (e) { - final error = validate?.call(e); - if (error != null) { - // clear the line - stdout.write('\n\r\u{1B}[K'); - stdout.write(ansi.red.wrap('$error\u{1B}[1A\r')); - return false; - } else { - stdout.write('\n\r\u{1B}[K'); - return true; - } - }, - ); + + value = logger.prompt(message); + + // TODO(elijah): validation callback if (desc != null) { stdout.write('\r\u{1B}[K'); } @@ -63,11 +52,13 @@ String getOption( String message, Map options, { required bool isInteractive, + required logger.Logger logger, String? desc, String? defaultsTo, Map fullOptions = const {}, }) { var value = results[name] as String?; + if (!isInteractive) { if (value == null) { if (defaultsTo != null) { @@ -83,11 +74,15 @@ String getOption( print('Invalid value $value provided. Must be in: ${options.values}'); value = null; } + while (value == null) { if (desc != null) { stdout.write(ansi.darkGray.wrap('\n$desc\u{1B}[1A\r')); } - value = options[prompts.choose(message, options.keys)]; + + final option = logger.chooseOne(message, choices: options.keys.toList()); + value = options[option]; + if (desc != null) { stdout.write('\r\u{1B}[K'); } @@ -114,6 +109,7 @@ List getMultiOption( List options, { required bool isInteractive, required bool isRequired, + required logger.Logger logger, List startingOptions = const [], String? desc, }) { @@ -130,6 +126,7 @@ List getMultiOption( return value; } } + if (value.any((e) => !options.contains(e))) { print('Invalid value $value provided. Must be in: $options'); value = []; @@ -137,92 +134,15 @@ List getMultiOption( if (desc != null) { stdout.write(ansi.darkGray.wrap('\n$desc\u{1B}[1A\r')); } - final selectedOptions = value.isEmpty ? startingOptions : value; - value = cbx(message, options, selectedOptions); + + value = logger.chooseAny(message, choices: options); + if (desc != null) { stdout.write('\r\u{1B}[K'); } return value; } -List cbx( - String message, - List keys, - List startingKeys, -) { - final selected = startingKeys; - var hereIdx = 0; - - var needsClear = false; - void writeIt() { - if (needsClear) { - for (var i = 0; i <= keys.length; i++) { - prompts.goUpOneLine(); - prompts.clearLine(); - } - } else { - needsClear = true; - } - print(message); - keys.asMap().forEach((index, option) { - final isSelected = selected.contains(option); - final isHere = index == hereIdx; - final text = ' ${isSelected ? '♦' : '♢'} $option'; - final color = isHere ? ansi.cyan : ansi.darkGray; - print(color.wrap(text)); - }); - } - - final oldEchoMode = stdin.echoMode; - final oldLineMode = stdin.lineMode; - while (true) { - int ch; - writeIt(); - - try { - stdin.lineMode = stdin.echoMode = false; - ch = stdin.readByteSync(); - - if (ch == ascii.$esc) { - ch = stdin.readByteSync(); - if (ch == ascii.$lbracket) { - ch = stdin.readByteSync(); - if (ch == ascii.$A) { - // Up key - hereIdx--; - if (hereIdx < 0) { - hereIdx = keys.length - 1; - } - writeIt(); - } else if (ch == ascii.$B) { - // Down key - hereIdx++; - if (hereIdx >= keys.length) { - hereIdx = 0; - } - writeIt(); - } - } - } else if (ch == ascii.$lf) { - // Enter key pressed - submit - return selected; - } else if (ch == ascii.$space) { - // Space key pressed - selected/unselect - final key = keys[hereIdx]; - if (selected.contains(key)) { - selected.remove(key); - } else { - selected.add(key); - } - writeIt(); - } - } finally { - stdin.lineMode = oldLineMode; - stdin.echoMode = oldEchoMode; - } - } -} - extension SortedBy on Iterable { Iterable sortedBy(Comparable Function(T) selector) { return toList()..sort((a, b) => selector(a).compareTo(selector(b))); diff --git a/pubspec.lock b/pubspec.lock index a3d104d..a3556dc 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -306,7 +306,7 @@ packages: source: hosted version: "0.1.2" mason_logger: - dependency: transitive + dependency: "direct main" description: name: mason_logger sha256: "6d5a989ff41157915cb5162ed6e41196d5e31b070d2f86e1c2edf216996a158c" @@ -409,14 +409,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.4" - prompts: - dependency: "direct main" - description: - name: prompts - sha256: "3773b845e85a849f01e793c4fc18a45d52d7783b4cb6c0569fad19f9d0a774a1" - url: "https://pub.dev" - source: hosted - version: "2.0.0" pub_semver: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 0a03079..eff4693 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,9 +18,9 @@ dependencies: http: ^1.3.0 io: ^1.0.5 mason: ^0.1.1 + mason_logger: ^0.3.3 path: ^1.9.1 process_run: ^1.2.4 - prompts: ^2.0.0 yaml: ^3.1.3 dev_dependencies: From 6927bb236e376f0bf19e114b024cc081b763ad8e Mon Sep 17 00:00:00 2001 From: Elijah Luckey Date: Fri, 18 Apr 2025 22:10:41 -0400 Subject: [PATCH 02/20] switch to more descriptive exit codes & logger --- lib/utils.dart | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/lib/utils.dart b/lib/utils.dart index 310251b..0443773 100644 --- a/lib/utils.dart +++ b/lib/utils.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:args/args.dart'; import 'package:io/ansi.dart' as ansi; +import 'package:io/io.dart'; import 'package:mason_logger/mason_logger.dart' as logger; import 'package:path/path.dart' as p; @@ -22,25 +23,26 @@ String getString( var value = results[name] as String?; if (!isInteractive) { if (value == null || value.isEmpty) { - print('Missing parameter $name is required.'); - exit(1); + logger.err('Missing parameter $name is required.'); + exit(ExitCode.usage.code); } final error = validate?.call(value); if (error != null) { - print('Invalid value $value provided: $error'); - exit(1); + logger.err('Invalid value $value provided: $error'); + exit(ExitCode.usage.code); } } while (value == null || value.isEmpty) { if (desc != null) { - stdout.write(ansi.darkGray.wrap('\n$desc\u{1B}[1A\r')); + logger.info(ansi.darkGray.wrap('\n$desc\u{1B}[1A\r')); } value = logger.prompt(message); // TODO(elijah): validation callback + if (desc != null) { - stdout.write('\r\u{1B}[K'); + logger.info('\r\u{1B}[K'); } } return value; @@ -64,27 +66,27 @@ String getOption( if (defaultsTo != null) { return defaultsTo; } else { - print('Missing parameter $name is required.'); - exit(1); + logger.err('Missing parameter $name is required.'); + exit(ExitCode.usage.code); } } } final fullValues = {...options, ...fullOptions}.values; if (value != null && !fullValues.contains(value)) { - print('Invalid value $value provided. Must be in: ${options.values}'); + logger.err('Invalid value $value provided. Must be in: ${options.values}'); value = null; } while (value == null) { if (desc != null) { - stdout.write(ansi.darkGray.wrap('\n$desc\u{1B}[1A\r')); + logger.info(ansi.darkGray.wrap('\n$desc\u{1B}[1A\r')); } final option = logger.chooseOne(message, choices: options.keys.toList()); value = options[option]; if (desc != null) { - stdout.write('\r\u{1B}[K'); + logger.info('\r\u{1B}[K'); } } return value; @@ -119,8 +121,8 @@ List getMultiOption( if (startingOptions.isNotEmpty) { return startingOptions; } else { - print('Missing parameter $name is required.'); - exit(1); + logger.err('Missing parameter $name is required.'); + exit(ExitCode.usage.code); } } else { return value; @@ -128,17 +130,17 @@ List getMultiOption( } if (value.any((e) => !options.contains(e))) { - print('Invalid value $value provided. Must be in: $options'); + logger.err('Invalid value $value provided. Must be in: $options'); value = []; } if (desc != null) { - stdout.write(ansi.darkGray.wrap('\n$desc\u{1B}[1A\r')); + logger.info(ansi.darkGray.wrap('\n$desc\u{1B}[1A\r')); } value = logger.chooseAny(message, choices: options); if (desc != null) { - stdout.write('\r\u{1B}[K'); + logger.info('\r\u{1B}[K'); } return value; } From c858e09c6fa15820b595f7632d0dcd9830d201b3 Mon Sep 17 00:00:00 2001 From: Elijah Luckey Date: Fri, 18 Apr 2025 22:11:16 -0400 Subject: [PATCH 03/20] use logger in create and version command logs --- lib/commands/create_command.dart | 3 +-- lib/commands/version_command.dart | 11 ++++++----- lib/main.dart | 23 ++++++++++++++--------- 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/lib/commands/create_command.dart b/lib/commands/create_command.dart index baba160..6f34a3f 100644 --- a/lib/commands/create_command.dart +++ b/lib/commands/create_command.dart @@ -9,9 +9,8 @@ import 'package:io/ansi.dart' as ansi; import 'package:mason/mason.dart'; import 'package:process_run/process_run.dart'; -Future createCommand(ArgResults command) async { +Future createCommand(ArgResults command, Logger logger) async { final interactive = command['interactive'] != 'false'; - final logger = Logger(); if (interactive) { stdout.write('\nWelcome to ${ansi.red.wrap('Ignite CLI')}! 🔥\n'); diff --git a/lib/commands/version_command.dart b/lib/commands/version_command.dart index 3d0a6e9..825a547 100644 --- a/lib/commands/version_command.dart +++ b/lib/commands/version_command.dart @@ -1,11 +1,12 @@ import 'package:ignite_cli/version.g.dart'; +import 'package:mason/mason.dart'; import 'package:process_run/process_run.dart'; -Future versionCommand() async { - print(r'$ ignite --version:'); - print(igniteVersion); - print(''); +Future versionCommand(Logger logger) async { + logger.detail(r'$ ignite --version:'); + logger.detail(igniteVersion); + logger.detail(''); await runExecutableArguments('dart', ['--version'], verbose: true); - print(''); + logger.detail(''); await runExecutableArguments('flutter', ['--version'], verbose: true); } diff --git a/lib/main.dart b/lib/main.dart index de09018..a9c37a3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,11 +3,14 @@ import 'package:completion/completion.dart' as completion; import 'package:ignite_cli/commands/create_command.dart'; import 'package:ignite_cli/commands/version_command.dart'; import 'package:ignite_cli/flame_version_manager.dart'; +import 'package:mason_logger/mason_logger.dart'; Future mainCommand(List args) async { await FlameVersionManager.init(); final parser = ArgParser(); + final logger = Logger(); + parser.addFlag('help', abbr: 'h', help: 'Displays this message.'); parser.addFlag('version', abbr: 'v', help: 'Shows relevant version info.'); @@ -56,22 +59,24 @@ Future mainCommand(List args) async { final results = completion.tryArgsCompletion(args, parser); if (results['help'] as bool) { - print(parser.usage); - print(''); - print('List of available commands:'); - print(''); - print('create:'); - print(' ${create.usage}'); + logger.info(parser.usage); + logger.info(''); + logger.info('List of available commands:'); + logger.info(''); + logger.info('create:'); + logger.info(' ${create.usage}'); return; } else if (results['version'] as bool) { - await versionCommand(); + await versionCommand(logger); return; } final command = results.command; if (command?.name == 'create') { - await createCommand(command!); + await createCommand(command!, logger); } else { - print('Invalid command. Please select an option, use --help for help.'); + logger.err( + 'Invalid command. Please select an option, use --help for help.', + ); } } From dc7ff3bd366d709b08f39b50860513727699383c Mon Sep 17 00:00:00 2001 From: Elijah Luckey Date: Fri, 18 Apr 2025 22:19:17 -0400 Subject: [PATCH 04/20] refactor: Change error logging to info level for command validation --- lib/main.dart | 2 +- lib/utils.dart | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index a9c37a3..53294a1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -75,7 +75,7 @@ Future mainCommand(List args) async { if (command?.name == 'create') { await createCommand(command!, logger); } else { - logger.err( + logger.info( 'Invalid command. Please select an option, use --help for help.', ); } diff --git a/lib/utils.dart b/lib/utils.dart index 0443773..22a0c77 100644 --- a/lib/utils.dart +++ b/lib/utils.dart @@ -23,12 +23,12 @@ String getString( var value = results[name] as String?; if (!isInteractive) { if (value == null || value.isEmpty) { - logger.err('Missing parameter $name is required.'); + logger.info('Missing parameter $name is required.'); exit(ExitCode.usage.code); } final error = validate?.call(value); if (error != null) { - logger.err('Invalid value $value provided: $error'); + logger.info('Invalid value $value provided: $error'); exit(ExitCode.usage.code); } } @@ -66,14 +66,14 @@ String getOption( if (defaultsTo != null) { return defaultsTo; } else { - logger.err('Missing parameter $name is required.'); + logger.info('Missing parameter $name is required.'); exit(ExitCode.usage.code); } } } final fullValues = {...options, ...fullOptions}.values; if (value != null && !fullValues.contains(value)) { - logger.err('Invalid value $value provided. Must be in: ${options.values}'); + logger.info('Invalid value $value provided. Must be in: ${options.values}'); value = null; } @@ -121,7 +121,7 @@ List getMultiOption( if (startingOptions.isNotEmpty) { return startingOptions; } else { - logger.err('Missing parameter $name is required.'); + logger.info('Missing parameter $name is required.'); exit(ExitCode.usage.code); } } else { @@ -130,7 +130,7 @@ List getMultiOption( } if (value.any((e) => !options.contains(e))) { - logger.err('Invalid value $value provided. Must be in: $options'); + logger.info('Invalid value $value provided. Must be in: $options'); value = []; } if (desc != null) { From 667b18c20a3ac62f2d996b4013071a7d12ea2cda Mon Sep 17 00:00:00 2001 From: Elijah Luckey Date: Fri, 25 Apr 2025 14:16:33 -0400 Subject: [PATCH 05/20] setup context --- bin/ignite.dart | 13 +++- lib/commands/create_command.dart | 90 +++++++++++++++++++-- lib/commands/ignite_command.dart | 20 +++++ lib/flame_version_manager.dart | 6 +- lib/main.dart | 129 ++++++++++++++----------------- lib/utils.dart | 37 ++++----- pubspec.lock | 20 +---- pubspec.yaml | 4 - 8 files changed, 193 insertions(+), 126 deletions(-) create mode 100644 lib/commands/ignite_command.dart diff --git a/bin/ignite.dart b/bin/ignite.dart index 4282bab..cec7537 100644 --- a/bin/ignite.dart +++ b/bin/ignite.dart @@ -1,5 +1,14 @@ +import 'dart:io'; + +import 'package:ignite_cli/flame_version_manager.dart'; import 'package:ignite_cli/main.dart'; +import 'package:mason_logger/mason_logger.dart'; + +Future main(List args) async { + final runner = IgniteCommandRunner( + logger: Logger(), + flameVersionManager: await FlameVersionManager.init(), + ); -void main(List args) { - mainCommand(args); + exit((await runner.run(args)).code); } diff --git a/lib/commands/create_command.dart b/lib/commands/create_command.dart index 6f34a3f..b034a09 100644 --- a/lib/commands/create_command.dart +++ b/lib/commands/create_command.dart @@ -1,20 +1,98 @@ import 'dart:io'; import 'package:args/args.dart'; +import 'package:args/command_runner.dart'; import 'package:dartlin/dartlin.dart'; +import 'package:ignite_cli/commands/ignite_command.dart'; import 'package:ignite_cli/flame_version_manager.dart'; import 'package:ignite_cli/templates/template.dart'; import 'package:ignite_cli/utils.dart'; -import 'package:io/ansi.dart' as ansi; import 'package:mason/mason.dart'; import 'package:process_run/process_run.dart'; -Future createCommand(ArgResults command, Logger logger) async { +class CreateCommand extends Command with ContextProvider { + CreateCommand() { + final packages = context.flameVersionManager.versions; + final flameVersions = packages[Package.flame]!; + + argParser + ..addFlag( + 'interactive', + abbr: 'i', + help: 'Whether to run in interactive mode or not.', + defaultsTo: true, + ) + ..addOption( + 'name', + help: 'The name of your game (valid dart identifier).', + ) + ..addOption( + 'org', + help: 'The org name, in reverse domain notation ' + '(package name/bundle identifier).', + ) + ..addFlag( + 'create-folder', + abbr: 'f', + help: 'If you want to create a new folder on the current location with ' + "the project name or if you are already on the new project's folder.", + ) + ..addOption( + 'template', + help: 'What Flame template you would like to use for your new project', + allowed: ['simple', 'example'], + ) + ..addOption( + 'flame-version', + help: 'What Flame version you would like to use.', + allowed: [ + ...flameVersions.versions.take(5), + '...', + flameVersions.versions.last, + ], + ) + ..addMultiOption( + 'extra-packages', + help: 'Which packages to use', + allowed: packages.keys.map((e) => e.name).toList(), + ); + } + + @override + String get description => 'Create a new Flame project'; + + @override + String get name => 'create'; + + @override + Future run() async { + final argResults = this.argResults; + context.logger.write('$argResults'); + if (argResults == null) { + return ExitCode.usage; + } + + await createCommand( + context.logger, + argResults, + context.flameVersionManager, + ); + + return ExitCode.success; + } +} + +Future createCommand( + Logger logger, + ArgResults command, + FlameVersionManager flameVersionManager, +) async { final interactive = command['interactive'] != 'false'; if (interactive) { - stdout.write('\nWelcome to ${ansi.red.wrap('Ignite CLI')}! 🔥\n'); - stdout.write("Let's create a new project!\n\n"); + logger + ..info('\nWelcome to ${red.wrap('Ignite CLI')}! 🔥') + ..info("Let's create a new project!\n\n"); } final name = getString( @@ -50,7 +128,7 @@ Future createCommand(ArgResults command, Logger logger) async { }, ); - final versions = FlameVersionManager.singleton.versions; + final versions = flameVersionManager.versions; final flameVersions = versions[Package.flame]!; final flameVersion = getOption( logger: logger, @@ -63,7 +141,7 @@ Future createCommand(ArgResults command, Logger logger) async { fullOptions: flameVersions.versions.associateWith((e) => e), ); - final extraPackageOptions = FlameVersionManager.singleton.versions.keys + final extraPackageOptions = flameVersionManager.versions.keys .where((key) => !Package.includedByDefault.contains(key)) .map((key) => key.name) .toList(); diff --git a/lib/commands/ignite_command.dart b/lib/commands/ignite_command.dart new file mode 100644 index 0000000..801c838 --- /dev/null +++ b/lib/commands/ignite_command.dart @@ -0,0 +1,20 @@ +import 'package:args/command_runner.dart'; +import 'package:ignite_cli/flame_version_manager.dart'; +import 'package:ignite_cli/main.dart'; +import 'package:mason_logger/mason_logger.dart'; + +mixin ContextProvider on Command { + IgniteContext get context { + if (runner case IgniteCommandRunner(:final IgniteContext context)) { + return context; + } + throw Exception('The current runner must be a [IgniteCommandRunner]'); + } +} + +class IgniteContext { + final Logger logger; + final FlameVersionManager flameVersionManager; + + IgniteContext({required this.logger, required this.flameVersionManager}); +} diff --git a/lib/flame_version_manager.dart b/lib/flame_version_manager.dart index f1d3783..e465c17 100644 --- a/lib/flame_version_manager.dart +++ b/lib/flame_version_manager.dart @@ -3,11 +3,7 @@ import 'dart:convert'; import 'package:http/http.dart' as http; class FlameVersionManager { - static late FlameVersionManager singleton; - - static Future init() async { - singleton = await FlameVersionManager.fetch(); - } + static Future init() => FlameVersionManager.fetch(); final Map versions; diff --git a/lib/main.dart b/lib/main.dart index 53294a1..ca4e825 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,82 +1,73 @@ -import 'package:args/args.dart'; -import 'package:completion/completion.dart' as completion; +import 'dart:async'; +import 'dart:io'; + +import 'package:args/command_runner.dart'; import 'package:ignite_cli/commands/create_command.dart'; +import 'package:ignite_cli/commands/ignite_command.dart'; import 'package:ignite_cli/commands/version_command.dart'; import 'package:ignite_cli/flame_version_manager.dart'; import 'package:mason_logger/mason_logger.dart'; -Future mainCommand(List args) async { - await FlameVersionManager.init(); - - final parser = ArgParser(); - final logger = Logger(); +class IgniteCommandRunner extends CommandRunner { + late final IgniteContext context; - parser.addFlag('help', abbr: 'h', help: 'Displays this message.'); - parser.addFlag('version', abbr: 'v', help: 'Shows relevant version info.'); + IgniteCommandRunner({ + required Logger logger, + required FlameVersionManager flameVersionManager, + }) : super(_name, _description) { + context = IgniteContext( + logger: logger, + flameVersionManager: flameVersionManager, + ); - final create = parser.addCommand('create'); - create.addOption( - 'interactive', - abbr: 'i', - help: 'Whether to run in interactive mode or not.', - allowed: ['true', 'false'], - defaultsTo: 'true', - ); - create.addOption( - 'name', - help: 'The name of your game (valid dart identifier).', - ); - create.addOption( - 'org', - help: 'The org name, in reverse domain notation ' - '(package name/bundle identifier).', - ); - create.addOption( - 'create-folder', - abbr: 'f', - help: 'If you want to create a new folder on the current location with ' - "the project name or if you are already on the new project's folder.", - allowed: ['true', 'false'], - ); - create.addOption( - 'template', - help: 'What Flame template you would like to use for your new project', - allowed: ['simple', 'example'], - ); + addCommand(CreateCommand()); - final packages = FlameVersionManager.singleton.versions; - final flameVersions = packages[Package.flame]!; - create.addOption( - 'flame-version', - help: 'What Flame version you would like to use.', - allowed: flameVersions.versions, - ); - create.addMultiOption( - 'extra-packages', - help: 'Which packages to use', - allowed: packages.keys.map((e) => e.name).toList(), - ); + argParser.addFlag( + 'version', + help: 'Print the current version.', + negatable: false, + ); + } - final results = completion.tryArgsCompletion(args, parser); - if (results['help'] as bool) { - logger.info(parser.usage); - logger.info(''); - logger.info('List of available commands:'); - logger.info(''); - logger.info('create:'); - logger.info(' ${create.usage}'); - return; - } else if (results['version'] as bool) { - await versionCommand(logger); - return; + @override + void addCommand(covariant ContextProvider command) { + // forces commands to use the [ContextProvider] mixin on any new commands + super.addCommand(command); } - final command = results.command; - if (command?.name == 'create') { - await createCommand(command!, logger); - } else { - logger.info( - 'Invalid command. Please select an option, use --help for help.', - ); + @override + Future run(Iterable args) async { + try { + final parsedArgs = parse(args); + + if (parsedArgs['version'] == true) { + await versionCommand(context.logger); + return ExitCode.success; + } + + return await runCommand(parsedArgs) ?? ExitCode.success; + } on FormatException catch (exception) { + context.logger + ..err(exception.message) + ..info('') + ..info(usage); + return ExitCode.usage; + } on UsageException catch (exception) { + context.logger + ..err(exception.message) + ..info('') + ..info(exception.usage); + return ExitCode.usage; + } on ProcessException catch (error) { + context.logger.err(error.message); + return ExitCode.unavailable; + } on Exception catch (error) { + context.logger.err('$error'); + return ExitCode.software; + } } + + static const _name = 'ignite'; + static const _description = 'Ignite your projects with flame; ' + 'a CLI scaffolding tool to create and setup your Flame projects.'; } diff --git a/lib/utils.dart b/lib/utils.dart index 22a0c77..7915156 100644 --- a/lib/utils.dart +++ b/lib/utils.dart @@ -1,9 +1,7 @@ import 'dart:io'; import 'package:args/args.dart'; -import 'package:io/ansi.dart' as ansi; -import 'package:io/io.dart'; -import 'package:mason_logger/mason_logger.dart' as logger; +import 'package:mason_logger/mason_logger.dart'; import 'package:path/path.dart' as p; String getBundledFile(String name) { @@ -16,36 +14,31 @@ String getString( String name, String message, { required bool isInteractive, - required logger.Logger logger, + required Logger logger, String? desc, String? Function(String)? validate, }) { - var value = results[name] as String?; + final value = results[name] as String?; if (!isInteractive) { if (value == null || value.isEmpty) { - logger.info('Missing parameter $name is required.'); + logger.err('Missing parameter $name is required.'); exit(ExitCode.usage.code); } final error = validate?.call(value); if (error != null) { - logger.info('Invalid value $value provided: $error'); + logger.err('Invalid value $value provided: $error'); exit(ExitCode.usage.code); } } - while (value == null || value.isEmpty) { - if (desc != null) { - logger.info(ansi.darkGray.wrap('\n$desc\u{1B}[1A\r')); - } - value = logger.prompt(message); + desc = darkGray.wrap(desc); + var msg = message; - // TODO(elijah): validation callback - - if (desc != null) { - logger.info('\r\u{1B}[K'); - } + if (desc != null) { + msg = '$msg\n$desc'; } - return value; + + return logger.prompt(msg); } String getOption( @@ -54,7 +47,7 @@ String getOption( String message, Map options, { required bool isInteractive, - required logger.Logger logger, + required Logger logger, String? desc, String? defaultsTo, Map fullOptions = const {}, @@ -79,7 +72,7 @@ String getOption( while (value == null) { if (desc != null) { - logger.info(ansi.darkGray.wrap('\n$desc\u{1B}[1A\r')); + logger.info(darkGray.wrap('\n$desc\u{1B}[1A\r')); } final option = logger.chooseOne(message, choices: options.keys.toList()); @@ -111,7 +104,7 @@ List getMultiOption( List options, { required bool isInteractive, required bool isRequired, - required logger.Logger logger, + required Logger logger, List startingOptions = const [], String? desc, }) { @@ -134,7 +127,7 @@ List getMultiOption( value = []; } if (desc != null) { - logger.info(ansi.darkGray.wrap('\n$desc\u{1B}[1A\r')); + logger.info(darkGray.wrap('\n$desc\u{1B}[1A\r')); } value = logger.chooseAny(message, choices: options); diff --git a/pubspec.lock b/pubspec.lock index a3556dc..f7d9000 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -49,14 +49,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" - charcode: - dependency: "direct main" - description: - name: charcode - sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a - url: "https://pub.dev" - source: hosted - version: "1.4.0" checked_yaml: dependency: transitive description: @@ -97,14 +89,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" - completion: - dependency: "direct main" - description: - name: completion - sha256: f11b7a628e6c42b9edc9b0bc3aa490e2d930397546d2f794e8e1325909d11c60 - url: "https://pub.dev" - source: hosted - version: "1.0.1" convert: dependency: transitive description: @@ -234,7 +218,7 @@ packages: source: hosted version: "4.1.2" io: - dependency: "direct main" + dependency: transitive description: name: io sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b @@ -610,7 +594,7 @@ packages: source: hosted version: "5.12.0" yaml: - dependency: "direct main" + dependency: transitive description: name: yaml sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce diff --git a/pubspec.yaml b/pubspec.yaml index eff4693..ab91f24 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,16 +12,12 @@ environment: dependencies: args: ^2.7.0 - charcode: ^1.4.0 - completion: ^1.0.1 dartlin: ^0.6.3 http: ^1.3.0 - io: ^1.0.5 mason: ^0.1.1 mason_logger: ^0.3.3 path: ^1.9.1 process_run: ^1.2.4 - yaml: ^3.1.3 dev_dependencies: dartdoc: ^8.0.2 From 79ad1f88472bb74e3d507d789aded1915f198f48 Mon Sep 17 00:00:00 2001 From: Elijah Luckey Date: Fri, 25 Apr 2025 14:30:02 -0400 Subject: [PATCH 06/20] simplifying --- bin/ignite.dart | 2 +- lib/commands/create_command.dart | 10 +++++++++- lib/commands/ignite_command.dart | 13 +++++++++++-- lib/flame_version_manager.dart | 2 -- lib/main.dart | 5 +++-- 5 files changed, 24 insertions(+), 8 deletions(-) diff --git a/bin/ignite.dart b/bin/ignite.dart index cec7537..ba8bb76 100644 --- a/bin/ignite.dart +++ b/bin/ignite.dart @@ -7,7 +7,7 @@ import 'package:mason_logger/mason_logger.dart'; Future main(List args) async { final runner = IgniteCommandRunner( logger: Logger(), - flameVersionManager: await FlameVersionManager.init(), + flameVersionManager: await FlameVersionManager.fetch(), ); exit((await runner.run(args)).code); diff --git a/lib/commands/create_command.dart b/lib/commands/create_command.dart index b034a09..04a04ad 100644 --- a/lib/commands/create_command.dart +++ b/lib/commands/create_command.dart @@ -10,7 +10,15 @@ import 'package:ignite_cli/utils.dart'; import 'package:mason/mason.dart'; import 'package:process_run/process_run.dart'; -class CreateCommand extends Command with ContextProvider { +class SubCommand extends Command { + @override + String get description => throw UnimplementedError(); + + @override + String get name => throw UnimplementedError(); +} + +class CreateCommand extends Command with IgniteCommand { CreateCommand() { final packages = context.flameVersionManager.versions; final flameVersions = packages[Package.flame]!; diff --git a/lib/commands/ignite_command.dart b/lib/commands/ignite_command.dart index 801c838..fc76270 100644 --- a/lib/commands/ignite_command.dart +++ b/lib/commands/ignite_command.dart @@ -1,14 +1,23 @@ import 'package:args/command_runner.dart'; import 'package:ignite_cli/flame_version_manager.dart'; import 'package:ignite_cli/main.dart'; +import 'package:mason/mason.dart'; import 'package:mason_logger/mason_logger.dart'; -mixin ContextProvider on Command { +mixin IgniteCommand on Command { + /// Forces sun-commands added to [IgniteCommand] command + /// to use the [IgniteCommand] mixin on those sun-commands. + @override + void addSubcommand(covariant IgniteCommand command) { + super.addSubcommand(command); + } + IgniteContext get context { if (runner case IgniteCommandRunner(:final IgniteContext context)) { return context; } - throw Exception('The current runner must be a [IgniteCommandRunner]'); + // definitely in some invalid state + throw StateError('The current runner must be a [IgniteCommandRunner]'); } } diff --git a/lib/flame_version_manager.dart b/lib/flame_version_manager.dart index e465c17..4dcc7c4 100644 --- a/lib/flame_version_manager.dart +++ b/lib/flame_version_manager.dart @@ -3,8 +3,6 @@ import 'dart:convert'; import 'package:http/http.dart' as http; class FlameVersionManager { - static Future init() => FlameVersionManager.fetch(); - final Map versions; const FlameVersionManager._(this.versions); diff --git a/lib/main.dart b/lib/main.dart index ca4e825..67efc38 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -29,9 +29,10 @@ class IgniteCommandRunner extends CommandRunner { ); } + /// Forces commands added to [IgniteCommandRunner] + /// to use the [IgniteCommand] mixin on those commands. @override - void addCommand(covariant ContextProvider command) { - // forces commands to use the [ContextProvider] mixin on any new commands + void addCommand(covariant IgniteCommand command) { super.addCommand(command); } From 757ad4d7918114dfff3c2b4e0961ebec4e6bfadb Mon Sep 17 00:00:00 2001 From: Elijah Luckey Date: Fri, 25 Apr 2025 14:55:47 -0400 Subject: [PATCH 07/20] working --- lib/commands/create_command.dart | 12 ++--------- lib/commands/ignite_command.dart | 36 +++++++++++++++++++------------- lib/main.dart | 7 ++++--- 3 files changed, 28 insertions(+), 27 deletions(-) diff --git a/lib/commands/create_command.dart b/lib/commands/create_command.dart index 04a04ad..31dd941 100644 --- a/lib/commands/create_command.dart +++ b/lib/commands/create_command.dart @@ -1,7 +1,6 @@ import 'dart:io'; import 'package:args/args.dart'; -import 'package:args/command_runner.dart'; import 'package:dartlin/dartlin.dart'; import 'package:ignite_cli/commands/ignite_command.dart'; import 'package:ignite_cli/flame_version_manager.dart'; @@ -10,16 +9,9 @@ import 'package:ignite_cli/utils.dart'; import 'package:mason/mason.dart'; import 'package:process_run/process_run.dart'; -class SubCommand extends Command { +class CreateCommand extends IgniteCommand { @override - String get description => throw UnimplementedError(); - - @override - String get name => throw UnimplementedError(); -} - -class CreateCommand extends Command with IgniteCommand { - CreateCommand() { + void setup(ArgParser argParser) { final packages = context.flameVersionManager.versions; final flameVersions = packages[Package.flame]!; diff --git a/lib/commands/ignite_command.dart b/lib/commands/ignite_command.dart index fc76270..5dcadf8 100644 --- a/lib/commands/ignite_command.dart +++ b/lib/commands/ignite_command.dart @@ -1,29 +1,37 @@ +import 'dart:async'; + +import 'package:args/args.dart'; import 'package:args/command_runner.dart'; import 'package:ignite_cli/flame_version_manager.dart'; -import 'package:ignite_cli/main.dart'; import 'package:mason/mason.dart'; import 'package:mason_logger/mason_logger.dart'; -mixin IgniteCommand on Command { - /// Forces sun-commands added to [IgniteCommand] command - /// to use the [IgniteCommand] mixin on those sun-commands. +abstract class IgniteCommand extends Command { + late IgniteContext context; + + void setup(ArgParser argParser); + @override - void addSubcommand(covariant IgniteCommand command) { + void addSubcommand(Command command) { + if (command is IgniteCommand) { + command + ..context = context + ..setup(command.argParser); + } + super.addSubcommand(command); } - IgniteContext get context { - if (runner case IgniteCommandRunner(:final IgniteContext context)) { - return context; - } - // definitely in some invalid state - throw StateError('The current runner must be a [IgniteCommandRunner]'); - } + @override + Future run(); } class IgniteContext { + const IgniteContext({ + required this.logger, + required this.flameVersionManager, + }); + final Logger logger; final FlameVersionManager flameVersionManager; - - IgniteContext({required this.logger, required this.flameVersionManager}); } diff --git a/lib/main.dart b/lib/main.dart index 67efc38..91fa1a4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -29,10 +29,11 @@ class IgniteCommandRunner extends CommandRunner { ); } - /// Forces commands added to [IgniteCommandRunner] - /// to use the [IgniteCommand] mixin on those commands. @override - void addCommand(covariant IgniteCommand command) { + void addCommand(Command command) { + if (command is IgniteCommand) { + command.context = context; + } super.addCommand(command); } From e347f5e5b1d1b74d01db05752c6746f6a02870c3 Mon Sep 17 00:00:00 2001 From: Elijah Luckey Date: Fri, 25 Apr 2025 16:54:44 -0400 Subject: [PATCH 08/20] fix up thiings? --- analysis_options.yaml | 4 +- bin/ignite.dart | 2 +- lib/commands/create_command.dart | 114 +++++++++--------- lib/commands/ignite_command.dart | 16 +-- lib/commands/version_command.dart | 32 ++++- ...{main.dart => ignite_commmand_runner.dart} | 23 ++-- lib/utils.dart | 37 ++++-- 7 files changed, 125 insertions(+), 103 deletions(-) rename lib/{main.dart => ignite_commmand_runner.dart} (85%) diff --git a/analysis_options.yaml b/analysis_options.yaml index ae7ff1a..c6accd6 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -2,8 +2,8 @@ include: package:flame_lint/analysis_options.yaml linter: rules: - avoid_print: false # since it is a CLI application + avoid_print: true # should always use a Logger analyzer: exclude: - - bricks/ \ No newline at end of file + - bricks/ diff --git a/bin/ignite.dart b/bin/ignite.dart index ba8bb76..587dad3 100644 --- a/bin/ignite.dart +++ b/bin/ignite.dart @@ -1,7 +1,7 @@ import 'dart:io'; import 'package:ignite_cli/flame_version_manager.dart'; -import 'package:ignite_cli/main.dart'; +import 'package:ignite_cli/ignite_commmand_runner.dart'; import 'package:mason_logger/mason_logger.dart'; Future main(List args) async { diff --git a/lib/commands/create_command.dart b/lib/commands/create_command.dart index 31dd941..f3c1d9c 100644 --- a/lib/commands/create_command.dart +++ b/lib/commands/create_command.dart @@ -10,52 +10,50 @@ import 'package:mason/mason.dart'; import 'package:process_run/process_run.dart'; class CreateCommand extends IgniteCommand { - @override - void setup(ArgParser argParser) { + CreateCommand(super.context) { final packages = context.flameVersionManager.versions; final flameVersions = packages[Package.flame]!; - argParser - ..addFlag( - 'interactive', - abbr: 'i', - help: 'Whether to run in interactive mode or not.', - defaultsTo: true, - ) - ..addOption( - 'name', - help: 'The name of your game (valid dart identifier).', - ) - ..addOption( - 'org', - help: 'The org name, in reverse domain notation ' - '(package name/bundle identifier).', - ) - ..addFlag( - 'create-folder', - abbr: 'f', - help: 'If you want to create a new folder on the current location with ' - "the project name or if you are already on the new project's folder.", - ) - ..addOption( - 'template', - help: 'What Flame template you would like to use for your new project', - allowed: ['simple', 'example'], - ) - ..addOption( - 'flame-version', - help: 'What Flame version you would like to use.', - allowed: [ - ...flameVersions.versions.take(5), - '...', - flameVersions.versions.last, - ], - ) - ..addMultiOption( - 'extra-packages', - help: 'Which packages to use', - allowed: packages.keys.map((e) => e.name).toList(), - ); + argParser.addFlag( + 'interactive', + abbr: 'i', + help: 'Whether to run in interactive mode or not.', + defaultsTo: true, + ); + argParser.addOption( + 'name', + help: 'The name of your game (valid dart identifier).', + ); + argParser.addOption( + 'org', + help: 'The org name, in reverse domain notation ' + '(package name/bundle identifier).', + ); + argParser.addFlag( + 'create-folder', + abbr: 'f', + help: 'If you want to create a new folder on the current location with ' + "the project name or if you are already on the new project's folder.", + ); + argParser.addOption( + 'template', + help: 'What Flame template you would like to use for your new project', + allowed: ['simple', 'example'], + ); + argParser.addOption( + 'flame-version', + help: 'What Flame version you would like to use.', + allowed: [ + ...flameVersions.versions.take(5), + '...', + flameVersions.versions.last, + ], + ); + argParser.addMultiOption( + 'extra-packages', + help: 'Which packages to use', + allowed: packages.keys.map((e) => e.name).toList(), + ); } @override @@ -67,7 +65,6 @@ class CreateCommand extends IgniteCommand { @override Future run() async { final argResults = this.argResults; - context.logger.write('$argResults'); if (argResults == null) { return ExitCode.usage; } @@ -92,7 +89,7 @@ Future createCommand( if (interactive) { logger ..info('\nWelcome to ${red.wrap('Ignite CLI')}! 🔥') - ..info("Let's create a new project!\n\n"); + ..info("Let's create a new project!\n"); } final name = getString( @@ -100,7 +97,7 @@ Future createCommand( logger: logger, command, 'name', - 'Choose a name for your project: ', + 'Choose a name for your project', desc: 'Note: this must be a valid dart identifier (no dashes). ' 'For example: my_game', validate: (it) => switch (it) { @@ -165,7 +162,7 @@ Future createCommand( final devDependencies = packages.where((e) => e.isDevDependency); final currentDir = Directory.current.path; - print('\nYour current directory is: $currentDir'); + logger.info('\nYour current directory is: $currentDir'); final createFolder = getOption( logger: logger, @@ -181,7 +178,7 @@ Future createCommand( ) == 'true'; - print('\n'); + logger.info('\n'); final template = getOption( logger: logger, isInteractive: interactive, @@ -196,26 +193,26 @@ Future createCommand( if (createFolder) { await runExecutableArguments('mkdir', [actualDir]); } - print('\nRunning flutter create on $actualDir ...'); + logger.info('\nRunning flutter create on $actualDir ...'); await runExecutableArguments( 'flutter', 'create --org $org --project-name $name .'.split(' '), workingDirectory: actualDir, - verbose: true, + verbose: logger.level == Level.verbose, ); + await runExecutableArguments( 'rm', '-rf lib test'.split(' '), workingDirectory: actualDir, - verbose: true, + verbose: logger.level == Level.verbose, ); final bundle = Template.byKey(template).bundle; final generator = await MasonGenerator.fromBundle(bundle); - final target = DirectoryGeneratorTarget( - Directory(actualDir), - ); + final target = DirectoryGeneratorTarget(Directory(actualDir)); + final variables = { 'name': name, 'description': 'A simple Flame game.', @@ -237,16 +234,17 @@ Future createCommand( 'rm', '-rf test'.split(' '), workingDirectory: actualDir, - verbose: true, + verbose: logger.level == Level.verbose, ); } + await runExecutableArguments( 'flutter', 'pub get'.split(' '), workingDirectory: actualDir, - verbose: true, + verbose: logger.level == Level.verbose, ); - print('Updated ${files.length} files on top of flutter create.\n'); - print('Your new Flame project was successfully created!'); + // logger.info('Updated ${files.length} files on top of flutter create.\n'); + logger.info('Your new Flame project was successfully created!'); } diff --git a/lib/commands/ignite_command.dart b/lib/commands/ignite_command.dart index 5dcadf8..0bcbae7 100644 --- a/lib/commands/ignite_command.dart +++ b/lib/commands/ignite_command.dart @@ -1,26 +1,14 @@ import 'dart:async'; -import 'package:args/args.dart'; import 'package:args/command_runner.dart'; import 'package:ignite_cli/flame_version_manager.dart'; import 'package:mason/mason.dart'; import 'package:mason_logger/mason_logger.dart'; abstract class IgniteCommand extends Command { - late IgniteContext context; + final IgniteContext context; - void setup(ArgParser argParser); - - @override - void addSubcommand(Command command) { - if (command is IgniteCommand) { - command - ..context = context - ..setup(command.argParser); - } - - super.addSubcommand(command); - } + IgniteCommand(this.context); @override Future run(); diff --git a/lib/commands/version_command.dart b/lib/commands/version_command.dart index 825a547..2a80a5e 100644 --- a/lib/commands/version_command.dart +++ b/lib/commands/version_command.dart @@ -3,10 +3,30 @@ import 'package:mason/mason.dart'; import 'package:process_run/process_run.dart'; Future versionCommand(Logger logger) async { - logger.detail(r'$ ignite --version:'); - logger.detail(igniteVersion); - logger.detail(''); - await runExecutableArguments('dart', ['--version'], verbose: true); - logger.detail(''); - await runExecutableArguments('flutter', ['--version'], verbose: true); + logger.info('ignite --version: $igniteVersion\n'); + + final [dartProcess, flutterProcess] = await [ + runExecutableArguments( + 'dart', + ['--version'], + commandVerbose: false, + verbose: false, + ), + runExecutableArguments( + 'flutter', + ['--version'], + commandVerbose: false, + verbose: false, + ), + ].wait; + + logger.info('${dartProcess.stdout}'); + logger.info('${flutterProcess.stdout}'); + + if (dartProcess.stderr case final String err when err.isNotEmpty) { + logger.err(err); + } + if (flutterProcess.stderr case final String err when err.isNotEmpty) { + logger.err(err); + } } diff --git a/lib/main.dart b/lib/ignite_commmand_runner.dart similarity index 85% rename from lib/main.dart rename to lib/ignite_commmand_runner.dart index 91fa1a4..17ced79 100644 --- a/lib/main.dart +++ b/lib/ignite_commmand_runner.dart @@ -9,18 +9,22 @@ import 'package:ignite_cli/flame_version_manager.dart'; import 'package:mason_logger/mason_logger.dart'; class IgniteCommandRunner extends CommandRunner { - late final IgniteContext context; + late IgniteContext context; IgniteCommandRunner({ required Logger logger, required FlameVersionManager flameVersionManager, - }) : super(_name, _description) { + }) : super('ignite', _description) { context = IgniteContext( logger: logger, flameVersionManager: flameVersionManager, ); - addCommand(CreateCommand()); + addCommand(CreateCommand(context)); + + argParser.addFlag( + 'verbose', + ); argParser.addFlag( 'version', @@ -29,19 +33,15 @@ class IgniteCommandRunner extends CommandRunner { ); } - @override - void addCommand(Command command) { - if (command is IgniteCommand) { - command.context = context; - } - super.addCommand(command); - } - @override Future run(Iterable args) async { try { final parsedArgs = parse(args); + if (parsedArgs['verbose'] == true) { + context.logger.level = Level.verbose; + } + if (parsedArgs['version'] == true) { await versionCommand(context.logger); return ExitCode.success; @@ -69,7 +69,6 @@ class IgniteCommandRunner extends CommandRunner { } } - static const _name = 'ignite'; static const _description = 'Ignite your projects with flame; ' 'a CLI scaffolding tool to create and setup your Flame projects.'; } diff --git a/lib/utils.dart b/lib/utils.dart index 7915156..542598c 100644 --- a/lib/utils.dart +++ b/lib/utils.dart @@ -1,6 +1,6 @@ import 'dart:io'; -import 'package:args/args.dart'; +import 'package:args/args.dart' show ArgResults; import 'package:mason_logger/mason_logger.dart'; import 'package:path/path.dart' as p; @@ -18,7 +18,7 @@ String getString( String? desc, String? Function(String)? validate, }) { - final value = results[name] as String?; + var value = results[name] as String?; if (!isInteractive) { if (value == null || value.isEmpty) { logger.err('Missing parameter $name is required.'); @@ -31,14 +31,27 @@ String getString( } } - desc = darkGray.wrap(desc); - var msg = message; + String? validation; - if (desc != null) { - msg = '$msg\n$desc'; - } + do { + desc = darkGray.wrap(desc); + var msg = message; + + if (desc != null) { + msg = '$msg ($desc)'; + } + + if (validation != null) { + validation = logger.theme.warn(validation); + // omit the description on errors? + msg = '$message [$validation]'; + } + + value = logger.prompt(msg); + validation = validate?.call(value); + } while (validation != null); - return logger.prompt(msg); + return value; } String getOption( @@ -52,7 +65,7 @@ String getOption( String? defaultsTo, Map fullOptions = const {}, }) { - var value = results[name] as String?; + var value = results[name]; if (!isInteractive) { if (value == null) { @@ -82,7 +95,11 @@ String getOption( logger.info('\r\u{1B}[K'); } } - return value; + return switch (value) { + final String value => value, + final bool value => 'true', + _ => '', + }; } List _unwrap(dynamic value) { From ef061f2729421d4d04cd63f728a80991ceae4ba6 Mon Sep 17 00:00:00 2001 From: Elijah Luckey Date: Mon, 28 Apr 2025 14:33:27 -0400 Subject: [PATCH 09/20] ninx rules --- analysis_options.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index c6accd6..719c7bf 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,9 +1,5 @@ include: package:flame_lint/analysis_options.yaml -linter: - rules: - avoid_print: true # should always use a Logger - analyzer: exclude: - bricks/ From 7dbf9b1d91f35eba4974f4450f485d4670f5e735 Mon Sep 17 00:00:00 2001 From: Elijah Luckey Date: Mon, 28 Apr 2025 14:36:29 -0400 Subject: [PATCH 10/20] grammar --- lib/ignite_commmand_runner.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ignite_commmand_runner.dart b/lib/ignite_commmand_runner.dart index 17ced79..8fcaf23 100644 --- a/lib/ignite_commmand_runner.dart +++ b/lib/ignite_commmand_runner.dart @@ -69,6 +69,6 @@ class IgniteCommandRunner extends CommandRunner { } } - static const _description = 'Ignite your projects with flame; ' + static const _description = 'Ignite your projects with Flame; ' 'a CLI scaffolding tool to create and setup your Flame projects.'; } From ea21a7f257a315b9265a3d668feeb66a2e1a9bf6 Mon Sep 17 00:00:00 2001 From: Elijah Luckey Date: Mon, 28 Apr 2025 14:39:23 -0400 Subject: [PATCH 11/20] tidy up --- lib/commands/create_command.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/commands/create_command.dart b/lib/commands/create_command.dart index f3c1d9c..84a00e5 100644 --- a/lib/commands/create_command.dart +++ b/lib/commands/create_command.dart @@ -114,7 +114,7 @@ Future createCommand( isInteractive: interactive, command, 'org', - 'Choose an org for your project: ', + 'Choose an org for your project:', desc: 'Note: this is a dot separated list of "packages", ' 'normally in reverse domain notation. ' 'For example: org.flame_engine.games', @@ -245,6 +245,6 @@ Future createCommand( verbose: logger.level == Level.verbose, ); - // logger.info('Updated ${files.length} files on top of flutter create.\n'); + logger.info('Updated ${files.length} files on top of flutter create.\n'); logger.info('Your new Flame project was successfully created!'); } From b12f3dbe6c3007f82a9d5b87e0a1aecb6b9877a0 Mon Sep 17 00:00:00 2001 From: Elijah Luckey Date: Mon, 28 Apr 2025 14:40:35 -0400 Subject: [PATCH 12/20] coment --- lib/utils.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/utils.dart b/lib/utils.dart index 542598c..9d1cc93 100644 --- a/lib/utils.dart +++ b/lib/utils.dart @@ -43,8 +43,7 @@ String getString( if (validation != null) { validation = logger.theme.warn(validation); - // omit the description on errors? - msg = '$message [$validation]'; + msg = '$message [$validation]'; // omit the description on errors } value = logger.prompt(msg); From 541280b1521060119607c85cb7bf6dead6133bdf Mon Sep 17 00:00:00 2001 From: Elijah Luckey Date: Mon, 28 Apr 2025 15:27:18 -0400 Subject: [PATCH 13/20] remove unnessacry deps --- pubspec.lock | 16 ---------------- pubspec.yaml | 1 - 2 files changed, 17 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index f7d9000..5126986 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -385,14 +385,6 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.3" - process_run: - dependency: "direct main" - description: - name: process_run - sha256: "6ec839cdd3e6de4685318e7686cd4abb523c3d3a55af0e8d32a12ae19bc66622" - url: "https://pub.dev" - source: hosted - version: "1.2.4" pub_semver: dependency: transitive description: @@ -489,14 +481,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" - synchronized: - dependency: transitive - description: - name: synchronized - sha256: "0669c70faae6270521ee4f05bffd2919892d42d1276e6c495be80174b6bc0ef6" - url: "https://pub.dev" - source: hosted - version: "3.3.1" term_glyph: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index ab91f24..1e73bef 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,7 +17,6 @@ dependencies: mason: ^0.1.1 mason_logger: ^0.3.3 path: ^1.9.1 - process_run: ^1.2.4 dev_dependencies: dartdoc: ^8.0.2 From 2d1dbd6d5c02e5df8e1e2bde1388712f8b63491b Mon Sep 17 00:00:00 2001 From: Elijah Luckey Date: Mon, 28 Apr 2025 22:25:29 -0400 Subject: [PATCH 14/20] tweaks with testing --- bin/ignite.dart | 27 ++- lib/commands/create_command.dart | 206 ++++++++++-------- lib/commands/ignite_command.dart | 15 +- lib/commands/version_command.dart | 49 ++--- ...runner.dart => ignite_command_runner.dart} | 37 ++-- lib/utils.dart | 23 +- pubspec.lock | 8 + pubspec.yaml | 1 + test/ignite_command_runner_test.dart | 91 ++++++++ 9 files changed, 295 insertions(+), 162 deletions(-) rename lib/{ignite_commmand_runner.dart => ignite_command_runner.dart} (63%) create mode 100644 test/ignite_command_runner_test.dart diff --git a/bin/ignite.dart b/bin/ignite.dart index 587dad3..9474a31 100644 --- a/bin/ignite.dart +++ b/bin/ignite.dart @@ -1,14 +1,33 @@ import 'dart:io'; +import 'package:ignite_cli/commands/ignite_command.dart'; import 'package:ignite_cli/flame_version_manager.dart'; -import 'package:ignite_cli/ignite_commmand_runner.dart'; +import 'package:ignite_cli/ignite_command_runner.dart'; import 'package:mason_logger/mason_logger.dart'; Future main(List args) async { final runner = IgniteCommandRunner( - logger: Logger(), - flameVersionManager: await FlameVersionManager.fetch(), + IgniteContext( + logger: Logger(), + flameVersionManager: await FlameVersionManager.fetch(), + process: _IgniteProcess(), + ), ); - exit((await runner.run(args)).code); + exit(await runner.run(args)); +} + +class _IgniteProcess extends IgniteProcess { + @override + Future run( + String executable, + List arguments, { + String? workingDirectory, + }) { + return Process.run( + executable, + arguments, + workingDirectory: workingDirectory, + ); + } } diff --git a/lib/commands/create_command.dart b/lib/commands/create_command.dart index 84a00e5..8494797 100644 --- a/lib/commands/create_command.dart +++ b/lib/commands/create_command.dart @@ -7,7 +7,6 @@ import 'package:ignite_cli/flame_version_manager.dart'; import 'package:ignite_cli/templates/template.dart'; import 'package:ignite_cli/utils.dart'; import 'package:mason/mason.dart'; -import 'package:process_run/process_run.dart'; class CreateCommand extends IgniteCommand { CreateCommand(super.context) { @@ -63,38 +62,33 @@ class CreateCommand extends IgniteCommand { String get name => 'create'; @override - Future run() async { + Future run() async { final argResults = this.argResults; if (argResults == null) { - return ExitCode.usage; + return ExitCode.usage.code; } - await createCommand( - context.logger, - argResults, - context.flameVersionManager, - ); + final code = await createCommand(context, argResults); - return ExitCode.success; + return code; } } -Future createCommand( - Logger logger, +Future createCommand( + IgniteContext context, ArgResults command, - FlameVersionManager flameVersionManager, ) async { final interactive = command['interactive'] != 'false'; if (interactive) { - logger + context.logger ..info('\nWelcome to ${red.wrap('Ignite CLI')}! 🔥') ..info("Let's create a new project!\n"); } final name = getString( isInteractive: interactive, - logger: logger, + logger: context.logger, command, 'name', 'Choose a name for your project', @@ -110,7 +104,7 @@ Future createCommand( ); final org = getString( - logger: logger, + logger: context.logger, isInteractive: interactive, command, 'org', @@ -125,25 +119,25 @@ Future createCommand( }, ); - final versions = flameVersionManager.versions; - final flameVersions = versions[Package.flame]!; + final versions = context.flameVersionManager.versions; + final flameVersions = versions[Package.flame]; final flameVersion = getOption( - logger: logger, + logger: context.logger, isInteractive: interactive, command, 'flame-version', 'Which Flame version do you wish to use?', - flameVersions.visible.associateWith((e) => e), - defaultsTo: flameVersions.versions.first, - fullOptions: flameVersions.versions.associateWith((e) => e), + (flameVersions?.visible ?? []).associateWith((e) => e), + defaultsTo: flameVersions?.versions.first, + fullOptions: flameVersions?.versions.associateWith((e) => e) ?? {}, ); - final extraPackageOptions = flameVersionManager.versions.keys + final extraPackageOptions = context.flameVersionManager.versions.keys .where((key) => !Package.includedByDefault.contains(key)) .map((key) => key.name) .toList(); final extraPackages = getMultiOption( - logger: logger, + logger: context.logger, isInteractive: interactive, isRequired: false, command, @@ -162,25 +156,21 @@ Future createCommand( final devDependencies = packages.where((e) => e.isDevDependency); final currentDir = Directory.current.path; - logger.info('\nYour current directory is: $currentDir'); - - final createFolder = getOption( - logger: logger, - isInteractive: interactive, - command, - 'create-folder', - 'Do you want to put your project files directly on the current dir, ' - 'or do you want to create a folder called $name?', - { - 'Create a folder called $name': 'true', - 'Put the files directly on $currentDir': 'false', - }, - ) == - 'true'; - - logger.info('\n'); + + context.logger.info('Your current directory is: $currentDir'); + + bool createFolder; + if (!interactive) { + createFolder = command['create-folder'] == true; + } else { + createFolder = context.logger.confirm( + 'Create project a folder called $name?', + defaultValue: command['create-folder'] == true, + ); + } + final template = getOption( - logger: logger, + logger: context.logger, isInteractive: interactive, command, 'template', @@ -190,61 +180,97 @@ Future createCommand( ); final actualDir = '$currentDir${createFolder ? '/$name' : ''}'; - if (createFolder) { - await runExecutableArguments('mkdir', [actualDir]); - } - logger.info('\nRunning flutter create on $actualDir ...'); - await runExecutableArguments( - 'flutter', - 'create --org $org --project-name $name .'.split(' '), - workingDirectory: actualDir, - verbose: logger.level == Level.verbose, - ); + final progress = context.logger.progress('Generating project'); + ProcessResult? processResult; + var exitCode = ExitCode.success.code; - await runExecutableArguments( - 'rm', - '-rf lib test'.split(' '), - workingDirectory: actualDir, - verbose: logger.level == Level.verbose, - ); + try { + if (createFolder) { + progress.update('Running [mkdir] on $actualDir'); + processResult = await context.process.run('mkdir', [actualDir]); + if (processResult.exitCode > ExitCode.success.code) { + return exitCode = processResult.exitCode; + } + } - final bundle = Template.byKey(template).bundle; - final generator = await MasonGenerator.fromBundle(bundle); - final target = DirectoryGeneratorTarget(Directory(actualDir)); - - final variables = { - 'name': name, - 'description': 'A simple Flame game.', - 'version': '0.1.0', - 'extra-dependencies': dependencies - .sortedBy((e) => e.name) - .map((package) => package.toMustache(versions, flameVersion)) - .toList(), - 'extra-dev-dependencies': devDependencies - .sortedBy((e) => e.name) - .map((package) => package.toMustache(versions, flameVersion)) - .toList(), - }; - final files = await generator.generate(target, vars: variables); - - final canHaveTests = devDependencies.contains(Package.flameTest); - if (!canHaveTests) { - await runExecutableArguments( + progress.update('Running [flutter create] on $actualDir'); + processResult = await context.process.run( + 'flutter', + 'create --org $org --project-name $name .'.split(' '), + workingDirectory: actualDir, + ); + if (processResult.exitCode > ExitCode.success.code) { + return exitCode = processResult.exitCode; + } + + progress.update('Running [rm -rf lib test] on $actualDir'); + processResult = await context.process.run( 'rm', - '-rf test'.split(' '), + '-rf lib test'.split(' '), workingDirectory: actualDir, - verbose: logger.level == Level.verbose, ); - } + if (processResult.exitCode > ExitCode.success.code) { + return exitCode = processResult.exitCode; + } + progress.update('Bundling game template'); - await runExecutableArguments( - 'flutter', - 'pub get'.split(' '), - workingDirectory: actualDir, - verbose: logger.level == Level.verbose, - ); + final bundle = Template.byKey(template).bundle; + final generator = await MasonGenerator.fromBundle(bundle); + final target = DirectoryGeneratorTarget(Directory(actualDir)); + + final variables = { + 'name': name, + 'description': 'A simple Flame game.', + 'version': '0.1.0', + 'extra-dependencies': dependencies + .sortedBy((e) => e.name) + .map((package) => package.toMustache(versions, flameVersion)) + .toList(), + 'extra-dev-dependencies': devDependencies + .sortedBy((e) => e.name) + .map((package) => package.toMustache(versions, flameVersion)) + .toList(), + }; + final files = await generator.generate(target, vars: variables); + final canHaveTests = devDependencies.contains(Package.flameTest); + + if (!canHaveTests) { + progress.update('Removing tests'); + processResult = await context.process.run( + 'rm', + '-rf test'.split(' '), + workingDirectory: actualDir, + ); + if (processResult.exitCode > ExitCode.success.code) { + return exitCode = processResult.exitCode; + } + } + + progress.update('Removing tests'); + processResult = await context.process.run( + 'flutter', + 'pub get'.split(' '), + workingDirectory: actualDir, + ); + + if (processResult.exitCode > ExitCode.success.code) { + return exitCode = processResult.exitCode; + } + + progress + ..complete('Updated ${files.length} files on top of flutter create.') + ..complete('Your new Flame project was successfully created!'); - logger.info('Updated ${files.length} files on top of flutter create.\n'); - logger.info('Your new Flame project was successfully created!'); + return exitCode; + } catch (_) { + if (processResult != null) { + progress.fail(processResult.stderr.toString()); + exitCode = processResult.exitCode; + } else { + progress.fail(); + } + + rethrow; + } } diff --git a/lib/commands/ignite_command.dart b/lib/commands/ignite_command.dart index 0bcbae7..8ba939c 100644 --- a/lib/commands/ignite_command.dart +++ b/lib/commands/ignite_command.dart @@ -1,25 +1,36 @@ import 'dart:async'; +import 'dart:io' show ProcessResult; import 'package:args/command_runner.dart'; import 'package:ignite_cli/flame_version_manager.dart'; import 'package:mason/mason.dart'; import 'package:mason_logger/mason_logger.dart'; -abstract class IgniteCommand extends Command { +abstract class IgniteCommand extends Command { final IgniteContext context; IgniteCommand(this.context); @override - Future run(); + Future run(); } class IgniteContext { const IgniteContext({ required this.logger, required this.flameVersionManager, + required this.process, }); final Logger logger; final FlameVersionManager flameVersionManager; + final IgniteProcess process; +} + +abstract class IgniteProcess { + Future run( + String executable, + List arguments, { + String? workingDirectory, + }); } diff --git a/lib/commands/version_command.dart b/lib/commands/version_command.dart index 2a80a5e..d2a1d0c 100644 --- a/lib/commands/version_command.dart +++ b/lib/commands/version_command.dart @@ -1,32 +1,31 @@ +import 'dart:math'; + +import 'package:ignite_cli/commands/ignite_command.dart'; import 'package:ignite_cli/version.g.dart'; -import 'package:mason/mason.dart'; -import 'package:process_run/process_run.dart'; - -Future versionCommand(Logger logger) async { - logger.info('ignite --version: $igniteVersion\n'); - - final [dartProcess, flutterProcess] = await [ - runExecutableArguments( - 'dart', - ['--version'], - commandVerbose: false, - verbose: false, - ), - runExecutableArguments( - 'flutter', - ['--version'], - commandVerbose: false, - verbose: false, - ), - ].wait; - - logger.info('${dartProcess.stdout}'); - logger.info('${flutterProcess.stdout}'); + +Future versionCommand(IgniteContext context) async { + context.logger.info('ignite --version: $igniteVersion\n'); + + final (dartProcess, flutterProcess) = await ( + context.process.run('dart', ['--version']), + context.process.run('flutter', ['--version']), + ).wait; + + if (dartProcess.stdout case final String out) { + context.logger.info(out); + } + + if (flutterProcess.stdout case final String out) { + context.logger.info(out); + } if (dartProcess.stderr case final String err when err.isNotEmpty) { - logger.err(err); + context.logger.err(err); } + if (flutterProcess.stderr case final String err when err.isNotEmpty) { - logger.err(err); + context.logger.err(err); } + + return max(dartProcess.exitCode, flutterProcess.exitCode); } diff --git a/lib/ignite_commmand_runner.dart b/lib/ignite_command_runner.dart similarity index 63% rename from lib/ignite_commmand_runner.dart rename to lib/ignite_command_runner.dart index 8fcaf23..93795af 100644 --- a/lib/ignite_commmand_runner.dart +++ b/lib/ignite_command_runner.dart @@ -5,27 +5,14 @@ import 'package:args/command_runner.dart'; import 'package:ignite_cli/commands/create_command.dart'; import 'package:ignite_cli/commands/ignite_command.dart'; import 'package:ignite_cli/commands/version_command.dart'; -import 'package:ignite_cli/flame_version_manager.dart'; import 'package:mason_logger/mason_logger.dart'; -class IgniteCommandRunner extends CommandRunner { +class IgniteCommandRunner extends CommandRunner { late IgniteContext context; - IgniteCommandRunner({ - required Logger logger, - required FlameVersionManager flameVersionManager, - }) : super('ignite', _description) { - context = IgniteContext( - logger: logger, - flameVersionManager: flameVersionManager, - ); - + IgniteCommandRunner(this.context) : super('ignite', _description) { addCommand(CreateCommand(context)); - - argParser.addFlag( - 'verbose', - ); - + argParser.addFlag('verbose'); argParser.addFlag( 'version', help: 'Print the current version.', @@ -34,7 +21,10 @@ class IgniteCommandRunner extends CommandRunner { } @override - Future run(Iterable args) async { + void printUsage() => context.logger.info(usage); + + @override + Future run(Iterable args) async { try { final parsedArgs = parse(args); @@ -43,29 +33,28 @@ class IgniteCommandRunner extends CommandRunner { } if (parsedArgs['version'] == true) { - await versionCommand(context.logger); - return ExitCode.success; + return await versionCommand(context); } - return await runCommand(parsedArgs) ?? ExitCode.success; + return await runCommand(parsedArgs) ?? ExitCode.success.code; } on FormatException catch (exception) { context.logger ..err(exception.message) ..info('') ..info(usage); - return ExitCode.usage; + return ExitCode.usage.code; } on UsageException catch (exception) { context.logger ..err(exception.message) ..info('') ..info(exception.usage); - return ExitCode.usage; + return ExitCode.usage.code; } on ProcessException catch (error) { context.logger.err(error.message); - return ExitCode.unavailable; + return ExitCode.unavailable.code; } on Exception catch (error) { context.logger.err('$error'); - return ExitCode.software; + return ExitCode.software.code; } } diff --git a/lib/utils.dart b/lib/utils.dart index 9d1cc93..fc1950b 100644 --- a/lib/utils.dart +++ b/lib/utils.dart @@ -60,45 +60,33 @@ String getOption( Map options, { required bool isInteractive, required Logger logger, - String? desc, String? defaultsTo, Map fullOptions = const {}, }) { - var value = results[name]; + var value = results[name]?.toString(); if (!isInteractive) { if (value == null) { if (defaultsTo != null) { return defaultsTo; } else { - logger.info('Missing parameter $name is required.'); + logger.err('Missing parameter $name is required.'); exit(ExitCode.usage.code); } } } final fullValues = {...options, ...fullOptions}.values; if (value != null && !fullValues.contains(value)) { - logger.info('Invalid value $value provided. Must be in: ${options.values}'); + logger.err('Invalid value $value provided. Must be in: ${options.values}'); value = null; } while (value == null) { - if (desc != null) { - logger.info(darkGray.wrap('\n$desc\u{1B}[1A\r')); - } - final option = logger.chooseOne(message, choices: options.keys.toList()); value = options[option]; - - if (desc != null) { - logger.info('\r\u{1B}[K'); - } } - return switch (value) { - final String value => value, - final bool value => 'true', - _ => '', - }; + + return value; } List _unwrap(dynamic value) { @@ -142,6 +130,7 @@ List getMultiOption( logger.info('Invalid value $value provided. Must be in: $options'); value = []; } + if (desc != null) { logger.info(darkGray.wrap('\n$desc\u{1B}[1A\r')); } diff --git a/pubspec.lock b/pubspec.lock index 5126986..92b89f2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -321,6 +321,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + mocktail: + dependency: "direct dev" + description: + name: mocktail + sha256: "890df3f9688106f25755f26b1c60589a92b3ab91a22b8b224947ad041bf172d8" + url: "https://pub.dev" + source: hosted + version: "1.0.4" mustache_template: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 1e73bef..0af924c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -22,4 +22,5 @@ dev_dependencies: dartdoc: ^8.0.2 flame_lint: ^1.3.0 mason_cli: ^0.1.2 + mocktail: ^1.0.4 test: ^1.24.9 diff --git a/test/ignite_command_runner_test.dart b/test/ignite_command_runner_test.dart new file mode 100644 index 0000000..71319a8 --- /dev/null +++ b/test/ignite_command_runner_test.dart @@ -0,0 +1,91 @@ +import 'dart:io'; + +import 'package:ignite_cli/commands/ignite_command.dart'; +import 'package:ignite_cli/flame_version_manager.dart'; +import 'package:ignite_cli/ignite_command_runner.dart'; +import 'package:ignite_cli/version.g.dart'; +import 'package:mason_logger/mason_logger.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart'; + +class _MockLogger extends Mock implements Logger {} + +class _MockFlameVersionManager extends Mock implements FlameVersionManager {} + +class _MockIgniteProcess extends Mock implements IgniteProcess {} + +class _MockStdin extends Mock implements Stdin {} + +class _MockStdout extends Mock implements Stdout {} + +const usage = + '''Ignite your projects with Flame; a CLI scaffolding tool to create and setup your Flame projects. + +Usage: ignite [arguments] + +Global options: +-h, --help Print this usage information. + --[no-]verbose + --version Print the current version. + +Available commands: + create Create a new Flame project + +Run "ignite help " for more information about a command.'''; + +void main() { + group('IgniteCommandRunner', () { + late Logger logger; + late IgniteProcess process; + late IgniteCommandRunner commandRunner; + late FlameVersionManager flameVersionManager; + late Stdin stdin; + late Stdout stdout; // ignore: close_sinks + + setUp(() { + logger = _MockLogger(); + flameVersionManager = _MockFlameVersionManager(); + process = _MockIgniteProcess(); + stdin = _MockStdin(); + stdout = _MockStdout(); + + when( + () => process.run( + any(), + any(), + workingDirectory: any(named: 'workingDirectory'), + ), + ).thenAnswer((_) async => ProcessResult(0, 0, stdin, stdout)); + + when(() => flameVersionManager.versions).thenReturn({ + Package.flame: const Versions(['1.28.1']), + }); + + commandRunner = IgniteCommandRunner( + IgniteContext( + logger: logger, + flameVersionManager: flameVersionManager, + process: process, + ), + ); + }); + + test('exits with code 0 when [version] is called', () async { + final exitCode = await commandRunner.run(['--version']); + expect(exitCode, equals(ExitCode.success.code)); + + verify(() => logger.info('ignite --version: $igniteVersion\n')); + verify(() => process.run('flutter', ['--version'])).called(1); + verify(() => process.run('dart', ['--version'])).called(1); + verifyNever(() => logger.err(any())); + }); + + test('exits with code 0 when [verbose] is set', () async { + final exitCode = await commandRunner.run(['--verbose']); + expect(exitCode, equals(ExitCode.success.code)); + + verify(() => logger.level = Level.verbose).called(1); + verifyNever(() => logger.err(any())); + }); + }); +} From 427fef4d13f8a5476186c6f6f89f21e114d69856 Mon Sep 17 00:00:00 2001 From: Elijah Luckey Date: Mon, 28 Apr 2025 22:28:55 -0400 Subject: [PATCH 15/20] rename --- lib/commands/create_command.dart | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/commands/create_command.dart b/lib/commands/create_command.dart index 8494797..c11c6a1 100644 --- a/lib/commands/create_command.dart +++ b/lib/commands/create_command.dart @@ -183,14 +183,14 @@ Future createCommand( final progress = context.logger.progress('Generating project'); ProcessResult? processResult; - var exitCode = ExitCode.success.code; + var code = ExitCode.success.code; try { if (createFolder) { progress.update('Running [mkdir] on $actualDir'); processResult = await context.process.run('mkdir', [actualDir]); if (processResult.exitCode > ExitCode.success.code) { - return exitCode = processResult.exitCode; + return code = processResult.exitCode; } } @@ -201,7 +201,7 @@ Future createCommand( workingDirectory: actualDir, ); if (processResult.exitCode > ExitCode.success.code) { - return exitCode = processResult.exitCode; + return code = processResult.exitCode; } progress.update('Running [rm -rf lib test] on $actualDir'); @@ -211,7 +211,7 @@ Future createCommand( workingDirectory: actualDir, ); if (processResult.exitCode > ExitCode.success.code) { - return exitCode = processResult.exitCode; + return code = processResult.exitCode; } progress.update('Bundling game template'); @@ -243,7 +243,7 @@ Future createCommand( workingDirectory: actualDir, ); if (processResult.exitCode > ExitCode.success.code) { - return exitCode = processResult.exitCode; + return code = processResult.exitCode; } } @@ -255,18 +255,18 @@ Future createCommand( ); if (processResult.exitCode > ExitCode.success.code) { - return exitCode = processResult.exitCode; + return code = processResult.exitCode; } progress ..complete('Updated ${files.length} files on top of flutter create.') ..complete('Your new Flame project was successfully created!'); - return exitCode; + return code; } catch (_) { if (processResult != null) { progress.fail(processResult.stderr.toString()); - exitCode = processResult.exitCode; + code = processResult.exitCode; } else { progress.fail(); } From d291f10b7e87b2e65f036de82c023e8a19720054 Mon Sep 17 00:00:00 2001 From: Elijah Luckey Date: Mon, 28 Apr 2025 22:36:38 -0400 Subject: [PATCH 16/20] scope run to just context --- analysis_options.yaml | 2 +- bin/ignite.dart | 16 ------------ lib/commands/create_command.dart | 13 +++++----- lib/commands/ignite_command.dart | 14 ++++++----- lib/commands/version_command.dart | 4 +-- test/ignite_command_runner_test.dart | 37 +++++++--------------------- 6 files changed, 27 insertions(+), 59 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index 719c7bf..c00cf11 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -2,4 +2,4 @@ include: package:flame_lint/analysis_options.yaml analyzer: exclude: - - bricks/ + - bricks/**/* diff --git a/bin/ignite.dart b/bin/ignite.dart index 9474a31..86c8d36 100644 --- a/bin/ignite.dart +++ b/bin/ignite.dart @@ -10,24 +10,8 @@ Future main(List args) async { IgniteContext( logger: Logger(), flameVersionManager: await FlameVersionManager.fetch(), - process: _IgniteProcess(), ), ); exit(await runner.run(args)); } - -class _IgniteProcess extends IgniteProcess { - @override - Future run( - String executable, - List arguments, { - String? workingDirectory, - }) { - return Process.run( - executable, - arguments, - workingDirectory: workingDirectory, - ); - } -} diff --git a/lib/commands/create_command.dart b/lib/commands/create_command.dart index c11c6a1..f66c613 100644 --- a/lib/commands/create_command.dart +++ b/lib/commands/create_command.dart @@ -6,7 +6,8 @@ import 'package:ignite_cli/commands/ignite_command.dart'; import 'package:ignite_cli/flame_version_manager.dart'; import 'package:ignite_cli/templates/template.dart'; import 'package:ignite_cli/utils.dart'; -import 'package:mason/mason.dart'; +import 'package:mason/mason.dart' + show DirectoryGeneratorTarget, ExitCode, MasonGenerator, red; class CreateCommand extends IgniteCommand { CreateCommand(super.context) { @@ -188,14 +189,14 @@ Future createCommand( try { if (createFolder) { progress.update('Running [mkdir] on $actualDir'); - processResult = await context.process.run('mkdir', [actualDir]); + processResult = await context.run('mkdir', [actualDir]); if (processResult.exitCode > ExitCode.success.code) { return code = processResult.exitCode; } } progress.update('Running [flutter create] on $actualDir'); - processResult = await context.process.run( + processResult = await context.run( 'flutter', 'create --org $org --project-name $name .'.split(' '), workingDirectory: actualDir, @@ -205,7 +206,7 @@ Future createCommand( } progress.update('Running [rm -rf lib test] on $actualDir'); - processResult = await context.process.run( + processResult = await context.run( 'rm', '-rf lib test'.split(' '), workingDirectory: actualDir, @@ -237,7 +238,7 @@ Future createCommand( if (!canHaveTests) { progress.update('Removing tests'); - processResult = await context.process.run( + processResult = await context.run( 'rm', '-rf test'.split(' '), workingDirectory: actualDir, @@ -248,7 +249,7 @@ Future createCommand( } progress.update('Removing tests'); - processResult = await context.process.run( + processResult = await context.run( 'flutter', 'pub get'.split(' '), workingDirectory: actualDir, diff --git a/lib/commands/ignite_command.dart b/lib/commands/ignite_command.dart index 8ba939c..6cd75d8 100644 --- a/lib/commands/ignite_command.dart +++ b/lib/commands/ignite_command.dart @@ -1,5 +1,5 @@ import 'dart:async'; -import 'dart:io' show ProcessResult; +import 'dart:io' show Process, ProcessResult; import 'package:args/command_runner.dart'; import 'package:ignite_cli/flame_version_manager.dart'; @@ -19,18 +19,20 @@ class IgniteContext { const IgniteContext({ required this.logger, required this.flameVersionManager, - required this.process, }); final Logger logger; final FlameVersionManager flameVersionManager; - final IgniteProcess process; -} -abstract class IgniteProcess { Future run( String executable, List arguments, { String? workingDirectory, - }); + }) { + return Process.run( + executable, + arguments, + workingDirectory: workingDirectory, + ); + } } diff --git a/lib/commands/version_command.dart b/lib/commands/version_command.dart index d2a1d0c..5a7e809 100644 --- a/lib/commands/version_command.dart +++ b/lib/commands/version_command.dart @@ -7,8 +7,8 @@ Future versionCommand(IgniteContext context) async { context.logger.info('ignite --version: $igniteVersion\n'); final (dartProcess, flutterProcess) = await ( - context.process.run('dart', ['--version']), - context.process.run('flutter', ['--version']), + context.run('dart', ['--version']), + context.run('flutter', ['--version']), ).wait; if (dartProcess.stdout case final String out) { diff --git a/test/ignite_command_runner_test.dart b/test/ignite_command_runner_test.dart index 71319a8..d50df42 100644 --- a/test/ignite_command_runner_test.dart +++ b/test/ignite_command_runner_test.dart @@ -12,45 +12,32 @@ class _MockLogger extends Mock implements Logger {} class _MockFlameVersionManager extends Mock implements FlameVersionManager {} -class _MockIgniteProcess extends Mock implements IgniteProcess {} - class _MockStdin extends Mock implements Stdin {} class _MockStdout extends Mock implements Stdout {} -const usage = - '''Ignite your projects with Flame; a CLI scaffolding tool to create and setup your Flame projects. - -Usage: ignite [arguments] - -Global options: --h, --help Print this usage information. - --[no-]verbose - --version Print the current version. - -Available commands: - create Create a new Flame project - -Run "ignite help " for more information about a command.'''; +class _MockIgniteContext extends Mock implements IgniteContext {} void main() { group('IgniteCommandRunner', () { late Logger logger; - late IgniteProcess process; late IgniteCommandRunner commandRunner; late FlameVersionManager flameVersionManager; late Stdin stdin; late Stdout stdout; // ignore: close_sinks + late IgniteContext context; setUp(() { logger = _MockLogger(); flameVersionManager = _MockFlameVersionManager(); - process = _MockIgniteProcess(); stdin = _MockStdin(); stdout = _MockStdout(); + context = _MockIgniteContext(); + when(() => context.logger).thenReturn(logger); + when(() => context.flameVersionManager).thenReturn(flameVersionManager); when( - () => process.run( + () => context.run( any(), any(), workingDirectory: any(named: 'workingDirectory'), @@ -61,13 +48,7 @@ void main() { Package.flame: const Versions(['1.28.1']), }); - commandRunner = IgniteCommandRunner( - IgniteContext( - logger: logger, - flameVersionManager: flameVersionManager, - process: process, - ), - ); + commandRunner = IgniteCommandRunner(context); }); test('exits with code 0 when [version] is called', () async { @@ -75,8 +56,8 @@ void main() { expect(exitCode, equals(ExitCode.success.code)); verify(() => logger.info('ignite --version: $igniteVersion\n')); - verify(() => process.run('flutter', ['--version'])).called(1); - verify(() => process.run('dart', ['--version'])).called(1); + verify(() => context.run('flutter', ['--version'])).called(1); + verify(() => context.run('dart', ['--version'])).called(1); verifyNever(() => logger.err(any())); }); From 96a18dbbc13fb0d5cc755be6ac20cb992b8547d8 Mon Sep 17 00:00:00 2001 From: Elijah Luckey Date: Mon, 28 Apr 2025 22:47:19 -0400 Subject: [PATCH 17/20] it's all context --- bin/ignite.dart | 2 +- lib/commands/create_command.dart | 9 ++++---- lib/commands/ignite_command.dart | 27 +--------------------- lib/commands/version_command.dart | 2 +- lib/ignite_command_runner.dart | 2 +- lib/ignite_context.dart | 34 ++++++++++++++++++++++++++++ test/ignite_command_runner_test.dart | 2 +- 7 files changed, 44 insertions(+), 34 deletions(-) create mode 100644 lib/ignite_context.dart diff --git a/bin/ignite.dart b/bin/ignite.dart index 86c8d36..5caa576 100644 --- a/bin/ignite.dart +++ b/bin/ignite.dart @@ -1,8 +1,8 @@ import 'dart:io'; -import 'package:ignite_cli/commands/ignite_command.dart'; import 'package:ignite_cli/flame_version_manager.dart'; import 'package:ignite_cli/ignite_command_runner.dart'; +import 'package:ignite_cli/ignite_context.dart'; import 'package:mason_logger/mason_logger.dart'; Future main(List args) async { diff --git a/lib/commands/create_command.dart b/lib/commands/create_command.dart index f66c613..5404777 100644 --- a/lib/commands/create_command.dart +++ b/lib/commands/create_command.dart @@ -4,10 +4,10 @@ import 'package:args/args.dart'; import 'package:dartlin/dartlin.dart'; import 'package:ignite_cli/commands/ignite_command.dart'; import 'package:ignite_cli/flame_version_manager.dart'; +import 'package:ignite_cli/ignite_context.dart'; import 'package:ignite_cli/templates/template.dart'; import 'package:ignite_cli/utils.dart'; -import 'package:mason/mason.dart' - show DirectoryGeneratorTarget, ExitCode, MasonGenerator, red; +import 'package:mason/mason.dart' show ExitCode, red; class CreateCommand extends IgniteCommand { CreateCommand(super.context) { @@ -217,8 +217,8 @@ Future createCommand( progress.update('Bundling game template'); final bundle = Template.byKey(template).bundle; - final generator = await MasonGenerator.fromBundle(bundle); - final target = DirectoryGeneratorTarget(Directory(actualDir)); + final generator = await context.generatorFromBundle(bundle); + final target = context.createTarget(Directory(actualDir)); final variables = { 'name': name, @@ -233,6 +233,7 @@ Future createCommand( .map((package) => package.toMustache(versions, flameVersion)) .toList(), }; + final files = await generator.generate(target, vars: variables); final canHaveTests = devDependencies.contains(Package.flameTest); diff --git a/lib/commands/ignite_command.dart b/lib/commands/ignite_command.dart index 6cd75d8..9638004 100644 --- a/lib/commands/ignite_command.dart +++ b/lib/commands/ignite_command.dart @@ -1,10 +1,7 @@ import 'dart:async'; -import 'dart:io' show Process, ProcessResult; import 'package:args/command_runner.dart'; -import 'package:ignite_cli/flame_version_manager.dart'; -import 'package:mason/mason.dart'; -import 'package:mason_logger/mason_logger.dart'; +import 'package:ignite_cli/ignite_context.dart'; abstract class IgniteCommand extends Command { final IgniteContext context; @@ -14,25 +11,3 @@ abstract class IgniteCommand extends Command { @override Future run(); } - -class IgniteContext { - const IgniteContext({ - required this.logger, - required this.flameVersionManager, - }); - - final Logger logger; - final FlameVersionManager flameVersionManager; - - Future run( - String executable, - List arguments, { - String? workingDirectory, - }) { - return Process.run( - executable, - arguments, - workingDirectory: workingDirectory, - ); - } -} diff --git a/lib/commands/version_command.dart b/lib/commands/version_command.dart index 5a7e809..b4f521b 100644 --- a/lib/commands/version_command.dart +++ b/lib/commands/version_command.dart @@ -1,6 +1,6 @@ import 'dart:math'; -import 'package:ignite_cli/commands/ignite_command.dart'; +import 'package:ignite_cli/ignite_context.dart'; import 'package:ignite_cli/version.g.dart'; Future versionCommand(IgniteContext context) async { diff --git a/lib/ignite_command_runner.dart b/lib/ignite_command_runner.dart index 93795af..c4c1cfd 100644 --- a/lib/ignite_command_runner.dart +++ b/lib/ignite_command_runner.dart @@ -3,8 +3,8 @@ import 'dart:io'; import 'package:args/command_runner.dart'; import 'package:ignite_cli/commands/create_command.dart'; -import 'package:ignite_cli/commands/ignite_command.dart'; import 'package:ignite_cli/commands/version_command.dart'; +import 'package:ignite_cli/ignite_context.dart'; import 'package:mason_logger/mason_logger.dart'; class IgniteCommandRunner extends CommandRunner { diff --git a/lib/ignite_context.dart b/lib/ignite_context.dart new file mode 100644 index 0000000..1fce520 --- /dev/null +++ b/lib/ignite_context.dart @@ -0,0 +1,34 @@ +import 'dart:io' show Directory, Process, ProcessResult; + +import 'package:ignite_cli/flame_version_manager.dart'; +import 'package:mason/mason.dart'; + +class IgniteContext { + const IgniteContext({ + required this.logger, + required this.flameVersionManager, + }); + + final Logger logger; + final FlameVersionManager flameVersionManager; + + Future run( + String executable, + List arguments, { + String? workingDirectory, + }) { + return Process.run( + executable, + arguments, + workingDirectory: workingDirectory, + ); + } + + Future generatorFromBundle(MasonBundle bundle) { + return MasonGenerator.fromBundle(bundle); + } + + DirectoryGeneratorTarget createTarget(Directory directory) { + return DirectoryGeneratorTarget(directory); + } +} diff --git a/test/ignite_command_runner_test.dart b/test/ignite_command_runner_test.dart index d50df42..6cb751f 100644 --- a/test/ignite_command_runner_test.dart +++ b/test/ignite_command_runner_test.dart @@ -1,8 +1,8 @@ import 'dart:io'; -import 'package:ignite_cli/commands/ignite_command.dart'; import 'package:ignite_cli/flame_version_manager.dart'; import 'package:ignite_cli/ignite_command_runner.dart'; +import 'package:ignite_cli/ignite_context.dart'; import 'package:ignite_cli/version.g.dart'; import 'package:mason_logger/mason_logger.dart'; import 'package:mocktail/mocktail.dart'; From 323759326c6a34eac8da592d8a5498fe39859d15 Mon Sep 17 00:00:00 2001 From: Elijah Luckey Date: Mon, 28 Apr 2025 23:00:05 -0400 Subject: [PATCH 18/20] fix closing buffer for stdout and stdin --- bin/ignite.dart | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/bin/ignite.dart b/bin/ignite.dart index 5caa576..c1fdaf3 100644 --- a/bin/ignite.dart +++ b/bin/ignite.dart @@ -6,12 +6,11 @@ import 'package:ignite_cli/ignite_context.dart'; import 'package:mason_logger/mason_logger.dart'; Future main(List args) async { - final runner = IgniteCommandRunner( - IgniteContext( - logger: Logger(), - flameVersionManager: await FlameVersionManager.fetch(), - ), - ); + final manager = await FlameVersionManager.fetch(); + final context = IgniteContext(logger: Logger(), flameVersionManager: manager); + final runner = IgniteCommandRunner(context); + final code = await runner.run(args); - exit(await runner.run(args)); + // ensure all buffered output is written before exit + await Future.wait([stdout.close(), stderr.close()]).then((_) => exit(code)); } From 76d24c245b1c5e5c1b953677df00e1ad9390a958 Mon Sep 17 00:00:00 2001 From: Elijah Luckey Date: Mon, 28 Apr 2025 23:00:14 -0400 Subject: [PATCH 19/20] no show imports --- lib/commands/create_command.dart | 2 +- lib/ignite_context.dart | 2 +- lib/utils.dart | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/commands/create_command.dart b/lib/commands/create_command.dart index 5404777..52f4a54 100644 --- a/lib/commands/create_command.dart +++ b/lib/commands/create_command.dart @@ -7,7 +7,7 @@ import 'package:ignite_cli/flame_version_manager.dart'; import 'package:ignite_cli/ignite_context.dart'; import 'package:ignite_cli/templates/template.dart'; import 'package:ignite_cli/utils.dart'; -import 'package:mason/mason.dart' show ExitCode, red; +import 'package:mason/mason.dart'; class CreateCommand extends IgniteCommand { CreateCommand(super.context) { diff --git a/lib/ignite_context.dart b/lib/ignite_context.dart index 1fce520..31a691d 100644 --- a/lib/ignite_context.dart +++ b/lib/ignite_context.dart @@ -1,4 +1,4 @@ -import 'dart:io' show Directory, Process, ProcessResult; +import 'dart:io'; import 'package:ignite_cli/flame_version_manager.dart'; import 'package:mason/mason.dart'; diff --git a/lib/utils.dart b/lib/utils.dart index fc1950b..afeaa25 100644 --- a/lib/utils.dart +++ b/lib/utils.dart @@ -1,6 +1,6 @@ import 'dart:io'; -import 'package:args/args.dart' show ArgResults; +import 'package:args/args.dart'; import 'package:mason_logger/mason_logger.dart'; import 'package:path/path.dart' as p; From 5137bd4ec1c87a2308094afe884b3fd1e6cf6615 Mon Sep 17 00:00:00 2001 From: Elijah Luckey Date: Mon, 28 Apr 2025 23:01:17 -0400 Subject: [PATCH 20/20] null assert --- lib/commands/create_command.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/commands/create_command.dart b/lib/commands/create_command.dart index 52f4a54..e68c42f 100644 --- a/lib/commands/create_command.dart +++ b/lib/commands/create_command.dart @@ -121,16 +121,16 @@ Future createCommand( ); final versions = context.flameVersionManager.versions; - final flameVersions = versions[Package.flame]; + final flameVersions = versions[Package.flame]!; final flameVersion = getOption( logger: context.logger, isInteractive: interactive, command, 'flame-version', 'Which Flame version do you wish to use?', - (flameVersions?.visible ?? []).associateWith((e) => e), - defaultsTo: flameVersions?.versions.first, - fullOptions: flameVersions?.versions.associateWith((e) => e) ?? {}, + flameVersions.visible.associateWith((e) => e), + defaultsTo: flameVersions.versions.first, + fullOptions: flameVersions.versions.associateWith((e) => e), ); final extraPackageOptions = context.flameVersionManager.versions.keys