diff --git a/packages/shorebird_cli/README.md b/packages/shorebird_cli/README.md index c13ff0a6..e68840a1 100644 --- a/packages/shorebird_cli/README.md +++ b/packages/shorebird_cli/README.md @@ -268,6 +268,30 @@ shorebird channels list └─────────────┘ ``` +### Create Channels + +Create a new channel for your application using the `shorebird channels create` command: + +```bash +shorebird channels create --name MyChannel +``` + +**Sample** + +``` +shorebird channels create --name MyChannel + +🚀 Ready to create a new channel! + +📱 App ID: 485df03f-f522-4242-bf3d-31c0869bacac +📺 Channel: MyChannel + +Would you like to continue? (y/N) Yes +✓ Creating channel (0.2s) + +✅ New Channel Created! +``` + ### Usage ``` diff --git a/packages/shorebird_cli/lib/src/commands/channels/channels.dart b/packages/shorebird_cli/lib/src/commands/channels/channels.dart index 496e8f1c..86e60788 100644 --- a/packages/shorebird_cli/lib/src/commands/channels/channels.dart +++ b/packages/shorebird_cli/lib/src/commands/channels/channels.dart @@ -1,2 +1,3 @@ export 'channels_command.dart'; +export 'create_channels_command.dart'; export 'list_channels_command.dart'; diff --git a/packages/shorebird_cli/lib/src/commands/channels/channels_command.dart b/packages/shorebird_cli/lib/src/commands/channels/channels_command.dart index b61b6e21..7d1fa087 100644 --- a/packages/shorebird_cli/lib/src/commands/channels/channels_command.dart +++ b/packages/shorebird_cli/lib/src/commands/channels/channels_command.dart @@ -8,6 +8,7 @@ import 'package:shorebird_cli/src/commands/commands.dart'; class ChannelsCommand extends ShorebirdCommand { /// {@macro channels_command} ChannelsCommand({required super.logger}) { + addSubcommand(CreateChannelsCommand(logger: logger)); addSubcommand(ListChannelsCommand(logger: logger)); } diff --git a/packages/shorebird_cli/lib/src/commands/channels/create_channels_command.dart b/packages/shorebird_cli/lib/src/commands/channels/create_channels_command.dart new file mode 100644 index 00000000..1fda1dec --- /dev/null +++ b/packages/shorebird_cli/lib/src/commands/channels/create_channels_command.dart @@ -0,0 +1,95 @@ +import 'dart:async'; + +import 'package:mason_logger/mason_logger.dart'; +import 'package:shorebird_cli/src/command.dart'; +import 'package:shorebird_cli/src/shorebird_config_mixin.dart'; + +/// {@template create_channels_command} +/// `shorebird channels create` +/// Create a new channel for a Shorebird app. +/// {@endtemplate} +class CreateChannelsCommand extends ShorebirdCommand with ShorebirdConfigMixin { + /// {@macro create_channels_command} + CreateChannelsCommand({ + required super.logger, + super.buildCodePushClient, + super.auth, + }) { + argParser + ..addOption( + _appIdOption, + help: 'The app id to create a channel for.', + ) + ..addOption( + _channelNameOption, + help: 'The name of the channel to create.', + ); + } + + static const String _appIdOption = 'app-id'; + static const String _channelNameOption = 'name'; + + @override + String get description => 'Create a new channel for a Shorebird app.'; + + @override + String get name => 'create'; + + @override + Future? run() async { + final session = auth.currentSession; + if (session == null) { + logger.err('You must be logged in to view channels.'); + return ExitCode.noUser.code; + } + + final client = buildCodePushClient( + apiKey: session.apiKey, + hostedUri: hostedUri, + ); + + final appId = results[_appIdOption] as String? ?? getShorebirdYaml()?.appId; + if (appId == null) { + logger.err( + ''' +Could not find an app id. + +You must either specify an app id via the "--$_appIdOption" flag or run this command from within a directory with a valid "shorebird.yaml" file.''', + ); + return ExitCode.usage.code; + } + + final channel = results[_channelNameOption] as String; + + logger.info( + ''' + +${styleBold.wrap(lightGreen.wrap('🚀 Ready to create a new channel!'))} + +📱 App ID: ${lightCyan.wrap(appId)} +📺 Channel: ${lightCyan.wrap(channel)} +''', + ); + + final confirm = logger.confirm('Would you like to continue?'); + + if (!confirm) { + logger.info('Aborted.'); + return ExitCode.success.code; + } + + final progress = logger.progress('Creating channel'); + try { + await client.createChannel(appId: appId, channel: channel); + progress.complete(); + } catch (error) { + progress.fail(); + logger.err('$error'); + return ExitCode.software.code; + } + + logger.success('\n✅ New Channel Created!'); + + return ExitCode.success.code; + } +} diff --git a/packages/shorebird_cli/test/src/commands/channels/create_channels_command_test.dart b/packages/shorebird_cli/test/src/commands/channels/create_channels_command_test.dart new file mode 100644 index 00000000..1cebe5c6 --- /dev/null +++ b/packages/shorebird_cli/test/src/commands/channels/create_channels_command_test.dart @@ -0,0 +1,107 @@ +import 'package:args/args.dart'; +import 'package:mason_logger/mason_logger.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:shorebird_cli/src/auth/auth.dart'; +import 'package:shorebird_cli/src/auth/session.dart'; +import 'package:shorebird_cli/src/commands/commands.dart'; +import 'package:shorebird_code_push_client/shorebird_code_push_client.dart'; +import 'package:test/test.dart'; + +class _MockArgResults extends Mock implements ArgResults {} + +class _MockAuth extends Mock implements Auth {} + +class _MockCodePushClient extends Mock implements CodePushClient {} + +class _MockLogger extends Mock implements Logger {} + +class _MockProgress extends Mock implements Progress {} + +void main() { + group('create', () { + const session = Session(apiKey: 'test-api-key'); + const appId = 'test-app-id'; + const channelName = 'my-channel'; + const channel = Channel(id: 0, appId: appId, name: channelName); + + late ArgResults argResults; + late Auth auth; + late CodePushClient codePushClient; + late Logger logger; + late Progress progress; + late CreateChannelsCommand command; + + setUp(() { + argResults = _MockArgResults(); + auth = _MockAuth(); + codePushClient = _MockCodePushClient(); + logger = _MockLogger(); + progress = _MockProgress(); + command = CreateChannelsCommand( + auth: auth, + buildCodePushClient: ({required String apiKey, Uri? hostedUri}) { + return codePushClient; + }, + logger: logger, + )..testArgResults = argResults; + + when(() => argResults['app-id']).thenReturn(appId); + when(() => argResults['name']).thenReturn(channelName); + when(() => auth.currentSession).thenReturn(session); + when(() => logger.confirm(any())).thenReturn(true); + when(() => logger.progress(any())).thenReturn(progress); + }); + + test('description is correct', () { + expect( + command.description, + equals('Create a new channel for a Shorebird app.'), + ); + }); + + test('returns ExitCode.noUser when not logged in', () async { + when(() => auth.currentSession).thenReturn(null); + expect(await command.run(), ExitCode.noUser.code); + }); + + test('returns ExitCode.usage when app id is missing.', () async { + when(() => argResults['app-id']).thenReturn(null); + expect(await command.run(), ExitCode.usage.code); + }); + + test('returns ExitCode.success when user aborts', () async { + when(() => logger.confirm(any())).thenReturn(false); + expect(await command.run(), ExitCode.success.code); + verifyNever( + () => codePushClient.createChannel( + appId: any(named: 'appId'), + channel: any(named: 'channel'), + ), + ); + verify(() => logger.info('Aborted.')).called(1); + }); + + test('returns ExitCode.software when creating a channel fails', () async { + const error = 'oops something went wrong'; + when( + () => codePushClient.createChannel( + appId: any(named: 'appId'), + channel: any(named: 'channel'), + ), + ).thenThrow(error); + expect(await command.run(), ExitCode.software.code); + verify(() => logger.err(error)).called(1); + }); + + test('returns ExitCode.success on success', () async { + when( + () => codePushClient.createChannel( + appId: any(named: 'appId'), + channel: any(named: 'channel'), + ), + ).thenAnswer((_) async => channel); + expect(await command.run(), ExitCode.success.code); + verify(() => logger.success('\n✅ New Channel Created!')).called(1); + }); + }); +}