diff --git a/packages/shorebird_cli/README.md b/packages/shorebird_cli/README.md index c0b75d3c..c20b4fd2 100644 --- a/packages/shorebird_cli/README.md +++ b/packages/shorebird_cli/README.md @@ -74,6 +74,26 @@ shorebird logout ✓ Logging out of shorebird.dev (1ms) ``` +### Create App + +To create an app use the `shorebird apps create` command. An app-id can be specified as a CLI option but shorebird will default to the product_id defined in the `shorebird.yaml` + +```bash +# Create an app using default app id +shorebird apps create + +# Create an app using an explicit app id +shorebird apps create --app-id "my-app-id" +``` + +**Sample** + +``` +shorebird apps create +? Enter the App ID (default-id) my-app-id +Created new app: my-app-id +``` + ### List Apps List all existing apps in Shorebird using the `shorebird apps list` command: diff --git a/packages/shorebird_cli/lib/src/auth/auth.dart b/packages/shorebird_cli/lib/src/auth/auth.dart index c253a5f2..ae0d896d 100644 --- a/packages/shorebird_cli/lib/src/auth/auth.dart +++ b/packages/shorebird_cli/lib/src/auth/auth.dart @@ -13,8 +13,8 @@ class Auth { static const _applicationName = 'shorebird'; static const _sessionFileName = 'shorebird-session.json'; - void login({required String projectId, required String apiKey}) { - _session = Session(projectId: projectId, apiKey: apiKey); + void login({required String apiKey}) { + _session = Session(apiKey: apiKey); _flushSession(_session!); } diff --git a/packages/shorebird_cli/lib/src/auth/session.dart b/packages/shorebird_cli/lib/src/auth/session.dart index 6cbb1fa8..433f2357 100644 --- a/packages/shorebird_cli/lib/src/auth/session.dart +++ b/packages/shorebird_cli/lib/src/auth/session.dart @@ -1,15 +1,13 @@ class Session { - const Session({required this.projectId, required this.apiKey}); + const Session({required this.apiKey}); factory Session.fromJson(Map json) { return Session( - projectId: json['project_id'] as String, apiKey: json['api_key'] as String, ); } - final String projectId; final String apiKey; - Map toJson() => {'project_id': projectId, 'api_key': apiKey}; + Map toJson() => {'api_key': apiKey}; } diff --git a/packages/shorebird_cli/lib/src/commands/apps/apps.dart b/packages/shorebird_cli/lib/src/commands/apps/apps.dart index 4e65de10..d5931b37 100644 --- a/packages/shorebird_cli/lib/src/commands/apps/apps.dart +++ b/packages/shorebird_cli/lib/src/commands/apps/apps.dart @@ -1,2 +1,3 @@ export 'apps_command.dart'; +export 'create_apps_command.dart'; export 'list_apps_command.dart'; diff --git a/packages/shorebird_cli/lib/src/commands/apps/apps_command.dart b/packages/shorebird_cli/lib/src/commands/apps/apps_command.dart index 79452242..e9c7d523 100644 --- a/packages/shorebird_cli/lib/src/commands/apps/apps_command.dart +++ b/packages/shorebird_cli/lib/src/commands/apps/apps_command.dart @@ -1,5 +1,5 @@ import 'package:shorebird_cli/src/command.dart'; -import 'package:shorebird_cli/src/commands/apps/list_apps_command.dart'; +import 'package:shorebird_cli/src/commands/commands.dart'; /// {@template apps_command} /// @@ -9,6 +9,7 @@ import 'package:shorebird_cli/src/commands/apps/list_apps_command.dart'; class AppsCommand extends ShorebirdCommand { /// {@macro apps_command} AppsCommand({required super.logger}) { + addSubcommand(CreateAppCommand(logger: logger)); addSubcommand(ListAppsCommand(logger: logger)); } diff --git a/packages/shorebird_cli/lib/src/commands/apps/create_apps_command.dart b/packages/shorebird_cli/lib/src/commands/apps/create_apps_command.dart new file mode 100644 index 00000000..b9779120 --- /dev/null +++ b/packages/shorebird_cli/lib/src/commands/apps/create_apps_command.dart @@ -0,0 +1,73 @@ +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_app_command} +/// +/// `shorebird apps create` +/// Create a new app on Shorebird. +/// {@endtemplate} +class CreateAppCommand extends ShorebirdCommand with ShorebirdConfigMixin { + /// {@macro create_app_command} + CreateAppCommand({ + required super.logger, + super.buildCodePushClient, + super.auth, + }) { + argParser.addOption( + 'app-id', + help: ''' +The unique application identifier. +Defaults to the product_id in "shorebird.yaml".''', + ); + } + + @override + String get description => 'Create a new app on Shorebird.'; + + @override + String get name => 'create'; + + @override + Future? run() async { + final session = auth.currentSession; + if (session == null) { + logger.err('You must be logged in.'); + return ExitCode.noUser.code; + } + + final appId = results['app-id'] as String?; + late final String productId; + + if (appId == null) { + String? defaultProductId; + try { + defaultProductId = getShorebirdYaml()?.productId; + } catch (_) {} + + productId = logger.prompt( + '${lightGreen.wrap('?')} Enter the App ID', + defaultValue: defaultProductId, + ); + } else { + productId = appId; + } + + final client = buildCodePushClient(apiKey: session.apiKey); + + try { + await client.createApp(productId: productId); + } catch (error) { + logger.err('Unable to create app\n$error'); + return ExitCode.software.code; + } + + logger.info( + '${lightGreen.wrap('Created new app: ${cyan.wrap(productId)}')}', + ); + + return ExitCode.success.code; + } +} diff --git a/packages/shorebird_cli/lib/src/commands/init_command.dart b/packages/shorebird_cli/lib/src/commands/init_command.dart index 21ab4a9f..cb0b19ba 100644 --- a/packages/shorebird_cli/lib/src/commands/init_command.dart +++ b/packages/shorebird_cli/lib/src/commands/init_command.dart @@ -72,9 +72,10 @@ ${lightGreen.wrap('🐦 Shorebird initialized successfully!')} Reference the following commands to get started: +✨ To create a new app use: "${lightCyan.wrap('shorebird apps create')}". 🚙 To run your project use: "${lightCyan.wrap('shorebird run')}". 📦 To build your project use: "${lightCyan.wrap('shorebird build')}". -🚀 To publish a new update use: "${lightCyan.wrap('shorebird publish')}". +🚀 To publish an update use: "${lightCyan.wrap('shorebird publish')}". For more information about Shorebird, visit ${link(uri: Uri.parse('https://shorebird.dev'))}''', ); diff --git a/packages/shorebird_cli/lib/src/commands/login_command.dart b/packages/shorebird_cli/lib/src/commands/login_command.dart index 159f2016..aedf6217 100644 --- a/packages/shorebird_cli/lib/src/commands/login_command.dart +++ b/packages/shorebird_cli/lib/src/commands/login_command.dart @@ -31,7 +31,7 @@ class LoginCommand extends ShorebirdCommand { ); final loginProgress = logger.progress('Logging into shorebird.dev'); try { - auth.login(projectId: 'example', apiKey: apiKey); + auth.login(apiKey: apiKey); loginProgress.complete(); logger.success('You are now logged in.'); return ExitCode.success.code; diff --git a/packages/shorebird_cli/test/src/auth/auth_test.dart b/packages/shorebird_cli/test/src/auth/auth_test.dart index ea1170fa..b6b1c2f4 100644 --- a/packages/shorebird_cli/test/src/auth/auth_test.dart +++ b/packages/shorebird_cli/test/src/auth/auth_test.dart @@ -5,7 +5,6 @@ import 'package:test/test.dart'; void main() { group('Auth', () { const apiKey = 'test-api-key'; - const projectId = 'test-project-id'; late Auth auth; @@ -15,30 +14,24 @@ void main() { group('login', () { test('should set the current session', () { - auth.login(apiKey: apiKey, projectId: projectId); + auth.login(apiKey: apiKey); expect( auth.currentSession, - isA() - .having((s) => s.apiKey, 'apiKey', apiKey) - .having((s) => s.projectId, 'projectId', projectId), + isA().having((s) => s.apiKey, 'apiKey', apiKey), ); expect( Auth().currentSession, - isA() - .having((s) => s.apiKey, 'apiKey', apiKey) - .having((s) => s.projectId, 'projectId', projectId), + isA().having((s) => s.apiKey, 'apiKey', apiKey), ); }); }); group('logout', () { test('clears session and wipes state', () { - auth.login(apiKey: apiKey, projectId: projectId); + auth.login(apiKey: apiKey); expect( auth.currentSession, - isA() - .having((s) => s.apiKey, 'apiKey', apiKey) - .having((s) => s.projectId, 'projectId', projectId), + isA().having((s) => s.apiKey, 'apiKey', apiKey), ); auth.logout(); diff --git a/packages/shorebird_cli/test/src/commands/apps/create_apps_command_test.dart b/packages/shorebird_cli/test/src/commands/apps/create_apps_command_test.dart new file mode 100644 index 00000000..025ce54a --- /dev/null +++ b/packages/shorebird_cli/test/src/commands/apps/create_apps_command_test.dart @@ -0,0 +1,89 @@ +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 {} + +void main() { + group('create', () { + const apiKey = 'test-api-key'; + const productId = 'example'; + const session = Session(apiKey: apiKey); + + late ArgResults argResults; + late Auth auth; + late Logger logger; + late CodePushClient codePushClient; + late CreateAppCommand command; + + setUp(() { + argResults = _MockArgResults(); + auth = _MockAuth(); + logger = _MockLogger(); + codePushClient = _MockCodePushClient(); + command = CreateAppCommand( + auth: auth, + buildCodePushClient: ({required String apiKey}) => codePushClient, + logger: logger, + )..testArgResults = argResults; + + when(() => auth.currentSession).thenReturn(session); + }); + + test('returns correct description', () { + expect(command.description, equals('Create a new app on Shorebird.')); + }); + + test('returns no user error when not logged in', () async { + when(() => auth.currentSession).thenReturn(null); + final result = await command.run(); + expect(result, ExitCode.noUser.code); + }); + + test('prompts for app-id when not provided', () async { + when(() => logger.prompt(any())).thenReturn(productId); + await command.run(); + verify(() => logger.prompt(any())).called(1); + verify(() => codePushClient.createApp(productId: productId)).called(1); + }); + + test('uses provided app-id when provided', () async { + when(() => argResults['app-id']).thenReturn(productId); + await command.run(); + verifyNever(() => logger.prompt(any())); + verify(() => codePushClient.createApp(productId: productId)).called(1); + }); + + test('returns success when app is created', () async { + when(() => argResults['app-id']).thenReturn(productId); + when( + () => codePushClient.createApp(productId: productId), + ).thenAnswer((_) async {}); + final result = await command.run(); + expect(result, ExitCode.success.code); + }); + + test('returns software error when app creation fails', () async { + when(() => argResults['app-id']).thenReturn(productId); + when( + () => codePushClient.createApp(productId: productId), + ).thenThrow(Exception()); + final result = await command.run(); + expect(result, ExitCode.software.code); + verify( + () => logger.err(any(that: contains('Unable to create app'))), + ).called(1); + }); + }); +} diff --git a/packages/shorebird_cli/test/src/commands/apps/list_apps_command_test.dart b/packages/shorebird_cli/test/src/commands/apps/list_apps_command_test.dart index e384465c..feca4c97 100644 --- a/packages/shorebird_cli/test/src/commands/apps/list_apps_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/apps/list_apps_command_test.dart @@ -14,10 +14,7 @@ class _MockLogger extends Mock implements Logger {} void main() { group('list', () { - const session = Session( - projectId: 'test-project-id', - apiKey: 'test-api-key', - ); + const session = Session(apiKey: 'test-api-key'); late Auth auth; late CodePushClient codePushClient; diff --git a/packages/shorebird_cli/test/src/commands/build_command_test.dart b/packages/shorebird_cli/test/src/commands/build_command_test.dart index 9404ab4f..98ac8249 100644 --- a/packages/shorebird_cli/test/src/commands/build_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/build_command_test.dart @@ -24,10 +24,7 @@ class _MockCodePushClient extends Mock implements CodePushClient {} void main() { group('build', () { - const session = Session( - apiKey: 'test-api-key', - projectId: 'test-project-id', - ); + const session = Session(apiKey: 'test-api-key'); late ArgResults argResults; late Auth auth; diff --git a/packages/shorebird_cli/test/src/commands/login_command_test.dart b/packages/shorebird_cli/test/src/commands/login_command_test.dart index eb65ee33..108393c7 100644 --- a/packages/shorebird_cli/test/src/commands/login_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/login_command_test.dart @@ -14,8 +14,7 @@ class _MockProgress extends Mock implements Progress {} void main() { group('login', () { const apiKey = 'test-api-key'; - const projectId = 'example'; - const session = Session(apiKey: apiKey, projectId: projectId); + const session = Session(apiKey: apiKey); late Logger logger; late Auth auth; @@ -46,19 +45,14 @@ void main() { when(() => logger.prompt(any())).thenReturn(apiKey); when(() => auth.currentSession).thenReturn(null); when( - () => auth.login( - apiKey: any(named: 'apiKey'), - projectId: any(named: 'projectId'), - ), + () => auth.login(apiKey: any(named: 'apiKey')), ).thenThrow(error); final result = await loginCommand.run(); expect(result, equals(ExitCode.software.code)); verify(() => logger.progress('Logging into shorebird.dev')).called(1); - verify( - () => auth.login(apiKey: apiKey, projectId: projectId), - ).called(1); + verify(() => auth.login(apiKey: apiKey)).called(1); verify(() => logger.err(error.toString())).called(1); }); @@ -66,19 +60,14 @@ void main() { when(() => logger.prompt(any())).thenReturn(apiKey); when(() => auth.currentSession).thenReturn(null); when( - () => auth.login( - apiKey: any(named: 'apiKey'), - projectId: any(named: 'projectId'), - ), + () => auth.login(apiKey: any(named: 'apiKey')), ).thenAnswer((_) async {}); final result = await loginCommand.run(); expect(result, equals(ExitCode.success.code)); verify(() => logger.progress('Logging into shorebird.dev')).called(1); - verify( - () => auth.login(apiKey: apiKey, projectId: projectId), - ).called(1); + verify(() => auth.login(apiKey: apiKey)).called(1); verify( () => logger.success('You are now logged in.'), ).called(1); diff --git a/packages/shorebird_cli/test/src/commands/logout_command_test.dart b/packages/shorebird_cli/test/src/commands/logout_command_test.dart index 213180b8..dc92cfe5 100644 --- a/packages/shorebird_cli/test/src/commands/logout_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/logout_command_test.dart @@ -35,7 +35,7 @@ void main() { }); test('exits with code 0 when logged out successfully', () async { - const session = Session(apiKey: 'test-api-key', projectId: 'example'); + const session = Session(apiKey: 'test-api-key'); when(() => auth.currentSession).thenReturn(session); final progress = _MockProgress(); diff --git a/packages/shorebird_cli/test/src/commands/publish_command_test.dart b/packages/shorebird_cli/test/src/commands/publish_command_test.dart index 785d14a5..6d336a94 100644 --- a/packages/shorebird_cli/test/src/commands/publish_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/publish_command_test.dart @@ -28,10 +28,7 @@ class _FakeCommandRunner extends Fake implements CommandRunner { void main() { group('publish', () { - const session = Session( - projectId: 'test-project-id', - apiKey: 'test-api-key', - ); + const session = Session(apiKey: 'test-api-key'); const productId = 'test-product-id'; const version = '1.2.3'; const pubspecYamlContent = ''' diff --git a/packages/shorebird_cli/test/src/commands/run_command_test.dart b/packages/shorebird_cli/test/src/commands/run_command_test.dart index 3ea2cbcd..8efd20a3 100644 --- a/packages/shorebird_cli/test/src/commands/run_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/run_command_test.dart @@ -28,10 +28,7 @@ class _MockCodePushClient extends Mock implements CodePushClient {} void main() { group('run', () { - const session = Session( - apiKey: 'test-api-key', - projectId: 'test-project-id', - ); + const session = Session(apiKey: 'test-api-key'); late ArgResults argResults; late Auth auth;