feat(shorebird_cli): shorebird channels list (#201)
This commit is contained in:
@@ -239,6 +239,29 @@ Build a new release of your application using the `shorebird build` command:
|
|||||||
shorebird build
|
shorebird build
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### List Channels
|
||||||
|
|
||||||
|
See all available channels for your application using the `shorebird channels list` command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
shorebird channels list
|
||||||
|
```
|
||||||
|
|
||||||
|
**Sample**
|
||||||
|
|
||||||
|
```
|
||||||
|
shorebird channels list
|
||||||
|
📱 App ID: 61fc9c16-3c4a-4825-a155-9765993614aa
|
||||||
|
📺 Channels
|
||||||
|
┌─────────────┐
|
||||||
|
│ Name │
|
||||||
|
├─────────────┤
|
||||||
|
│ stable │
|
||||||
|
├─────────────┤
|
||||||
|
│ development │
|
||||||
|
└─────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
### Usage
|
### Usage
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -252,15 +275,16 @@ Global options:
|
|||||||
--[no-]verbose Noisy logging, including all shell commands executed.
|
--[no-]verbose Noisy logging, including all shell commands executed.
|
||||||
|
|
||||||
Available commands:
|
Available commands:
|
||||||
apps Manage your Shorebird apps.
|
apps Manage your Shorebird apps.
|
||||||
build Build a new release of your application.
|
build Build a new release of your application.
|
||||||
init Initialize Shorebird.
|
channels Manage the channels for your Shorebird app.
|
||||||
login Login as a new Shorebird user.
|
init Initialize Shorebird.
|
||||||
logout Logout of the current Shorebird user
|
login Login as a new Shorebird user.
|
||||||
patch Publish new patches for a specific release to Shorebird.
|
logout Logout of the current Shorebird user
|
||||||
release Builds and submits your app to Shorebird.
|
patch Publish new patches for a specific release to Shorebird.
|
||||||
run Run the Flutter application.
|
release Builds and submits your app to Shorebird.
|
||||||
upgrade Upgrade your copy of Shorebird.
|
run Run the Flutter application.
|
||||||
|
upgrade Upgrade your copy of Shorebird.
|
||||||
|
|
||||||
Run "shorebird help <command>" for more information about a command.
|
Run "shorebird help <command>" for more information about a command.
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ class ShorebirdCliCommandRunner extends CompletionCommandRunner<int> {
|
|||||||
|
|
||||||
addCommand(AppsCommand(logger: _logger));
|
addCommand(AppsCommand(logger: _logger));
|
||||||
addCommand(BuildCommand(logger: _logger));
|
addCommand(BuildCommand(logger: _logger));
|
||||||
|
addCommand(ChannelsCommand(logger: _logger));
|
||||||
addCommand(InitCommand(logger: _logger));
|
addCommand(InitCommand(logger: _logger));
|
||||||
addCommand(LoginCommand(logger: _logger));
|
addCommand(LoginCommand(logger: _logger));
|
||||||
addCommand(LogoutCommand(logger: _logger));
|
addCommand(LogoutCommand(logger: _logger));
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export 'channels_command.dart';
|
||||||
|
export 'list_channels_command.dart';
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import 'package:shorebird_cli/src/command.dart';
|
||||||
|
import 'package:shorebird_cli/src/commands/commands.dart';
|
||||||
|
|
||||||
|
/// {@template channels_command}
|
||||||
|
/// `shorebird channels`
|
||||||
|
/// Manage the channels for your Shorebird app.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class ChannelsCommand extends ShorebirdCommand {
|
||||||
|
/// {@macro channels_command}
|
||||||
|
ChannelsCommand({required super.logger}) {
|
||||||
|
addSubcommand(ListChannelsCommand(logger: logger));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get description => 'Manage the channels for your Shorebird app.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get name => 'channels';
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:barbecue/barbecue.dart';
|
||||||
|
import 'package:mason_logger/mason_logger.dart';
|
||||||
|
import 'package:shorebird_cli/src/command.dart';
|
||||||
|
import 'package:shorebird_cli/src/shorebird_config_mixin.dart';
|
||||||
|
import 'package:shorebird_code_push_client/shorebird_code_push_client.dart';
|
||||||
|
|
||||||
|
/// {@template list_channels_command}
|
||||||
|
/// `shorebird channels list`
|
||||||
|
/// List all channels for a Shorebird app.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class ListChannelsCommand extends ShorebirdCommand with ShorebirdConfigMixin {
|
||||||
|
/// {@macro list_channels_command}
|
||||||
|
ListChannelsCommand({
|
||||||
|
required super.logger,
|
||||||
|
super.buildCodePushClient,
|
||||||
|
super.auth,
|
||||||
|
}) {
|
||||||
|
argParser.addOption(
|
||||||
|
_appIdOption,
|
||||||
|
help: 'The app id to list channels for.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const String _appIdOption = 'app-id';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get description => 'List all channels for a Shorebird app.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get name => 'list';
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<String> get aliases => ['ls'];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<int>? 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 List<Channel> channels;
|
||||||
|
try {
|
||||||
|
channels = await client.getChannels(appId: appId);
|
||||||
|
} catch (error) {
|
||||||
|
logger.err('$error');
|
||||||
|
return ExitCode.software.code;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
'''
|
||||||
|
📱 App ID: ${lightCyan.wrap(appId)}
|
||||||
|
📺 Channels''',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (channels.isEmpty) {
|
||||||
|
logger.info('(empty)');
|
||||||
|
return ExitCode.success.code;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(channels.prettyPrint());
|
||||||
|
|
||||||
|
return ExitCode.success.code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension on List<Channel> {
|
||||||
|
String prettyPrint() {
|
||||||
|
const cellStyle = CellStyle(
|
||||||
|
paddingLeft: 1,
|
||||||
|
paddingRight: 1,
|
||||||
|
borderBottom: true,
|
||||||
|
borderTop: true,
|
||||||
|
borderLeft: true,
|
||||||
|
borderRight: true,
|
||||||
|
);
|
||||||
|
return Table(
|
||||||
|
cellStyle: cellStyle,
|
||||||
|
header: const TableSection(
|
||||||
|
rows: [
|
||||||
|
Row(cells: [Cell('Name')])
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: TableSection(
|
||||||
|
rows: [
|
||||||
|
for (final channel in this) Row(cells: [Cell(channel.name)]),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
).render();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
export 'apps/apps.dart';
|
export 'apps/apps.dart';
|
||||||
export 'build_command.dart';
|
export 'build_command.dart';
|
||||||
|
export 'channels/channels.dart';
|
||||||
export 'init_command.dart';
|
export 'init_command.dart';
|
||||||
export 'login_command.dart';
|
export 'login_command.dart';
|
||||||
export 'logout_command.dart';
|
export 'logout_command.dart';
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ environment:
|
|||||||
dependencies:
|
dependencies:
|
||||||
archive: ^3.3.6
|
archive: ^3.3.6
|
||||||
args: ^2.3.1
|
args: ^2.3.1
|
||||||
|
barbecue: ^0.4.0
|
||||||
checked_yaml: ^2.0.2
|
checked_yaml: ^2.0.2
|
||||||
cli_completion: ^0.3.0
|
cli_completion: ^0.3.0
|
||||||
cli_util: ^0.4.0
|
cli_util: ^0.4.0
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
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('list', () {
|
||||||
|
const session = Session(apiKey: 'test-api-key');
|
||||||
|
const appId = 'test-app-id';
|
||||||
|
|
||||||
|
late ArgResults argResults;
|
||||||
|
late Auth auth;
|
||||||
|
late CodePushClient codePushClient;
|
||||||
|
late Logger logger;
|
||||||
|
late ListChannelsCommand command;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
argResults = _MockArgResults();
|
||||||
|
auth = _MockAuth();
|
||||||
|
codePushClient = _MockCodePushClient();
|
||||||
|
logger = _MockLogger();
|
||||||
|
command = ListChannelsCommand(
|
||||||
|
auth: auth,
|
||||||
|
buildCodePushClient: ({required String apiKey, Uri? hostedUri}) {
|
||||||
|
return codePushClient;
|
||||||
|
},
|
||||||
|
logger: logger,
|
||||||
|
)..testArgResults = argResults;
|
||||||
|
|
||||||
|
when(() => argResults['app-id']).thenReturn(appId);
|
||||||
|
when(() => auth.currentSession).thenReturn(session);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('description is correct', () {
|
||||||
|
expect(
|
||||||
|
command.description,
|
||||||
|
equals('List all channels 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.software when unable to get channels', () async {
|
||||||
|
when(
|
||||||
|
() => codePushClient.getChannels(appId: any(named: 'appId')),
|
||||||
|
).thenThrow(Exception());
|
||||||
|
expect(await command.run(), ExitCode.software.code);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns ExitCode.success when channels are empty', () async {
|
||||||
|
when(
|
||||||
|
() => codePushClient.getChannels(appId: any(named: 'appId')),
|
||||||
|
).thenAnswer((_) async => []);
|
||||||
|
expect(await command.run(), ExitCode.success.code);
|
||||||
|
verify(() => logger.info('(empty)')).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns ExitCode.success when channels are not empty', () async {
|
||||||
|
final channels = [
|
||||||
|
const Channel(id: 0, appId: appId, name: 'stable'),
|
||||||
|
const Channel(id: 1, appId: appId, name: 'development'),
|
||||||
|
];
|
||||||
|
when(
|
||||||
|
() => codePushClient.getChannels(appId: any(named: 'appId')),
|
||||||
|
).thenAnswer((_) async => channels);
|
||||||
|
expect(await command.run(), ExitCode.success.code);
|
||||||
|
verify(
|
||||||
|
() => logger.info(
|
||||||
|
'''
|
||||||
|
📱 App ID: ${lightCyan.wrap(appId)}
|
||||||
|
📺 Channels''',
|
||||||
|
),
|
||||||
|
).called(1);
|
||||||
|
verify(
|
||||||
|
() => logger.info(
|
||||||
|
'''
|
||||||
|
┌─────────────┐
|
||||||
|
│ Name │
|
||||||
|
├─────────────┤
|
||||||
|
│ stable │
|
||||||
|
├─────────────┤
|
||||||
|
│ development │
|
||||||
|
└─────────────┘''',
|
||||||
|
),
|
||||||
|
).called(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user