Repository: VeryGoodOpenSource/very_good_cli (dart)

Issue:
feat: Support plain dart package generation
**Description**
I was thinking it would be nice to be able to generate an opinionated Dart package starter project that's already hooked up to use [very_good_analysis](https://pub.dev/packages/very_good_analysis), [mocktail](https://pub.dev/packages/mocktail), [test_coverage](https://pub.dev/packages/test_coverage) and/or anything else that would be helpful to use by default.

For reference, this is the usual Dart command which generates a new Dart package:

`dart create --template package-simple my_package`

I realized I would be repeating the same steps as the package creation steps [here](https://github.com/definitelyokay/app_http_client#create-a-dart-package) for every Dart package I wanted to make, hence this suggestion.

Pull request description:
<!--
  Thanks for contributing!

  Provide a description of your changes below and a general summary in the title

  Please look at the following checklist to ensure that your PR can be accepted quickly:
-->

## Description

Add dart and flutter package MasonBundles and adapt CLI to support specifying the template to use for generation

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [X] ✨ New feature (non-breaking change which adds functionality)
- [ ] 🛠️ Bug fix (non-breaking change which fixes an issue)
- [ ] ❌ Breaking change (fix or feature that would cause existing functionality to change)
- [ ] 🧹 Code refactor
- [ ] ✅ Build configuration change
- [ ] 📝 Documentation
- [ ] 🗑️ Chore

Patch:
diff --git a/README.md b/README.md
index b307907a8..6340cd166 100644
--- a/README.md
+++ b/README.md
@@ -35,9 +35,10 @@ Creates a new very good flutter project in the specified directory.
 
 Usage: very_good create <output directory>
 -h, --help            Print this usage information.
-    --project-name    The project name for this new Flutter project. This must be a valid dart package name.
-    --org-name        The organization for this new Flutter project.
+    --project-name    The project name for this new project. This must be a valid dart package name.
+    --org-name        The organization for this new project.
                       (defaults to "com.example.verygoodcore")
+-t, --template        The template to use to generate this new project.
 ```
 
 ### What's Included? 📦
diff --git a/lib/src/commands/create.dart b/lib/src/commands/create.dart
index 8f07d42a5..f1535a445 100644
--- a/lib/src/commands/create.dart
+++ b/lib/src/commands/create.dart
@@ -1,6 +1,5 @@
 import 'package:args/args.dart';
 import 'package:args/command_runner.dart';
-import 'package:io/ansi.dart';
 import 'package:io/io.dart';
 import 'package:mason/mason.dart';
 import 'package:meta/meta.dart';
@@ -9,10 +8,12 @@ import 'package:universal_io/io.dart';
 import 'package:usage/usage_io.dart';
 import 'package:very_good_analysis/very_good_analysis.dart';
 import 'package:very_good_cli/src/command_runner.dart';
-import 'package:very_good_cli/src/flutter_cli.dart';
-import 'package:very_good_cli/src/templates/very_good_core_bundle.dart';
+import 'package:very_good_cli/src/templates/templates.dart';
 
 const _defaultOrgName = 'com.example.verygoodcore';
+final _defaultTemplate = CoreTemplate();
+
+final _templates = [_defaultTemplate, DartPkgTemplate(), FlutterPkgTemplate()];
 
 // A valid Dart identifier that can be used for a package, i.e. no
 // capital letters.
@@ -24,7 +25,7 @@ final RegExp _orgNameRegExp = RegExp(r'^[a-zA-Z][\w-]*(\.[a-zA-Z][\w-]*)+$');
 typedef GeneratorBuilder = Future<MasonGenerator> Function(MasonBundle);
 
 /// {@template create_command}
-/// `very_good create` command creates a new very good flutter app.
+/// `very_good create` command creates code from various built-in templates.
 /// {@endtemplate}
 class CreateCommand extends Command<int> {
   /// {@macro create_command}
@@ -46,6 +47,20 @@ class CreateCommand extends Command<int> {
         'org-name',
         help: 'The organization for this new Flutter project.',
         defaultsTo: 'com.example.verygoodcore',
+      )
+      ..addOption(
+        'template',
+        abbr: 't',
+        help: 'The template used to generate this new project.',
+        defaultsTo: _defaultTemplate.name,
+        allowed: _templates.map((element) => element.name).toList(),
+        allowedHelp: _templates.fold<Map<String, String>>(
+          {},
+          (previousValue, element) => {
+            ...previousValue,
+            element.name: element.help,
+          },
+        ),
       );
   }
 
@@ -77,24 +92,16 @@ class CreateCommand extends Command<int> {
     final outputDirectory = _outputDirectory;
     final projectName = _projectName;
     final orgName = _orgName;
+    final template = _template;
     final generateDone = _logger.progress('Bootstrapping');
-    final generator = await _generator(veryGoodCoreBundle);
+    final generator = await _generator(template.bundle);
     final fileCount = await generator.generate(
       DirectoryGeneratorTarget(outputDirectory, _logger),
       vars: {'project_name': projectName, 'org_name': orgName},
     );
     generateDone('Generated $fileCount file(s)');
 
-    final isFlutterInstalled = await Flutter.installed();
-    if (isFlutterInstalled) {
-      final installDependenciesDone = _logger.progress(
-        'Running "flutter packages get" in ${outputDirectory.path}',
-      );
-      await Flutter.packagesGet(outputDirectory.path);
-      installDependenciesDone();
-    }
-
-    _logSummary();
+    await template.onGenerateComplete(_logger, outputDirectory);
 
     unawaited(_analytics.sendEvent(
       'create',
@@ -106,25 +113,6 @@ class CreateCommand extends Command<int> {
     return ExitCode.success.code;
   }
 
-  void _logSummary() {
-    _logger
-      ..info('\n')
-      ..alert('Created a Very Good App! 🦄')
-      ..info('\n')
-      ..info(
-        lightGray.wrap(
-          '''+----------------------------------------------------+
-| Looking for more features?                         |
-| We have an enterprise-grade solution for companies |
-| called Very Good Start.                            |
-|                                                    |
-| For more info visit:                               |
-| https://verygood.ventures/solution/very-good-start |
-+----------------------------------------------------+''',
-        ),
-      );
-  }
-
   /// Gets the project name.
   ///
   /// Uses the current directory path name
@@ -151,6 +139,15 @@ class CreateCommand extends Command<int> {
     return org;
   }
 
+  Template get _template {
+    final templateName = _argResults['template'] as String?;
+
+    return _templates.firstWhere(
+      (element) => element.name == templateName,
+      orElse: () => _defaultTemplate,
+    );
+  }
+
   void _validateOrgName(String name) {
     final isValidOrgName = _isValidOrgName(name);
     if (!isValidOrgName) {
diff --git a/lib/src/flutter_cli.dart b/lib/src/flutter_cli.dart
index 3570efff0..a5b6f5ebd 100644
--- a/lib/src/flutter_cli.dart
+++ b/lib/src/flutter_cli.dart
@@ -7,6 +7,11 @@ class Flutter {
     return _Cmd.run('flutter', ['packages', 'get'], workingDirectory: cwd);
   }
 
+  /// Install dart dependencies (`flutter pub get`).
+  static Future<void> pubGet([String? cwd]) {
+    return _Cmd.run('flutter', ['pub', 'get'], workingDirectory: cwd);
+  }
+
   /// Determine whether flutter is installed
   static Future<bool> installed() async {
     try {
@@ -18,7 +23,9 @@ class Flutter {
   }
 }
 
+/// Abstraction for running commands via command-line.
 class _Cmd {
+  /// Runs the specified [cmd] with the provided [args].
   static Future<ProcessResult> run(
     String cmd,
     List<String> args, {
diff --git a/lib/src/templates/dart_package_bundle.dart b/lib/src/templates/dart_package_bundle.dart
new file mode 100644
index 000000000..bc7665bfe
--- /dev/null
+++ b/lib/src/templates/dart_package_bundle.dart
@@ -0,0 +1,54 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+// ignore_for_file: prefer_single_quotes, public_member_api_docs, lines_longer_than_80_chars
+
+import 'package:mason/mason.dart';
+
+final dartPackageBundle = MasonBundle.fromJson({
+  "files": [
+    {
+      "path": "test/{{project_name}}_test.dart",
+      "data":
+          "Ly8gaWdub3JlX2Zvcl9maWxlOiBwcmVmZXJfY29uc3RfY29uc3RydWN0b3JzCmltcG9ydCAncGFja2FnZTp0ZXN0L3Rlc3QuZGFydCc7CmltcG9ydCAncGFja2FnZTp7e3Byb2plY3RfbmFtZX19L3t7cHJvamVjdF9uYW1lfX0uZGFydCc7Cgp2b2lkIG1haW4oKSB7CiAgZ3JvdXAoJ3t7IyBwYXNjYWxDYXNlfX17e3Byb2plY3RfbmFtZX19e3svIHBhc2NhbENhc2V9fScsICgpIHsKICAgIHRlc3QoJ2NhbiBiZSBpbnN0YW50aWF0ZWQnLCAoKSB7CiAgICAgIGV4cGVjdCh7eyMgcGFzY2FsQ2FzZX19e3twcm9qZWN0X25hbWV9fXt7LyBwYXNjYWxDYXNlfX0oKSwgaXNOb3ROdWxsKTsKICAgIH0pOwogIH0pOwp9",
+      "type": "text"
+    },
+    {
+      "path": "README.md",
+      "data":
+          "IyB7e3Byb2plY3RfbmFtZX19CgpbIVtzdHlsZTogdmVyeSBnb29kIGFuYWx5c2lzXVt2ZXJ5X2dvb2RfYW5hbHlzaXNfYmFkZ2VdXVt2ZXJ5X2dvb2RfYW5hbHlzaXNfbGlua10KWyFbTGljZW5zZTogTUlUXVtsaWNlbnNlX2JhZGdlXV1bbGljZW5zZV9saW5rXQoKe3tkZXNjcmlwdGlvbn19CgpbbGljZW5zZV9iYWRnZV06IGh0dHBzOi8vaW1nLnNoaWVsZHMuaW8vYmFkZ2UvbGljZW5zZS1NSVQtYmx1ZS5zdmcKW2xpY2Vuc2VfbGlua106IGh0dHBzOi8vb3BlbnNvdXJjZS5vcmcvbGljZW5zZXMvTUlUClt2ZXJ5X2dvb2RfYW5hbHlzaXNfYmFkZ2VdOiBodHRwczovL2ltZy5zaGllbGRzLmlvL2JhZGdlL3N0eWxlLXZlcnlfZ29vZF9hbmFseXNpcy1CMjJDODkuc3ZnClt2ZXJ5X2dvb2RfYW5hbHlzaXNfbGlua106IGh0dHBzOi8vcHViLmRldi9wYWNrYWdlcy92ZXJ5X2dvb2RfYW5hbHlzaXM=",
+      "type": "text"
+    },
+    {
+      "path": "pubspec.yaml",
+      "data":
+          "bmFtZToge3twcm9qZWN0X25hbWV9fQpkZXNjcmlwdGlvbjoge3tkZXNjcmlwdGlvbn19CnZlcnNpb246IDEuMC4wKzEKcHVibGlzaF90bzogbm9uZSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAoKZW52aXJvbm1lbnQ6CiAgc2RrOiAiPj0yLjEyLjAgPDMuMC4wIgoKZGV2X2RlcGVuZGVuY2llczoKICB0ZXN0OiBeMS4xNy4wCiAgdmVyeV9nb29kX2FuYWx5c2lzOiBeMi4xLjA=",
+      "type": "text"
+    },
+    {
+      "path": ".gitignore",
+      "data":
+          "IyBTZWUgaHR0cHM6Ly9kYXJ0LmRldi9ndWlkZXMvbGlicmFyaWVzL3ByaXZhdGUtZmlsZXMKCiMgRmlsZXMgYW5kIGRpcmVjdG9yaWVzIGNyZWF0ZWQgYnkgcHViCi5kYXJ0X3Rvb2wKLnBhY2thZ2VzCi5wdWIKcHVic3BlYy5sb2NrCgpjb3ZlcmFnZS8KLnRlc3RfY292ZXJhZ2UuZGFydApidWlsZC8=",
+      "type": "text"
+    },
+    {
+      "path": "lib/{{project_name}}.dart",
+      "data":
+          "bGlicmFyeSB7e3Byb2plY3RfbmFtZX19OwoKZXhwb3J0ICdzcmMve3twcm9qZWN0X25hbWV9fS5kYXJ0Jzs=",
+      "type": "text"
+    },
+    {
+      "path": "lib/src/{{project_name}}.dart",
+      "data":
+          "Ly8vIHtAdGVtcGxhdGUge3twcm9qZWN0X25hbWV9fX0KLy8vIHt7ZGVzY3JpcHRpb259fQovLy8ge0BlbmR0ZW1wbGF0ZX0KY2xhc3Mge3sjIHBhc2NhbENhc2V9fXt7cHJvamVjdF9uYW1lfX17ey8gcGFzY2FsQ2FzZX19IHsKICAvLy8ge0BtYWNybyB7e3Byb2plY3RfbmFtZX19fQogIGNvbnN0IHt7IyBwYXNjYWxDYXNlfX17e3Byb2plY3RfbmFtZX19e3svIHBhc2NhbENhc2V9fSgpOwp9",
+      "type": "text"
+    },
+    {
+      "path": "analysis_options.yaml",
+      "data":
+          "aW5jbHVkZTogcGFja2FnZTp2ZXJ5X2dvb2RfYW5hbHlzaXMvYW5hbHlzaXNfb3B0aW9ucy55YW1s",
+      "type": "text"
+    }
+  ],
+  "name": "dart_package",
+  "description": "A new Dart Package",
+  "vars": ["project_name", "description"]
+});
diff --git a/lib/src/templates/flutter_package_bundle.dart b/lib/src/templates/flutter_package_bundle.dart
new file mode 100644
index 000000000..4a2d41cba
--- /dev/null
+++ b/lib/src/templates/flutter_package_bundle.dart
@@ -0,0 +1,54 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+// ignore_for_file: prefer_single_quotes, public_member_api_docs, lines_longer_than_80_chars
+
+import 'package:mason/mason.dart';
+
+final flutterPackageBundle = MasonBundle.fromJson({
+  "files": [
+    {
+      "path": "test/{{project_name}}_test.dart",
+      "data":
+          "Ly8gaWdub3JlX2Zvcl9maWxlOiBwcmVmZXJfY29uc3RfY29uc3RydWN0b3JzCmltcG9ydCAncGFja2FnZTpmbHV0dGVyX3Rlc3QvZmx1dHRlcl90ZXN0LmRhcnQnOwppbXBvcnQgJ3BhY2thZ2U6e3twcm9qZWN0X25hbWV9fS97e3Byb2plY3RfbmFtZX19LmRhcnQnOwoKdm9pZCBtYWluKCkgewogIGdyb3VwKCd7eyMgcGFzY2FsQ2FzZX19e3twcm9qZWN0X25hbWV9fXt7LyBwYXNjYWxDYXNlfX0nLCAoKSB7CiAgICB0ZXN0KCdjYW4gYmUgaW5zdGFudGlhdGVkJywgKCkgewogICAgICBleHBlY3Qoe3sjIHBhc2NhbENhc2V9fXt7cHJvamVjdF9uYW1lfX17ey8gcGFzY2FsQ2FzZX19KCksIGlzTm90TnVsbCk7CiAgICB9KTsKICB9KTsKfQ==",
+      "type": "text"
+    },
+    {
+      "path": "README.md",
+      "data":
+          "IyB7e3Byb2plY3RfbmFtZX19CgpbIVtzdHlsZTogdmVyeSBnb29kIGFuYWx5c2lzXVt2ZXJ5X2dvb2RfYW5hbHlzaXNfYmFkZ2VdXVt2ZXJ5X2dvb2RfYW5hbHlzaXNfbGlua10KWyFbTGljZW5zZTogTUlUXVtsaWNlbnNlX2JhZGdlXV1bbGljZW5zZV9saW5rXQoKe3tkZXNjcmlwdGlvbn19CgpbbGljZW5zZV9iYWRnZV06IGh0dHBzOi8vaW1nLnNoaWVsZHMuaW8vYmFkZ2UvbGljZW5zZS1NSVQtYmx1ZS5zdmcKW2xpY2Vuc2VfbGlua106IGh0dHBzOi8vb3BlbnNvdXJjZS5vcmcvbGljZW5zZXMvTUlUClt2ZXJ5X2dvb2RfYW5hbHlzaXNfYmFkZ2VdOiBodHRwczovL2ltZy5zaGllbGRzLmlvL2JhZGdlL3N0eWxlLXZlcnlfZ29vZF9hbmFseXNpcy1CMjJDODkuc3ZnClt2ZXJ5X2dvb2RfYW5hbHlzaXNfbGlua106IGh0dHBzOi8vcHViLmRldi9wYWNrYWdlcy92ZXJ5X2dvb2RfYW5hbHlzaXM=",
+      "type": "text"
+    },
+    {
+      "path": "pubspec.yaml",
+      "data":
+          "bmFtZToge3twcm9qZWN0X25hbWV9fQpkZXNjcmlwdGlvbjoge3tkZXNjcmlwdGlvbn19CnZlcnNpb246IDEuMC4wKzEKcHVibGlzaF90bzogbm9uZQoKZW52aXJvbm1lbnQ6CiAgc2RrOiAiPj0yLjEyLjAgPDMuMC4wIgoKZGVwZW5kZW5jaWVzOgogIGZsdXR0ZXI6CiAgICBzZGs6IGZsdXR0ZXIKCmRldl9kZXBlbmRlbmNpZXM6CiAgZmx1dHRlcl90ZXN0OgogICAgc2RrOiBmbHV0dGVyCiAgdmVyeV9nb29kX2FuYWx5c2lzOiBeMi4xLjA=",
+      "type": "text"
+    },
+    {
+      "path": ".gitignore",
+      "data":
+          "IyBNaXNjZWxsYW5lb3VzCiouY2xhc3MKKi5sb2cKKi5weWMKKi5zd3AKLkRTX1N0b3JlCi5hdG9tLwouYnVpbGRsb2cvCi5oaXN0b3J5Ci5zdm4vCgojIEludGVsbGlKIHJlbGF0ZWQKKi5pbWwKKi5pcHIKKi5pd3MKLmlkZWEvCgojIFZTQ29kZSByZWxhdGVkCi52c2NvZGUvCgojIEZsdXR0ZXIvRGFydC9QdWIgcmVsYXRlZAoqKi9kb2MvYXBpLwoqKi9pb3MvRmx1dHRlci8ubGFzdF9idWlsZF9pZAouZGFydF90b29sLwouZmx1dHRlci1wbHVnaW5zCi5mbHV0dGVyLXBsdWdpbnMtZGVwZW5kZW5jaWVzCi5wYWNrYWdlcwoucHViLWNhY2hlLwoucHViLwovYnVpbGQvCgojIFdlYiByZWxhdGVkCmxpYi9nZW5lcmF0ZWRfcGx1Z2luX3JlZ2lzdHJhbnQuZGFydAoKIyBTeW1ib2xpY2F0aW9uIHJlbGF0ZWQKYXBwLiouc3ltYm9scwoKIyBPYmZ1c2NhdGlvbiByZWxhdGVkCmFwcC4qLm1hcC5qc29uCg==",
+      "type": "text"
+    },
+    {
+      "path": "lib/{{project_name}}.dart",
+      "data":
+          "bGlicmFyeSB7e3Byb2plY3RfbmFtZX19OwoKZXhwb3J0ICdzcmMve3twcm9qZWN0X25hbWV9fS5kYXJ0Jzs=",
+      "type": "text"
+    },
+    {
+      "path": "lib/src/{{project_name}}.dart",
+      "data":
+          "Ly8vIHtAdGVtcGxhdGUge3twcm9qZWN0X25hbWV9fX0KLy8vIHt7ZGVzY3JpcHRpb259fQovLy8ge0BlbmR0ZW1wbGF0ZX0KY2xhc3Mge3sjIHBhc2NhbENhc2V9fXt7cHJvamVjdF9uYW1lfX17ey8gcGFzY2FsQ2FzZX19IHsKICAvLy8ge0BtYWNybyB7e3Byb2plY3RfbmFtZX19fQogIGNvbnN0IHt7IyBwYXNjYWxDYXNlfX17e3Byb2plY3RfbmFtZX19e3svIHBhc2NhbENhc2V9fSgpOwp9",
+      "type": "text"
+    },
+    {
+      "path": "analysis_options.yaml",
+      "data":
+          "aW5jbHVkZTogcGFja2FnZTp2ZXJ5X2dvb2RfYW5hbHlzaXMvYW5hbHlzaXNfb3B0aW9ucy55YW1s",
+      "type": "text"
+    }
+  ],
+  "name": "flutter_package",
+  "description": "A new Flutter Package",
+  "vars": ["project_name", "description"]
+});
diff --git a/lib/src/templates/template.dart b/lib/src/templates/template.dart
new file mode 100644
index 000000000..4e60ae10c
--- /dev/null
+++ b/lib/src/templates/template.dart
@@ -0,0 +1,141 @@
+import 'package:io/ansi.dart';
+import 'package:mason/mason.dart';
+import 'package:universal_io/io.dart';
+import 'package:very_good_cli/src/flutter_cli.dart';
+import 'package:very_good_cli/src/templates/templates.dart';
+
+/// {@template template}
+/// Dart class that represents a VeryGoodCLI supported template.
+/// Each template consists of a [MasonBundle], name,
+/// and help text describing the template.
+/// {@endtemplate}
+abstract class Template {
+  /// {@macro template}
+  const Template({
+    required this.name,
+    required this.bundle,
+    required this.help,
+  });
+
+  /// The name associated with this template.
+  final String name;
+
+  /// The [MasonBundle] used to generate this template.
+  final MasonBundle bundle;
+
+  /// The help text shown in the usage information for the CLI.
+  final String help;
+
+  /// Callback invoked after template generation has completed.
+  Future<void> onGenerateComplete(Logger logger, Directory outputDir);
+}
+
+/// {@template dart_pkg_template}
+/// Dart class that represents a VeryGoodCLI dart package template.
+/// {@endtemplate}
+class DartPkgTemplate extends Template {
+  /// {@macro dart_pkg_template}
+  DartPkgTemplate()
+      : super(
+          name: 'dart_pkg',
+          bundle: dartPackageBundle,
+          help: 'Generate a reusable pure Dart package.',
+        );
+
+  @override
+  Future<void> onGenerateComplete(Logger logger, Directory outputDir) async {
+    final isFlutterInstalled = await Flutter.installed();
+    if (isFlutterInstalled) {
+      final installDependenciesDone = logger.progress(
+        'Running "flutter pub get" in ${outputDir.path}',
+      );
+      await Flutter.pubGet(outputDir.path);
+      installDependenciesDone();
+    }
+    _logSummary(logger);
+  }
+
+  void _logSummary(Logger logger) {
+    logger
+      ..info('\n')
+      ..alert('Created a Very Good Dart package! 🦄')
+      ..info('\n');
+  }
+}
+
+/// {@template flutter_pkg_template}
+/// Dart class that represents a VeryGoodCLI Flutter package template.
+/// {@endtemplate}
+class FlutterPkgTemplate extends Template {
+  /// {@macro flutter_pkg_template}
+  FlutterPkgTemplate()
+      : super(
+            name: 'flutter_pkg',
+            bundle: flutterPackageBundle,
+            help: 'Generate a reusable Flutter package.');
+
+  @override
+  Future<void> onGenerateComplete(Logger logger, Directory outputDir) async {
+    final isFlutterInstalled = await Flutter.installed();
+    if (isFlutterInstalled) {
+      final installDependenciesDone = logger.progress(
+        'Running "flutter packages get" in ${outputDir.path}',
+      );
+      await Flutter.packagesGet(outputDir.path);
+      installDependenciesDone();
+    }
+    _logSummary(logger);
+  }
+
+  void _logSummary(Logger logger) {
+    logger
+      ..info('\n')
+      ..alert('Created a Very Good Flutter package! 🦄')
+      ..info('\n');
+  }
+}
+
+/// {@template core_template}
+/// Dart class that represents a VeryGoodCLI core flutter app template.
+/// {@endtemplate}
+class CoreTemplate extends Template {
+  /// {@macro core_template}
+  CoreTemplate()
+      : super(
+          name: 'core',
+          bundle: veryGoodCoreBundle,
+          help: 'Generate a Very Good Flutter application.',
+        );
+
+  @override
+  Future<void> onGenerateComplete(Logger logger, Directory outputDir) async {
+    final isFlutterInstalled = await Flutter.installed();
+    if (isFlutterInstalled) {
+      final installDependenciesDone = logger.progress(
+        'Running "flutter packages get" in ${outputDir.path}',
+      );
+      await Flutter.packagesGet(outputDir.path);
+      installDependenciesDone();
+    }
+    _logSummary(logger);
+  }
+
+  void _logSummary(Logger logger) {
+    logger
+      ..info('\n')
+      ..alert('Created a Very Good App! 🦄')
+      ..info('\n')
+      ..info(
+        lightGray.wrap(
+          '''+----------------------------------------------------+
+| Looking for more features?                         |
+| We have an enterprise-grade solution for companies |
+| called Very Good Start.                            |
+|                                                    |
+| For more info visit:                               |
+| https://verygood.ventures/solution/very-good-start |
++----------------------------------------------------+''',
+        ),
+      );
+  }
+}
diff --git a/lib/src/templates/templates.dart b/lib/src/templates/templates.dart
new file mode 100644
index 000000000..a2ec0b1b1
--- /dev/null
+++ b/lib/src/templates/templates.dart
@@ -0,0 +1,4 @@
+export 'dart_package_bundle.dart';
+export 'flutter_package_bundle.dart';
+export 'template.dart';
+export 'very_good_core_bundle.dart';


Test patch:
diff --git a/test/src/flutter_cli_test.dart b/test/src/cmd_test.dart
similarity index 61%
rename from test/src/flutter_cli_test.dart
rename to test/src/cmd_test.dart
index e219ba5c4..ab51a7bfc 100644
--- a/test/src/flutter_cli_test.dart
+++ b/test/src/cmd_test.dart
@@ -16,5 +16,18 @@ void main() {
         expectLater(Flutter.packagesGet(), completes);
       });
     });
+
+    group('pub get', () {
+      test('throws when there is no pubspec.yaml', () {
+        expectLater(
+          Flutter.pubGet(Directory.systemTemp.path),
+          throwsException,
+        );
+      });
+
+      test('completes when there is a pubspec.yaml', () {
+        expectLater(Flutter.pubGet(), completes);
+      });
+    });
   });
 }
diff --git a/test/src/commands/create_test.dart b/test/src/commands/create_test.dart
index 3e2e17a80..875ece4a6 100644
--- a/test/src/commands/create_test.dart
+++ b/test/src/commands/create_test.dart
@@ -1,3 +1,4 @@
+import 'dart:async';
 import 'package:args/args.dart';
 import 'package:io/io.dart';
 import 'package:mason/mason.dart';
@@ -6,6 +7,31 @@ import 'package:test/test.dart';
 import 'package:usage/usage_io.dart';
 import 'package:very_good_cli/src/command_runner.dart';
 import 'package:very_good_cli/src/commands/create.dart';
+import 'package:very_good_cli/src/templates/templates.dart';
+
+const expectedUsage = [
+  // ignore: no_adjacent_strings_in_list
+  'Creates a new very good flutter project in the specified directory.\n'
+      '\n'
+      'Usage: very_good create <output directory>\n'
+      '-h, --help                    '
+      'Print this usage information.\n'
+      '    --project-name            '
+      'The project name for this new Flutter project. '
+      'This must be a valid dart package name.\n'
+      '    --org-name                '
+      'The organization for this new Flutter project.\n'
+      '                              (defaults to "com.example.verygoodcore")\n'
+      '-t, --template                '
+      'The template used to generate this new project.\n'
+      '\n'
+      '          [core] (default)    '
+      'Generate a Very Good Flutter application.\n'
+      '          [dart_pkg]          Generate a reusable pure Dart package.\n'
+      '          [flutter_pkg]       Generate a reusable Flutter package.\n'
+      '\n'
+      'Run "very_good help" to see global options.'
+];
 
 class MockArgResults extends Mock implements ArgResults {}
 
@@ -21,15 +47,26 @@ class FakeDirectoryGeneratorTarget extends Fake
 void main() {
   group('Create', () {
     late List<String> progressLogs;
+    late List<String> printLogs;
     late Analytics analytics;
     late Logger logger;
     late VeryGoodCommandRunner commandRunner;
 
+    void Function() overridePrint(void Function() fn) {
+      return () {
+        final spec = ZoneSpecification(print: (_, __, ___, String msg) {
+          printLogs.add(msg);
+        });
+        return Zone.current.fork(specification: spec).run<void>(fn);
+      };
+    }
+
     setUpAll(() {
       registerFallbackValue(FakeDirectoryGeneratorTarget());
     });
 
     setUp(() {
+      printLogs = [];
       progressLogs = <String>[];
       analytics = MockAnalytics();
       when(() => analytics.firstRun).thenReturn(false);
@@ -53,6 +90,18 @@ void main() {
       );
     });
 
+    test('help', overridePrint(() async {
+      final result = await commandRunner.run(['create', '--help']);
+      expect(printLogs, equals(expectedUsage));
+      expect(result, equals(ExitCode.success.code));
+
+      printLogs.clear();
+
+      final resultAbbr = await commandRunner.run(['create', '-h']);
+      expect(printLogs, equals(expectedUsage));
+      expect(resultAbbr, equals(ExitCode.success.code));
+    }));
+
     test('can be instantiated without explicit logger', () {
       final command = CreateCommand(analytics: analytics);
       expect(command, isNotNull);
@@ -271,5 +320,116 @@ void main() {
         });
       });
     });
+
+    group('--template', () {
+      group('invalid template name', () {
+        void expectInvalidTemplateName(String templateName) async {
+          final expectedErrorMessage =
+              '"$templateName" is not an allowed value for option "template".';
+          final result = await commandRunner.run(
+            ['create', '.', '--template', templateName],
+          );
+          expect(result, equals(ExitCode.usage.code));
+          verify(() => logger.err(expectedErrorMessage)).called(1);
+        }
+
+        test('invalid template name', () {
+          expectInvalidTemplateName('badtemplate');
+        });
+      });
+
+      group('valid template names', () {
+        Future<void> expectValidTemplateName({
+          required String getPackagesMsg,
+          required String templateName,
+          required MasonBundle expectedBundle,
+          required String expectedLogSummary,
+        }) async {
+          final argResults = MockArgResults();
+          final generator = MockMasonGenerator();
+          final command = CreateCommand(
+            analytics: analytics,
+            logger: logger,
+            generator: (bundle) async {
+              expect(bundle, equals(expectedBundle));
+              return generator;
+            },
+          )..argResultOverrides = argResults;
+          when(() => argResults['project-name']).thenReturn('my_app');
+          when(() => argResults['template']).thenReturn(templateName);
+          when(() => argResults.rest).thenReturn(['.tmp']);
+          when(() => generator.id).thenReturn('generator_id');
+          when(() => generator.description).thenReturn('generator description');
+          when(
+            () => generator.generate(any(), vars: any(named: 'vars')),
+          ).thenAnswer((_) async => 62);
+          final result = await command.run();
+          expect(result, equals(ExitCode.success.code));
+          verify(() => logger.progress('Bootstrapping')).called(1);
+          expect(progressLogs, equals(['Generated 62 file(s)']));
+          verify(
+            () => logger.progress(getPackagesMsg),
+          ).called(1);
+          verify(() => logger.alert(expectedLogSummary)).called(1);
+          verify(
+            () => generator.generate(
+              any(
+                that: isA<DirectoryGeneratorTarget>().having(
+                  (g) => g.dir.path,
+                  'dir',
+                  '.tmp',
+                ),
+              ),
+              vars: {
+                'project_name': 'my_app',
+                'org_name': [
+                  {'value': 'com', 'separator': '.'},
+                  {'value': 'example', 'separator': '.'},
+                  {'value': 'verygoodcore', 'separator': ''}
+                ],
+              },
+            ),
+          ).called(1);
+          verify(
+            () => analytics.sendEvent(
+              'create',
+              'generator_id',
+              label: 'generator description',
+            ),
+          ).called(1);
+          verify(
+            () => analytics.waitForLastPing(
+                timeout: VeryGoodCommandRunner.timeout),
+          ).called(1);
+        }
+
+        test('core template', () async {
+          await expectValidTemplateName(
+            getPackagesMsg: 'Running "flutter packages get" in .tmp',
+            templateName: 'core',
+            expectedBundle: veryGoodCoreBundle,
+            expectedLogSummary: 'Created a Very Good App! 🦄',
+          );
+        });
+
+        test('dart pkg template', () async {
+          await expectValidTemplateName(
+            getPackagesMsg: 'Running "flutter pub get" in .tmp',
+            templateName: 'dart_pkg',
+            expectedBundle: dartPackageBundle,
+            expectedLogSummary: 'Created a Very Good Dart package! 🦄',
+          );
+        });
+
+        test('flutter pkg template', () async {
+          await expectValidTemplateName(
+            getPackagesMsg: 'Running "flutter packages get" in .tmp',
+            templateName: 'flutter_pkg',
+            expectedBundle: flutterPackageBundle,
+            expectedLogSummary: 'Created a Very Good Flutter package! 🦄',
+          );
+        });
+      });
+    });
   });
 }