Line data Source code
1 : import 'package:args/args.dart'; 2 : import 'package:args/command_runner.dart'; 3 : import 'package:cli_completion/cli_completion.dart'; 4 : import 'package:mason/mason.dart' hide packageVersion; 5 : import 'package:meta/meta.dart'; 6 : import 'package:path/path.dart' as path; 7 : import 'package:pub_updater/pub_updater.dart'; 8 : import 'package:universal_io/io.dart'; 9 : import 'package:very_good_cli/src/commands/commands.dart'; 10 : import 'package:very_good_cli/src/logger_extension.dart'; 11 : import 'package:very_good_cli/src/version.dart'; 12 : 13 : /// The package name. 14 : const packageName = 'very_good_cli'; 15 : 16 : /// {@template very_good_command_runner} 17 : /// A [CommandRunner] for the Very Good CLI. 18 : /// {@endtemplate} 19 : class VeryGoodCommandRunner extends CompletionCommandRunner<int> { 20 : /// {@macro very_good_command_runner} 21 15 : VeryGoodCommandRunner({ 22 : Logger? logger, 23 : PubUpdater? pubUpdater, 24 : Map<String, String>? environment, 25 1 : }) : _logger = logger ?? Logger(), 26 1 : _pubUpdater = pubUpdater ?? PubUpdater(), 27 1 : _environment = environment ?? Platform.environment, 28 15 : super('very_good', '🦄 A Very Good Command-Line Interface') { 29 15 : argParser 30 15 : ..addFlag( 31 : 'version', 32 : negatable: false, 33 : help: 'Print the current version.', 34 : ) 35 15 : ..addFlag( 36 : 'verbose', 37 : help: 'Noisy logging, including all shell commands executed.', 38 : ); 39 45 : addCommand(CreateCommand(logger: _logger)); 40 45 : addCommand(PackagesCommand(logger: _logger)); 41 45 : addCommand(TestCommand(logger: _logger)); 42 45 : addCommand(UpdateCommand(logger: _logger, pubUpdater: pubUpdater)); 43 : } 44 : 45 : /// Standard timeout duration for the CLI. 46 : static const timeout = Duration(milliseconds: 500); 47 : 48 : final Logger _logger; 49 : final PubUpdater _pubUpdater; 50 : 51 : /// Map of environments information. 52 45 : Map<String, String> get environment => environmentOverride ?? _environment; 53 : final Map<String, String> _environment; 54 : 55 : /// Boolean for checking if windows, which can be overridden for 56 : /// testing purposes. 57 : @visibleForTesting 58 : bool? isWindowsOverride; 59 3 : bool get _isWindows => isWindowsOverride ?? Platform.isWindows; 60 : 61 1 : @override 62 3 : void printUsage() => _logger.info(usage); 63 : 64 15 : @override 65 : Future<int> run(Iterable<String> args) async { 66 : try { 67 15 : final argResults = parse(args); 68 : 69 30 : if (argResults['verbose'] == true) { 70 2 : _logger.level = Level.verbose; 71 : } 72 29 : return await runCommand(argResults) ?? ExitCode.success.code; 73 3 : } on FormatException catch (e, stackTrace) { 74 1 : _logger 75 2 : ..err(e.message) 76 2 : ..err('$stackTrace') 77 1 : ..info('') 78 2 : ..info(usage); 79 1 : return ExitCode.usage.code; 80 3 : } on UsageException catch (e) { 81 3 : _logger 82 6 : ..err(e.message) 83 3 : ..info('') 84 6 : ..info(e.usage); 85 3 : return ExitCode.usage.code; 86 : } 87 : } 88 : 89 15 : @override 90 : Future<int?> runCommand(ArgResults topLevelResults) async { 91 45 : if (topLevelResults.command?.name == 'completion') { 92 1 : await super.runCommand(topLevelResults); 93 1 : return ExitCode.success.code; 94 : } 95 : 96 15 : _logger 97 15 : ..detail('Argument information:') 98 15 : ..detail(' Top level options:'); 99 30 : for (final option in topLevelResults.options) { 100 15 : if (topLevelResults.wasParsed(option)) { 101 4 : _logger.detail(' - $option: ${topLevelResults[option]}'); 102 : } 103 : } 104 15 : if (topLevelResults.command != null) { 105 15 : final commandResult = topLevelResults.command!; 106 15 : _logger 107 45 : ..detail(' Command: ${commandResult.name}') 108 15 : ..detail(' Command options:'); 109 30 : for (final option in commandResult.options) { 110 15 : if (commandResult.wasParsed(option)) { 111 16 : _logger.detail(' - $option: ${commandResult[option]}'); 112 : } 113 : } 114 : 115 15 : if (commandResult.command != null) { 116 10 : final subCommandResult = commandResult.command!; 117 40 : _logger.detail(' Command sub command: ${subCommandResult.name}'); 118 : } 119 : } 120 : 121 15 : int? exitCode = ExitCode.unavailable.code; 122 30 : if (topLevelResults['version'] == true) { 123 2 : _logger.info(packageVersion); 124 1 : exitCode = ExitCode.success.code; 125 : } else { 126 15 : exitCode = await super.runCommand(topLevelResults); 127 : } 128 45 : if (topLevelResults.command?.name != UpdateCommand.commandName) { 129 14 : await _checkForUpdates(); 130 : } 131 15 : _showThankYou(); 132 : return exitCode; 133 : } 134 : 135 14 : Future<void> _checkForUpdates() async { 136 : try { 137 28 : final latestVersion = await _pubUpdater.getLatestVersion(packageName); 138 1 : final isUpToDate = packageVersion == latestVersion; 139 : if (!isUpToDate) { 140 1 : _logger 141 1 : ..info('') 142 1 : ..info( 143 : ''' 144 3 : ${lightYellow.wrap('Update available!')} ${lightCyan.wrap(packageVersion)} \u2192 ${lightCyan.wrap(latestVersion)} 145 3 : ${lightYellow.wrap('Changelog:')} ${lightCyan.wrap('https://github.com/verygoodopensource/very_good_cli/releases/tag/v$latestVersion')} 146 2 : Run ${lightCyan.wrap('very_good update')} to update''', 147 : ); 148 : } 149 : } catch (_) {} 150 : } 151 : 152 15 : void _showThankYou() { 153 30 : if (environment.containsKey('CI')) return; 154 : 155 1 : final versionFile = File( 156 3 : path.join(_configDir.path, 'version'), 157 1 : )..createSync(recursive: true); 158 : 159 2 : if (versionFile.readAsStringSync() == packageVersion) return; 160 1 : versionFile.writeAsStringSync(packageVersion); 161 : 162 2 : _logger.wrap( 163 1 : lightMagenta.wrap(''' 164 : 165 : Thank you for using Very Good Ventures open source tools! 166 4 : Don't forget to fill out this form to get information on future updates and releases here: ${lightBlue.wrap(link(uri: Uri.parse('https://verygood.ventures/open-source/cli/subscribe-latest-tool-updates')))}'''), 167 2 : print: _logger.info, 168 : ); 169 : } 170 : 171 1 : Directory get _configDir { 172 1 : if (_isWindows) { 173 : // Use localappdata on windows 174 2 : final localAppData = environment['LOCALAPPDATA']!; 175 2 : return Directory(path.join(localAppData, 'VeryGoodCLI')); 176 : } else { 177 : // Try using XDG config folder 178 2 : var dirPath = environment['XDG_CONFIG_HOME']; 179 : // Fallback to $HOME if not following XDG specification 180 1 : if (dirPath == null || dirPath.isEmpty) { 181 2 : dirPath = environment['HOME']; 182 : } 183 2 : return Directory(path.join(dirPath!, '.very_good_cli')); 184 : } 185 : } 186 : }