From 42a8799be0fc70ef9d98229d57fc5d80c67c2a2a Mon Sep 17 00:00:00 2001 From: Chima Precious Date: Wed, 4 Dec 2024 18:27:12 +0300 Subject: [PATCH] feat: Multi project linking via CLI (#117) * allow multi project linking & unlinking * fix lint issues * migrate old schema & auto update project name if changed * use --project arg as project id or slug * restore token validation * _ * _ * _ * _ * show all linked projects * _ * show globe error details * make token list use scope * make token delete use scope --- .../globe_cli/lib/src/command_runner.dart | 52 +++--- .../lib/src/commands/build_logs_command.dart | 2 + .../lib/src/commands/deploy_command.dart | 6 +- .../lib/src/commands/link_command.dart | 34 ++++ .../project/project_pause_command.dart | 2 + .../project/project_resume_command.dart | 2 + .../commands/token/token_delete_command.dart | 8 +- .../commands/token/token_list_command.dart | 61 +++---- .../lib/src/commands/unlink_command.dart | 9 +- packages/globe_cli/lib/src/utils/api.dart | 21 ++- packages/globe_cli/lib/src/utils/prompts.dart | 23 +-- packages/globe_cli/lib/src/utils/scope.dart | 158 +++++++++++++++--- packages/globe_cli/pubspec.lock | 74 +++++++- packages/globe_cli/pubspec.yaml | 2 + 14 files changed, 339 insertions(+), 115 deletions(-) diff --git a/packages/globe_cli/lib/src/command_runner.dart b/packages/globe_cli/lib/src/command_runner.dart index 7c54cd25..141a384b 100644 --- a/packages/globe_cli/lib/src/command_runner.dart +++ b/packages/globe_cli/lib/src/command_runner.dart @@ -128,15 +128,17 @@ class GlobeCliCommandRunner extends CompletionCommandRunner { GetIt.instance.registerSingleton(metadata); GetIt.instance.registerSingleton(scope); - final maybeToken = topLevelResults['token']; - final maybeProjectId = topLevelResults['project']; - final maybeOrgId = topLevelResults['org']; + final maybeProjectIdOrSlug = topLevelResults['project'] as String?; + final maybeToken = topLevelResults['token'] as String?; + + // Load the current project scope. + auth.loadSession(); + scope.loadScope(projectIdOrSlug: maybeProjectIdOrSlug); Organization? org; - Project? project; if (maybeToken != null) { - api.auth.loginWithApiToken(jwt: maybeToken as String); + api.auth.loginWithApiToken(jwt: maybeToken); org = await selectOrganization( logger: _logger, api: api, @@ -146,36 +148,26 @@ class GlobeCliCommandRunner extends CompletionCommandRunner { ); } - // Load the current project scope. - scope.loadScope(); - auth.loadSession(); - - final currentSession = auth.currentSession; + if (maybeProjectIdOrSlug != null && !scope.hasScope()) { + org ??= await selectOrganization(logger: _logger, api: api); + final projects = await api.getProjects(org: org.id); - if (maybeOrgId != null) { - if (currentSession == null) throw Exception('Auth required.'); - final orgs = await api.getOrganizations(); - org = orgs.firstWhere( - (org) => org.id == maybeOrgId, - orElse: () => throw Exception('Project #$maybeProjectId not found.'), + final selectedProject = projects.firstWhere( + (project) => + project.id == maybeProjectIdOrSlug || + project.slug == maybeProjectIdOrSlug, + orElse: () => + throw Exception('Project #$maybeProjectIdOrSlug not found.'), ); - } - - if (maybeProjectId != null) { - if (currentSession == null) throw Exception('Auth required.'); - if (org == null) throw Exception('Organization not found.'); - - final projects = await api.getProjects(org: org.id); - project = projects.firstWhere( - (project) => project.id == maybeProjectId, - orElse: () => throw Exception('Project #$maybeProjectId not found.'), + scope.setScope( + ScopeMetadata( + orgId: org.id, + projectId: selectedProject.id, + projectSlug: selectedProject.slug, + ), ); } - if (org != null && project != null) { - scope.setScope(orgId: org.id, projectId: project.id); - } - return await runCommand(topLevelResults) ?? ExitCode.success.code; // TODO(rrousselGit) why are we checking FormatExceptions here? } on FormatException catch (e, stackTrace) { diff --git a/packages/globe_cli/lib/src/commands/build_logs_command.dart b/packages/globe_cli/lib/src/commands/build_logs_command.dart index a2ed90fe..bba99b34 100644 --- a/packages/globe_cli/lib/src/commands/build_logs_command.dart +++ b/packages/globe_cli/lib/src/commands/build_logs_command.dart @@ -25,6 +25,8 @@ class BuildLogsCommand extends BaseGlobeCommand { Future run() async { requireAuth(); + await scope.selectOrLinkNewScope(); + final validated = await _validator(); final deploymentId = argResults!['deployment'] as String?; diff --git a/packages/globe_cli/lib/src/commands/deploy_command.dart b/packages/globe_cli/lib/src/commands/deploy_command.dart index c5672cd1..e01c0224 100644 --- a/packages/globe_cli/lib/src/commands/deploy_command.dart +++ b/packages/globe_cli/lib/src/commands/deploy_command.dart @@ -7,7 +7,6 @@ import '../command.dart'; import '../utils/api.dart'; import '../utils/archiver.dart'; import '../utils/logs.dart'; -import '../utils/prompts.dart'; /// `globe deploy` /// @@ -45,10 +44,7 @@ class DeployCommand extends BaseGlobeCommand { @override Future run() async { - // If there is no scope, ask the user to link the project. - if (!scope.hasScope()) { - await linkProject(logger: logger, api: api); - } + await scope.selectOrLinkNewScope(); final validated = await _validator(); if (validated.project.paused) { diff --git a/packages/globe_cli/lib/src/commands/link_command.dart b/packages/globe_cli/lib/src/commands/link_command.dart index 19cbe9c7..a87de925 100644 --- a/packages/globe_cli/lib/src/commands/link_command.dart +++ b/packages/globe_cli/lib/src/commands/link_command.dart @@ -1,3 +1,4 @@ +import 'package:cli_table/cli_table.dart'; import 'package:mason_logger/mason_logger.dart'; import '../command.dart'; @@ -7,6 +8,14 @@ import '../utils/prompts.dart'; /// /// Links the local project to a Globe project. class LinkCommand extends BaseGlobeCommand { + LinkCommand() { + argParser.addFlag( + 'show-all', + help: 'Show all linked projects', + negatable: false, + ); + } + @override String get description => 'Link this local project to a Globe project.'; @@ -15,6 +24,31 @@ class LinkCommand extends BaseGlobeCommand { @override Future run() async { + final showLinked = argResults!['show-all'] as bool; + if (showLinked) { + final linkedProjects = scope.workspace; + if (linkedProjects.isEmpty) return ExitCode.success.code; + + final table = Table( + header: [ + cyan.wrap('Project'), + cyan.wrap('Project ID'), + cyan.wrap('Organization ID'), + ], + columnWidths: [30, 30, 30], + ); + for (final project in linkedProjects) { + table.add([ + project.projectSlug, + project.projectId, + project.orgId, + ]); + } + + logger.info(table.toString()); + return ExitCode.success.code; + } + requireAuth(); await linkProject(logger: logger, api: api); diff --git a/packages/globe_cli/lib/src/commands/project/project_pause_command.dart b/packages/globe_cli/lib/src/commands/project/project_pause_command.dart index 6d192173..91c64865 100644 --- a/packages/globe_cli/lib/src/commands/project/project_pause_command.dart +++ b/packages/globe_cli/lib/src/commands/project/project_pause_command.dart @@ -22,6 +22,8 @@ class ProjectPauseCommand extends BaseGlobeCommand { FutureOr run() async { requireAuth(); + await scope.selectOrLinkNewScope(); + final validated = await _validator(); final projectSlug = validated.project.slug; final pauseProjectProgress = diff --git a/packages/globe_cli/lib/src/commands/project/project_resume_command.dart b/packages/globe_cli/lib/src/commands/project/project_resume_command.dart index 9f2053bd..960fb269 100644 --- a/packages/globe_cli/lib/src/commands/project/project_resume_command.dart +++ b/packages/globe_cli/lib/src/commands/project/project_resume_command.dart @@ -22,6 +22,8 @@ class ProjectResumeCommand extends BaseGlobeCommand { FutureOr run() async { requireAuth(); + await scope.selectOrLinkNewScope(); + final validated = await _validator(); final projectSlug = validated.project.slug; final pauseProjectProgress = diff --git a/packages/globe_cli/lib/src/commands/token/token_delete_command.dart b/packages/globe_cli/lib/src/commands/token/token_delete_command.dart index ccfde2d0..ce158930 100644 --- a/packages/globe_cli/lib/src/commands/token/token_delete_command.dart +++ b/packages/globe_cli/lib/src/commands/token/token_delete_command.dart @@ -4,7 +4,6 @@ import 'package:mason_logger/mason_logger.dart'; import '../../command.dart'; import '../../utils/api.dart'; -import '../../utils/prompts.dart'; class TokenDeleteCommand extends BaseGlobeCommand { TokenDeleteCommand() { @@ -23,8 +22,8 @@ class TokenDeleteCommand extends BaseGlobeCommand { @override FutureOr run() async { requireAuth(); + await scope.selectOrLinkNewScope(); - final organization = await selectOrganization(logger: logger, api: api); final tokenId = (argResults?['tokenId'] as String?) ?? logger.prompt('❓ Provide id for token:'); @@ -32,10 +31,7 @@ class TokenDeleteCommand extends BaseGlobeCommand { logger.progress('Deleting Token: ${cyan.wrap(tokenId)}'); try { - await api.deleteToken( - orgId: organization.id, - tokenId: tokenId, - ); + await api.deleteToken(orgId: scope.current!.orgId, tokenId: tokenId); deleteTokenProgress.complete('Token deleted'); } on ApiException catch (e) { deleteTokenProgress.fail('✗ Failed to delete token: ${e.message}'); diff --git a/packages/globe_cli/lib/src/commands/token/token_list_command.dart b/packages/globe_cli/lib/src/commands/token/token_list_command.dart index ff7bc7e0..d8bb2782 100644 --- a/packages/globe_cli/lib/src/commands/token/token_list_command.dart +++ b/packages/globe_cli/lib/src/commands/token/token_list_command.dart @@ -1,18 +1,12 @@ import 'dart:async'; +import 'package:cli_table/cli_table.dart'; import 'package:mason_logger/mason_logger.dart'; import '../../command.dart'; import '../../utils/api.dart'; -import '../../utils/prompts.dart'; class TokenListCommand extends BaseGlobeCommand { - TokenListCommand() { - argParser.addMultiOption( - 'project', - help: 'Specify projects(s) to list token for.', - ); - } @override String get description => 'List globe auth tokens for project(s)'; @@ -22,42 +16,49 @@ class TokenListCommand extends BaseGlobeCommand { @override FutureOr? run() async { requireAuth(); + await scope.selectOrLinkNewScope(); - final organization = await selectOrganization(logger: logger, api: api); - final projects = await selectProjects( - 'Select projects to list tokens for:', - organization, - logger: logger, - api: api, - scope: scope, - ids: argResults?['project'] as List?, - ); - - final projectNames = projects.map((e) => cyan.wrap(e.slug)).join(', '); - final listTokenProgress = logger.progress( - 'Listing Tokens for $projectNames', - ); + final projectName = scope.current!.projectSlug; + final listTokenProgress = + logger.progress('Listing tokens for $projectName'); try { final tokens = await api.listTokens( - orgId: organization.id, - projectUuids: projects.map((e) => e.id).toList(), + orgId: scope.current!.orgId, + projectUuids: [scope.current!.projectId], ); if (tokens.isEmpty) { - listTokenProgress.fail('No Tokens found for $projectNames'); + listTokenProgress.fail('No Tokens found for $projectName'); return ExitCode.success.code; } - String tokenLog(Token token) => ''' ----------------------------------- - ID: ${cyan.wrap(token.uuid)} - Name: ${token.name} - Expiry: ${token.expiresAt.toLocal()}'''; + final table = Table( + header: [ + cyan.wrap('ID'), + cyan.wrap('Name'), + cyan.wrap('Expiry'), + ], + columnWidths: [ + tokens.first.uuid.length + 2, + tokens.first.name.length + 2, + 25, + ], + ); + + for (final token in tokens) { + table.add([ + token.uuid, + token.name, + token.expiresAt.toLocal().toIso8601String(), + ]); + } listTokenProgress.complete( - 'Tokens for $projectNames\n${tokens.map(tokenLog).join('\n')}', + 'Found ${tokens.length} ${tokens.length > 1 ? 'tokens' : 'token'}', ); + logger.info(table.toString()); + return ExitCode.success.code; } on ApiException catch (e) { listTokenProgress.fail('✗ Failed to list tokens: ${e.message}'); diff --git a/packages/globe_cli/lib/src/commands/unlink_command.dart b/packages/globe_cli/lib/src/commands/unlink_command.dart index 754bd9c3..e058de51 100644 --- a/packages/globe_cli/lib/src/commands/unlink_command.dart +++ b/packages/globe_cli/lib/src/commands/unlink_command.dart @@ -15,7 +15,14 @@ class UnlinkCommand extends BaseGlobeCommand { @override Future run() async { requireAuth(); - scope.clear(); + + if (scope.hasScope()) { + scope.unlink(); + } else if (scope.workspace.isNotEmpty) { + final selected = await scope.selectOrLinkNewScope(canLinkNew: false); + scope.unlinkScope(selected); + } + logger.success( 'Project unlinked successfully. To link this project again, run ${cyan.wrap('globe link')}.', ); diff --git a/packages/globe_cli/lib/src/utils/api.dart b/packages/globe_cli/lib/src/utils/api.dart index 9b6d2e7c..eb6c840d 100644 --- a/packages/globe_cli/lib/src/utils/api.dart +++ b/packages/globe_cli/lib/src/utils/api.dart @@ -16,6 +16,9 @@ class ApiException implements Exception { final int statusCode; final String message; + + @override + String toString() => 'ApiException: [$statusCode] $message'; } class GlobeApi { @@ -108,29 +111,43 @@ class GlobeApi { } /// Gets all of the organizations that the current user is a member of. + List? _orgsCache; Future> getOrganizations() async { + if (_orgsCache != null && _orgsCache!.isNotEmpty) { + logger.detail('Cached API Request: GET /user/orgs'); + return _orgsCache!; + } + requireAuth(); logger.detail('API Request: GET /user/orgs'); final response = _handleResponse( await http.get(_buildUri('/user/orgs'), headers: headers), )! as List; - return response + return _orgsCache = response .cast>() .map(Organization.fromJson) .toList(); } /// Gets all of the projects that the current user is a member of. + List? _projectsCache; Future> getProjects({ required String org, }) async { + if (_projectsCache != null && _projectsCache!.isNotEmpty) { + logger.detail('Cached API Request: GET /orgs/$org/projects'); + return _projectsCache!; + } + requireAuth(); logger.detail('API Request: GET /orgs/$org/projects'); final response = _handleResponse( await http.get(_buildUri('/orgs/$org/projects'), headers: headers), )! as List; - return response.cast>().map(Project.fromJson).toList(); + + return _projectsCache = + response.cast>().map(Project.fromJson).toList(); } /// Creates a new project and returns it. diff --git a/packages/globe_cli/lib/src/utils/prompts.dart b/packages/globe_cli/lib/src/utils/prompts.dart index 92218804..7988fff1 100644 --- a/packages/globe_cli/lib/src/utils/prompts.dart +++ b/packages/globe_cli/lib/src/utils/prompts.dart @@ -20,17 +20,6 @@ Future linkProject({ required Logger logger, required GlobeApi api, }) async { - // TODO inject as function parameter - final scope = GetIt.I(); - - if (scope.hasScope()) { - if (!logger.confirm( - '❓ Project already linked, would you like to link to a different project?', - )) { - exitOverride(0); - } - } - try { final organization = await selectOrganization( logger: logger, @@ -43,9 +32,12 @@ Future linkProject({ api: api, ); - final result = scope.setScope( - orgId: organization.id, - projectId: project.id, + final result = GetIt.I().setScope( + ScopeMetadata( + orgId: organization.id, + projectId: project.id, + projectSlug: project.slug, + ), ); final projectUrl = Uri.parse(api.metadata.endpoint) @@ -116,6 +108,7 @@ Future selectProject( Organization organization, { required Logger logger, required GlobeApi api, + String message = '❓ Please select a project you want to link:', }) async { logger.detail('Fetching organization projects'); final projects = await api.getProjects(org: organization.id); @@ -293,7 +286,7 @@ Future selectProject( // Select a project or create a new one. final selectedProject = logger.chooseOne( - '❓ Please select a project you want to deploy to:', + message, choices: [ if (api.auth.currentSession?.authenticationMethod != AuthenticationMethod.apiToken) diff --git a/packages/globe_cli/lib/src/utils/scope.dart b/packages/globe_cli/lib/src/utils/scope.dart index 01a3a8c1..56c4ab59 100644 --- a/packages/globe_cli/lib/src/utils/scope.dart +++ b/packages/globe_cli/lib/src/utils/scope.dart @@ -2,8 +2,8 @@ import 'dart:convert'; import 'dart:io'; import 'package:args/args.dart'; +import 'package:collection/collection.dart'; import 'package:mason_logger/mason_logger.dart'; -import 'package:meta/meta.dart'; import 'package:path/path.dart' as p; import '../exit.dart'; @@ -28,10 +28,8 @@ class GlobeScope { ); } - /// The current project metadata, or `null` if the project has not been setup. - /// - /// Use [validate] instead if you're looking to read the current project metadata. - @protected + late final List workspace; + ScopeMetadata? get current => _current; ScopeMetadata? _current; @@ -42,19 +40,88 @@ class GlobeScope { final GlobeApi api; final GlobeMetadata metadata; + ScopeMetadata? _findScope(String projectIdOrSlug, {String? orgId}) { + return workspace.firstWhereOrNull( + (e) { + final hasIdOrSlug = + e.projectId == projectIdOrSlug || e.projectSlug == projectIdOrSlug; + if (orgId == null) return hasIdOrSlug; + + return hasIdOrSlug && e.orgId == orgId; + }, + ); + } + + void _writeWorkspaceToFile() { + _projectFile + ..createSync(recursive: true) + ..writeAsStringSync(const JsonEncoder.withIndent(' ').convert(workspace)); + } + /// Sets the current project scope. - ScopeMetadata setScope({required String orgId, required String projectId}) { - final result = _current = ScopeMetadata(orgId: orgId, projectId: projectId); - _projectFile.createSync(recursive: true); - _projectFile.writeAsStringSync(json.encode(_current!.toJson())); + ScopeMetadata setScope(ScopeMetadata scope) { + workspace + ..removeWhere((e) { + return e.projectId == scope.projectId && e.orgId == scope.orgId; + }) + ..add(scope); + + _writeWorkspaceToFile(); - return result; + return _current = scope; + } + + void unlinkScope(ScopeMetadata scope) { + if (workspace.isEmpty) return; + workspace.removeWhere( + (e) => e.orgId == scope.orgId && e.projectId == scope.projectId, + ); + + _writeWorkspaceToFile(); } bool hasScope() { return current != null; } + Future selectOrLinkNewScope({ + bool canLinkNew = true, + }) async { + if (hasScope()) return current!; + + final selectOrg = await selectOrganization(logger: logger, api: api); + const linkNewProjectSymbol = '__LINK_NEW_PROJECT'; + + final scopes = workspace.where((p) => p.orgId == selectOrg.id); + var selectedProject = linkNewProjectSymbol; + + if (scopes.length > 1) { + selectedProject = logger.chooseOne( + '🔺 Select project:', + choices: [ + ...scopes.map((o) => o.projectId), + if (canLinkNew) linkNewProjectSymbol, + ], + display: (choice) { + if (choice == linkNewProjectSymbol) { + return lightYellow.wrap('link new project +')!; + } + return scopes.firstWhere((o) => o.projectId == choice).projectSlug; + }, + ); + } else if (scopes.length == 1) { + return setScope(scopes.first); + } + + if (selectedProject != linkNewProjectSymbol) { + return setScope( + scopes.firstWhere((scope) => scope.projectId == selectedProject), + ); + } + + return linkProject(logger: logger, api: api); + } + Future _findOrg() async { final orgId = current?.orgId; if (orgId is! String) return selectOrganization(logger: logger, api: api); @@ -90,6 +157,17 @@ class GlobeScope { final organization = await _findOrg(); final project = await _findProject(organization); + // if name not matching, update the name locally + if (current!.projectSlug != project.slug) { + setScope( + ScopeMetadata( + orgId: organization.id, + projectId: project.id, + projectSlug: project.slug, + ), + ); + } + logger.detail('Validated scope: ${organization.slug}/${project.slug}'); return ScopeValidation( @@ -106,24 +184,45 @@ class GlobeScope { } /// Clears the current project metadata. - void clear() { + void unlink() { + if (_current == null) return; + unlinkScope(_current!); _current = null; - if (_projectFile.existsSync()) { - _projectFile.deleteSync(recursive: true); - } } /// Sets the current scope metadata. - void loadScope() { - if (_projectFile.existsSync()) { - try { - final contents = _projectFile.readAsStringSync(); - _current = ScopeMetadata.fromJson( - json.decode(contents) as Map, - ); - } catch (_) { - // TODO(rrousselGit) why are we catching and ignoring errors? - } + void loadScope({String? projectIdOrSlug}) { + if (!_projectFile.existsSync()) { + workspace = []; + return; + } + + final contents = _projectFile.readAsStringSync(); + if (contents.trim().isEmpty) { + workspace = []; + return; + } + + final jsonContent = json.decode(contents); + workspace = switch (jsonContent) { + Map() => [ + ScopeMetadata.fromJson({...jsonContent, 'projectSlug': ''}), + ], + List() => jsonContent + .map((e) => ScopeMetadata.fromJson(e as Map)) + .toList(), + _ => throw StateError('Invalid workspace schema'), + }; + + if (projectIdOrSlug != null) { + _current = _findScope(projectIdOrSlug); + } else if (workspace.length == 1) { + _current = workspace[0]; + } + + // migrate old schema to new schema + if (jsonContent is Map) { + _writeWorkspaceToFile(); } } } @@ -144,6 +243,7 @@ class ScopeMetadata { const ScopeMetadata({ required this.orgId, required this.projectId, + required this.projectSlug, }); /// Creates a new [ScopeMetadata] instance from the given [json]. @@ -151,6 +251,7 @@ class ScopeMetadata { return ScopeMetadata( orgId: json['orgId'] as String, projectId: json['projectId'] as String, + projectSlug: json['projectSlug'] as String, ); } @@ -160,6 +261,13 @@ class ScopeMetadata { /// The project ID, which belongs to the org. final String projectId; + /// The project Slug, + final String projectSlug; + /// Converts this [ScopeMetadata] instance to a JSON map. - Map toJson() => {'orgId': orgId, 'projectId': projectId}; + Map toJson() => { + 'orgId': orgId, + 'projectId': projectId, + 'projectSlug': projectSlug, + }; } diff --git a/packages/globe_cli/pubspec.lock b/packages/globe_cli/pubspec.lock index 35ffaefb..f0389407 100644 --- a/packages/globe_cli/pubspec.lock +++ b/packages/globe_cli/pubspec.lock @@ -22,6 +22,22 @@ packages: url: "https://pub.dev" source: hosted version: "6.8.0" + ansi_regex: + dependency: transitive + description: + name: ansi_regex + sha256: ca4f2b24a85e797a1512e1d3fe34d5f8429648f78e2268b6a8b5628c8430e643 + url: "https://pub.dev" + source: hosted + version: "0.1.2" + ansi_strip: + dependency: transitive + description: + name: ansi_strip + sha256: "9bb54e10962ac1de86b9b64a278a5b8965a28a2f741975eac7fe9fb0ebe1aaac" + url: "https://pub.dev" + source: hosted + version: "0.1.1+1" archive: dependency: "direct main" description: @@ -118,6 +134,22 @@ packages: url: "https://pub.dev" source: hosted version: "8.9.2" + chalkdart: + dependency: transitive + description: + name: chalkdart + sha256: "0b7ec5c6a6bafd1445500632c00c573722bd7736e491675d4ac3fe560bbd9cfe" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + characters: + dependency: transitive + description: + name: characters + sha256: "81269c8d3f45541082bfbb117bbc962cfc68b5197eb4c705a00db4ddf394e1c1" + url: "https://pub.dev" + source: hosted + version: "1.3.1" checked_yaml: dependency: transitive description: @@ -134,6 +166,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.0" + cli_table: + dependency: "direct main" + description: + name: cli_table + sha256: "61b61c6dbfa248d8ec9c65b1d97d1ec1952482765563533087ec550405def016" + url: "https://pub.dev" + source: hosted + version: "1.0.2" cli_util: dependency: "direct main" description: @@ -151,7 +191,7 @@ packages: source: hosted version: "4.10.0" collection: - dependency: transitive + dependency: "direct main" description: name: collection sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" @@ -190,6 +230,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.7" + east_asian_width: + dependency: transitive + description: + name: east_asian_width + sha256: a13c5487dab7ddbad48875789819f0ea38a61cbaaa3024ebe7b199521e6f5788 + url: "https://pub.dev" + source: hosted + version: "1.0.1" + emoji_regex: + dependency: transitive + description: + name: emoji_regex + sha256: "3a25dd4d16f98b6f76dc37cc9ae49b8511891ac4b87beac9443a1e9f4634b6c7" + url: "https://pub.dev" + source: hosted + version: "0.0.5" equatable: dependency: transitive description: @@ -549,6 +605,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + string_width: + dependency: transitive + description: + name: string_width + sha256: "0ea481fbb6d5e2d70937fea303d8cc9296048da107dffeecf2acb675c8b47e7f" + url: "https://pub.dev" + source: hosted + version: "0.1.5" term_glyph: dependency: transitive description: @@ -613,6 +677,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + wcwidth: + dependency: transitive + description: + name: wcwidth + sha256: "4e68ce25701e56647cb305ab6d8c75fce5e5196227bcb6ba6886513ac36474c2" + url: "https://pub.dev" + source: hosted + version: "0.0.4" web: dependency: transitive description: diff --git a/packages/globe_cli/pubspec.yaml b/packages/globe_cli/pubspec.yaml index cd5c2fb9..c5884662 100644 --- a/packages/globe_cli/pubspec.yaml +++ b/packages/globe_cli/pubspec.yaml @@ -20,6 +20,8 @@ dependencies: pub_updater: ^0.3.0 pubspec_parse: ^1.2.3 pool: ^1.5.1 + collection: ^1.19.1 + cli_table: ^1.0.2 dev_dependencies: build_runner: ^2.4.4