diff --git a/lib/src/command_runner.dart b/lib/src/command_runner.dart index 91d0d9e09..f2b89cf2c 100644 --- a/lib/src/command_runner.dart +++ b/lib/src/command_runner.dart @@ -3,6 +3,7 @@ import 'package:args/command_runner.dart'; import 'package:io/ansi.dart'; import 'package:io/io.dart'; import 'package:mason/mason.dart'; +import 'package:pub_updater/pub_updater.dart'; import 'package:usage/usage_io.dart'; import 'package:very_good_cli/src/commands/commands.dart'; import 'package:very_good_cli/src/version.dart'; @@ -13,15 +14,22 @@ const _gaTrackingId = 'UA-117465969-4'; // The Google Analytics Application Name. const _gaAppName = 'very-good-cli'; +/// The package name. +const packageName = 'very_good_cli'; + /// {@template very_good_command_runner} /// A [CommandRunner] for the Very Good CLI. /// {@endtemplate} class VeryGoodCommandRunner extends CommandRunner { /// {@macro very_good_command_runner} - VeryGoodCommandRunner({Analytics? analytics, Logger? logger}) - : _logger = logger ?? Logger(), + VeryGoodCommandRunner({ + Analytics? analytics, + Logger? logger, + PubUpdater? pubUpdater, + }) : _logger = logger ?? Logger(), _analytics = analytics ?? AnalyticsIO(_gaTrackingId, _gaAppName, packageVersion), + _pubUpdater = pubUpdater ?? PubUpdater(), super('very_good', '🦄 A Very Good Command Line Interface') { argParser ..addFlag( @@ -46,6 +54,7 @@ class VeryGoodCommandRunner extends CommandRunner { final Logger _logger; final Analytics _analytics; + final PubUpdater _pubUpdater; @override Future run(Iterable args) async { @@ -85,6 +94,7 @@ class VeryGoodCommandRunner extends CommandRunner { @override Future runCommand(ArgResults topLevelResults) async { + await _checkForUpdates(); if (topLevelResults['version'] == true) { _logger.info('very_good version: $packageVersion'); return ExitCode.success.code; @@ -97,4 +107,32 @@ class VeryGoodCommandRunner extends CommandRunner { } return super.runCommand(topLevelResults); } + + Future _checkForUpdates() async { + try { + final isUpToDate = await _pubUpdater.isUpToDate( + packageName: packageName, + currentVersion: packageVersion, + ); + + if (!isUpToDate) { + _logger.info( + lightYellow.wrap('A new release of $packageName is available.'), + ); + final response = _logger.prompt('Would you like to update? (y/n) '); + if (response.isYes()) { + final done = _logger.progress('Updating'); + await _pubUpdater.update(packageName: packageName); + done('Updated!'); + } + } + } catch (_) {} + } +} + +extension on String { + bool isYes() { + final normalized = toLowerCase().trim(); + return normalized == 'y' || normalized == 'yes'; + } } diff --git a/pubspec.yaml b/pubspec.yaml index 0b631692c..8288a65da 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,6 +12,7 @@ dependencies: mason: ^0.0.1-dev.46 meta: ^1.3.0 path: ^1.8.0 + pub_updater: ^0.1.0 universal_io: ^2.0.4 usage: ^4.0.2 very_good_analysis: ^2.3.0 diff --git a/test/src/command_runner_test.dart b/test/src/command_runner_test.dart index f0ba26e57..ee7857cf4 100644 --- a/test/src/command_runner_test.dart +++ b/test/src/command_runner_test.dart @@ -2,10 +2,13 @@ import 'dart:async'; import 'package:args/command_runner.dart'; +import 'package:io/ansi.dart'; import 'package:io/io.dart'; import 'package:mason/mason.dart'; import 'package:mocktail/mocktail.dart'; +import 'package:pub_updater/pub_updater.dart'; import 'package:test/test.dart'; +import 'package:universal_io/io.dart'; import 'package:usage/usage_io.dart'; import 'package:very_good_cli/src/command_runner.dart'; import 'package:very_good_cli/src/version.dart'; @@ -14,6 +17,10 @@ class MockAnalytics extends Mock implements Analytics {} class MockLogger extends Mock implements Logger {} +class MockPubUpdater extends Mock implements PubUpdater {} + +class FakeProcessResult extends Fake implements ProcessResult {} + const expectedUsage = [ '🦄 A Very Good Command Line Interface\n' '\n' @@ -34,10 +41,14 @@ const expectedUsage = [ 'Run "very_good help " for more information about a command.' ]; +const responseBody = + '{"name": "very_good_cli", "versions": ["0.4.0", "0.3.3"]}'; + void main() { group('VeryGoodCommandRunner', () { late List printLogs; late Analytics analytics; + late PubUpdater pubUpdater; late Logger logger; late VeryGoodCommandRunner commandRunner; @@ -54,13 +65,30 @@ void main() { printLogs = []; analytics = MockAnalytics(); + pubUpdater = MockPubUpdater(); + when(() => analytics.firstRun).thenReturn(false); when(() => analytics.enabled).thenReturn(false); + when( + () => pubUpdater.isUpToDate( + packageName: any(named: 'packageName'), + currentVersion: any(named: 'currentVersion'), + ), + ).thenAnswer((_) => Future.value(true)); + + when( + () => pubUpdater.update( + packageName: any(named: 'packageName'), + ), + ).thenAnswer((_) => Future.value(FakeProcessResult())); + logger = MockLogger(); + commandRunner = VeryGoodCommandRunner( analytics: analytics, logger: logger, + pubUpdater: pubUpdater, ); }); @@ -71,6 +99,57 @@ void main() { }); group('run', () { + test('prompts for update when newer version exists', () async { + when(() => pubUpdater.isUpToDate( + packageName: any(named: 'packageName'), + currentVersion: any(named: 'currentVersion'), + )).thenAnswer((_) => Future.value(false)); + + when(() => logger.prompt(any())).thenReturn('n'); + + final result = await commandRunner.run(['--version']); + expect(result, equals(ExitCode.success.code)); + verify( + () => logger.info( + lightYellow.wrap('A new release of $packageName is available.'), + ), + ).called(1); + verify( + () => logger.prompt('Would you like to update? (y/n) '), + ).called(1); + }); + + test('handles pub update errors gracefully', () async { + when( + () => pubUpdater.isUpToDate( + packageName: any(named: 'packageName'), + currentVersion: any(named: 'currentVersion'), + ), + ).thenThrow(Exception('oops')); + + final result = await commandRunner.run(['--version']); + expect(result, equals(ExitCode.success.code)); + verifyNever( + () => logger.info( + lightYellow.wrap('A new release of $packageName is available.'), + ), + ); + }); + + test('updates on "y" response when newer version exists', () async { + when(() => pubUpdater.isUpToDate( + packageName: any(named: 'packageName'), + currentVersion: any(named: 'currentVersion'), + )).thenAnswer((_) => Future.value(false)); + + when(() => logger.prompt(any())).thenReturn('y'); + when(() => logger.progress(any())).thenReturn(([String? message]) {}); + + final result = await commandRunner.run(['--version']); + expect(result, equals(ExitCode.success.code)); + verify(() => logger.progress('Updating')).called(1); + }); + test('prompts for analytics collection on first run (y)', () async { when(() => analytics.firstRun).thenReturn(true); when(() => logger.prompt(any())).thenReturn('y'); diff --git a/test/src/commands/create_test.dart b/test/src/commands/create_test.dart index d5dafe0dc..538107601 100644 --- a/test/src/commands/create_test.dart +++ b/test/src/commands/create_test.dart @@ -5,6 +5,7 @@ import 'package:io/io.dart'; import 'package:mason/mason.dart'; import 'package:mocktail/mocktail.dart'; import 'package:path/path.dart' as p; +import 'package:pub_updater/pub_updater.dart'; import 'package:test/test.dart'; import 'package:universal_io/io.dart'; import 'package:usage/usage_io.dart'; @@ -45,6 +46,8 @@ class MockAnalytics extends Mock implements Analytics {} class MockLogger extends Mock implements Logger {} +class MockPubUpdater extends Mock implements PubUpdater {} + class MockMasonGenerator extends Mock implements MasonGenerator {} class FakeDirectoryGeneratorTarget extends Fake @@ -56,6 +59,7 @@ void main() { late List printLogs; late Analytics analytics; late Logger logger; + late PubUpdater pubUpdater; late VeryGoodCommandRunner commandRunner; void Function() overridePrint(void Function() fn) { @@ -90,9 +94,20 @@ void main() { if (_ != null) progressLogs.add(_); }, ); + + pubUpdater = MockPubUpdater(); + + when( + () => pubUpdater.isUpToDate( + packageName: any(named: 'packageName'), + currentVersion: any(named: 'currentVersion'), + ), + ).thenAnswer((_) => Future.value(true)); + commandRunner = VeryGoodCommandRunner( analytics: analytics, logger: logger, + pubUpdater: pubUpdater, ); });