feat(shorebird_cli): shorebird apps create (#75)
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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!);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
class Session {
|
||||
const Session({required this.projectId, required this.apiKey});
|
||||
const Session({required this.apiKey});
|
||||
|
||||
factory Session.fromJson(Map<String, dynamic> json) {
|
||||
return Session(
|
||||
projectId: json['project_id'] as String,
|
||||
apiKey: json['api_key'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
final String projectId;
|
||||
final String apiKey;
|
||||
|
||||
Map<String, dynamic> toJson() => {'project_id': projectId, 'api_key': apiKey};
|
||||
Map<String, dynamic> toJson() => {'api_key': apiKey};
|
||||
}
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export 'apps_command.dart';
|
||||
export 'create_apps_command.dart';
|
||||
export 'list_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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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<int>? 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;
|
||||
}
|
||||
}
|
||||
@@ -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'))}''',
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<Session>()
|
||||
.having((s) => s.apiKey, 'apiKey', apiKey)
|
||||
.having((s) => s.projectId, 'projectId', projectId),
|
||||
isA<Session>().having((s) => s.apiKey, 'apiKey', apiKey),
|
||||
);
|
||||
expect(
|
||||
Auth().currentSession,
|
||||
isA<Session>()
|
||||
.having((s) => s.apiKey, 'apiKey', apiKey)
|
||||
.having((s) => s.projectId, 'projectId', projectId),
|
||||
isA<Session>().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<Session>()
|
||||
.having((s) => s.apiKey, 'apiKey', apiKey)
|
||||
.having((s) => s.projectId, 'projectId', projectId),
|
||||
isA<Session>().having((s) => s.apiKey, 'apiKey', apiKey),
|
||||
);
|
||||
|
||||
auth.logout();
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -28,10 +28,7 @@ class _FakeCommandRunner extends Fake implements CommandRunner<int> {
|
||||
|
||||
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 = '''
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user