From 24f176c1df37876e1a2b3131c8ce6d5d0ed49039 Mon Sep 17 00:00:00 2001 From: Marcos Sevilla Date: Thu, 29 Oct 2026 12:29:22 +0100 Subject: [PATCH 1/3] feat(create): add --org-name to package and cli subcommands The app_ui, dart_cli, dart_package and flutter_package bundles declare an org_name var (the LICENSE copyright holder) that the CLI never passed, so users could not set it and the template vars check failed. Extract docs_site's free-form --org-name option into a FreeFormOrgName mixin with a per-command defaultOrgName, and apply it to those four subcommands. Each default matches the current bundle default, so generated output is unchanged unless the flag is passed. The value resolves from the flag, then very_good.yaml create.org_name, then the default; note that a create.org_name in very_good.yaml now also applies to these commands' LICENSE. --- .../create/commands/app_ui_package.dart | 5 +- .../create/commands/create_subcommand.dart | 40 +++++++ .../commands/create/commands/dart_cli.dart | 5 +- .../create/commands/dart_package.dart | 5 +- .../commands/create/commands/docs_site.dart | 25 +--- .../create/commands/flutter_package.dart | 5 +- site/docs/commands/create.md | 4 + site/docs/templates/app_ui_package.md | 3 + site/docs/templates/dart_cli.md | 3 + site/docs/templates/dart_pkg.md | 3 + site/docs/templates/flutter_pkg.md | 3 + .../create/commands/app_ui_package_test.dart | 18 +++ .../create/commands/dart_cli_test.dart | 15 +++ .../create/commands/dart_package_test.dart | 18 +++ .../create/commands/flutter_package_test.dart | 18 +++ .../create/create_subcommand_test.dart | 110 ++++++++++++++++++ 16 files changed, 255 insertions(+), 25 deletions(-) diff --git a/lib/src/commands/create/commands/app_ui_package.dart b/lib/src/commands/create/commands/app_ui_package.dart index 7d1c1dd78..22aa88db4 100644 --- a/lib/src/commands/create/commands/app_ui_package.dart +++ b/lib/src/commands/create/commands/app_ui_package.dart @@ -7,10 +7,13 @@ import 'package:very_good_cli/src/commands/create/templates/templates.dart'; class CreateAppUiPackage({ required super.logger, required super.generatorFromBundle, -}) extends CreateSubCommand with Publishable, Workspace { +}) extends CreateSubCommand with Publishable, Workspace, FreeFormOrgName { /// {@macro very_good_create_app_ui_package_command} this; + @override + String get defaultOrgName => 'com.example.verygoodapp'; + @override String get name => 'app_ui_package'; diff --git a/lib/src/commands/create/commands/create_subcommand.dart b/lib/src/commands/create/commands/create_subcommand.dart index 01bc48c7c..c8e8f51c5 100644 --- a/lib/src/commands/create/commands/create_subcommand.dart +++ b/lib/src/commands/create/commands/create_subcommand.dart @@ -49,6 +49,9 @@ typedef TemplateVarResolvers = Map; /// /// For sub commands that receive a workspace flag, sub classes must mix with /// [Workspace]. +/// +/// For sub commands that receive a free-form org name, sub classes must mix +/// with [FreeFormOrgName]. abstract class CreateSubCommand({ /// The logger user to notify the user of the command's progress. required final Logger logger, @@ -117,6 +120,15 @@ abstract class CreateSubCommand({ 'from a parent Pub workspace.', ); } + + if (this is FreeFormOrgName) { + argParser.addOption( + 'org-name', + help: 'The organization for this new project.', + defaultsTo: (this as FreeFormOrgName).defaultOrgName, + aliases: ['org'], + ); + } } final MasonGeneratorFromBundle _generatorFromBundle = @@ -410,3 +422,31 @@ mixin Workspace on CreateSubCommand { fallbackValue: false, ); } + +/// Mixin for [CreateSubCommand] subclasses that receive a free-form org name, +/// such as a copyright holder or a GitHub organization. +/// +/// Unlike [OrgName], the value is not validated as a reverse-domain +/// identifier. Both mixins register the `org-name` option, so combining them +/// throws an [ArgumentError] when the command is constructed. +/// +/// The org name resolves from the `--org-name` option, then the +/// `create.org_name` key in `very_good.yaml`, then [defaultOrgName]. +mixin FreeFormOrgName on CreateSubCommand { + /// The org name used when neither the `--org-name` option nor the + /// `create.org_name` configuration value is provided. + String get defaultOrgName; + + @override + Map get templateVarResolvers => { + ...super.templateVarResolvers, + 'org_name': () => orgName, + }; + + /// Gets the organization name. + String get orgName => argResults.resolve( + 'org-name', + createConfig.orgName, + fallbackValue: defaultOrgName, + ); +} diff --git a/lib/src/commands/create/commands/dart_cli.dart b/lib/src/commands/create/commands/dart_cli.dart index 189944eed..d1fcdf928 100644 --- a/lib/src/commands/create/commands/dart_cli.dart +++ b/lib/src/commands/create/commands/dart_cli.dart @@ -6,7 +6,7 @@ import 'package:very_good_cli/src/commands/create/templates/templates.dart'; /// {@endtemplate} class CreateDartCLI({required super.logger, required super.generatorFromBundle}) extends CreateSubCommand - with Publishable, Workspace { + with Publishable, Workspace, FreeFormOrgName { /// {@macro very_good_create_dart_cli_command} this { argParser.addOption( @@ -15,6 +15,9 @@ class CreateDartCLI({required super.logger, required super.generatorFromBundle}) ); } + @override + String get defaultOrgName => 'Very Good Ventures'; + @override String get name => 'dart_cli'; diff --git a/lib/src/commands/create/commands/dart_package.dart b/lib/src/commands/create/commands/dart_package.dart index fef90ceff..33e01c8a8 100644 --- a/lib/src/commands/create/commands/dart_package.dart +++ b/lib/src/commands/create/commands/dart_package.dart @@ -7,10 +7,13 @@ import 'package:very_good_cli/src/commands/create/templates/templates.dart'; class CreateDartPackage({ required super.logger, required super.generatorFromBundle, -}) extends CreateSubCommand with Publishable, Workspace { +}) extends CreateSubCommand with Publishable, Workspace, FreeFormOrgName { /// {@macro very_good_create_dart_package_command} this; + @override + String get defaultOrgName => 'Very Good Ventures'; + @override String get name => 'dart_package'; diff --git a/lib/src/commands/create/commands/docs_site.dart b/lib/src/commands/create/commands/docs_site.dart index 129e361a1..672bcd54b 100644 --- a/lib/src/commands/create/commands/docs_site.dart +++ b/lib/src/commands/create/commands/docs_site.dart @@ -1,6 +1,5 @@ import 'package:very_good_cli/src/commands/commands.dart'; import 'package:very_good_cli/src/commands/create/templates/templates.dart'; -import 'package:very_good_cli/src/very_good_config/very_good_config.dart'; /// {@template very_good_create_docs_site} /// A [CreateSubCommand] for creating Dart command line interfaces. @@ -8,18 +7,12 @@ import 'package:very_good_cli/src/very_good_config/very_good_config.dart'; class CreateDocsSite({ required super.logger, required super.generatorFromBundle, -}) extends CreateSubCommand with Publishable { +}) extends CreateSubCommand with Publishable, FreeFormOrgName { /// {@macro very_good_create_docs_site} - this { - argParser.addOption( - 'org-name', - help: 'The organization for this new project.', - defaultsTo: _defaultOrgName, - aliases: ['org'], - ); - } + this; - static const _defaultOrgName = 'my-org'; + @override + String get defaultOrgName => 'my-org'; @override String get name => 'docs_site'; @@ -27,16 +20,6 @@ class CreateDocsSite({ @override String get description => 'Generate a Very Good documentation site.'; - @override - TemplateVarResolvers get templateVarResolvers => { - ...super.templateVarResolvers, - 'org_name': () => argResults.resolve( - 'org-name', - createConfig.orgName, - fallbackValue: _defaultOrgName, - ), - }; - @override Template get template => VeryGoodDocsSiteTemplate(); } diff --git a/lib/src/commands/create/commands/flutter_package.dart b/lib/src/commands/create/commands/flutter_package.dart index d5d625776..08bb5ea1b 100644 --- a/lib/src/commands/create/commands/flutter_package.dart +++ b/lib/src/commands/create/commands/flutter_package.dart @@ -7,10 +7,13 @@ import 'package:very_good_cli/src/commands/create/templates/templates.dart'; class CreateFlutterPackage({ required super.logger, required super.generatorFromBundle, -}) extends CreateSubCommand with Publishable, Workspace { +}) extends CreateSubCommand with Publishable, Workspace, FreeFormOrgName { /// {@macro very_good_create_flutter_package_command} this; + @override + String get defaultOrgName => 'Very Good Ventures'; + @override String get name => 'flutter_package'; diff --git a/site/docs/commands/create.md b/site/docs/commands/create.md index 7b9bae7f8..35d626ad5 100644 --- a/site/docs/commands/create.md +++ b/site/docs/commands/create.md @@ -84,6 +84,10 @@ create: With the file above, running `very_good create flutter_app my_app` behaves the same as running `very_good create flutter_app my_app --desc 'A Very Good project.' --org-name com.very.good --publishable --template core`. You can still override any of these values on the command line, for example `very_good create flutter_app my_app --org-name com.example` to use a different org name for a single run. +:::note +For `app_ui_package`, `dart_cli`, `dart_package`, and `flutter_package`, the org name is the copyright holder in the generated `LICENSE` and accepts any text. Setting `org_name` in `very_good.yaml` applies to these subcommands too. +::: + The `very_good.yaml` file is looked up starting from the directory where the command runs and walking up through its ancestors. The closest file wins; configuration from ancestor directories is not merged. This lets a single `very_good.yaml` at the repository root apply to commands run from any nested package. ## Available templates diff --git a/site/docs/templates/app_ui_package.md b/site/docs/templates/app_ui_package.md index 159f6ba6a..29d189cec 100644 --- a/site/docs/templates/app_ui_package.md +++ b/site/docs/templates/app_ui_package.md @@ -19,6 +19,9 @@ generated project. # Create a new App UI package named my_app_ui very_good create app_ui_package my_app_ui --desc "My new App UI package" +# Create a new App UI package with a custom copyright holder +very_good create app_ui_package my_app_ui --desc "My new App UI package" --org-name "My Company" + # Create a new App UI package that is publishable very_good create app_ui_package my_app_ui --desc "My new App UI package" --publishable diff --git a/site/docs/templates/dart_cli.md b/site/docs/templates/dart_cli.md index 371ea0b95..03f968c14 100644 --- a/site/docs/templates/dart_cli.md +++ b/site/docs/templates/dart_cli.md @@ -21,6 +21,9 @@ very_good create dart_cli my_dart_cli --desc "My new Dart CLI package" # Create a new Dart CLI application named my_dart_cli with a custom executable name very_good create dart_cli my_dart_cli --desc "My new Dart CLI package" --executable-name my_executable_name +# Create a new Dart CLI application named my_dart_cli with a custom copyright holder +very_good create dart_cli my_dart_cli --desc "My new Dart CLI package" --org-name "My Company" + # Create a new Dart CLI application named my_dart_cli that is publishable very_good create dart_cli my_dart_cli --desc "My new Dart CLI package" --publishable diff --git a/site/docs/templates/dart_pkg.md b/site/docs/templates/dart_pkg.md index ebb6e90ae..90af78f16 100644 --- a/site/docs/templates/dart_pkg.md +++ b/site/docs/templates/dart_pkg.md @@ -16,6 +16,9 @@ Use `-o` or `--output-directory` to specify a custom output directory for the ge # Create a new Dart package named my_dart_package very_good create dart_package my_dart_package --desc "My new Dart package" +# Create a new Dart package named my_dart_package with a custom copyright holder +very_good create dart_package my_dart_package --desc "My new Dart package" --org-name "My Company" + # Create a new Dart package named my_dart_package that is publishable very_good create dart_package my_dart_package --desc "My new Dart package" --publishable diff --git a/site/docs/templates/flutter_pkg.md b/site/docs/templates/flutter_pkg.md index d6ee65561..1f24e8300 100644 --- a/site/docs/templates/flutter_pkg.md +++ b/site/docs/templates/flutter_pkg.md @@ -16,6 +16,9 @@ Use `-o` or `--output-directory` to specify a custom output directory for the ge # Create a new Flutter package named my_flutter_package very_good create flutter_package my_flutter_package --desc "My new Flutter package" +# Create a new Flutter package named my_flutter_package with a custom copyright holder +very_good create flutter_package my_flutter_package --desc "My new Flutter package" --org-name "My Company" + # Create a new Flutter package named my_flutter_package that is publishable very_good create flutter_package my_flutter_package --desc "My new Flutter package" --publishable diff --git a/test/src/commands/create/commands/app_ui_package_test.dart b/test/src/commands/create/commands/app_ui_package_test.dart index 1d44e09be..e43b07800 100644 --- a/test/src/commands/create/commands/app_ui_package_test.dart +++ b/test/src/commands/create/commands/app_ui_package_test.dart @@ -35,6 +35,8 @@ Usage: very_good create app_ui_package [arguments] (defaults to "A Very Good Project created by Very Good CLI.") --publishable Whether the generated project is intended to be published. --[no-]workspace Whether the generated project should resolve its dependencies from a parent Pub workspace. + --org-name The organization for this new project. + (defaults to "com.example.verygoodapp") Run "very_good help" to see global options.''', ]; @@ -103,6 +105,20 @@ void main() { }), ); + test('passes the org name', () { + final command = CreateAppUiPackage( + logger: logger, + generatorFromBundle: null, + ); + command.argResultOverrides = command.argParser.parse([ + 'my_app_ui', + '--org-name', + 'Acme', + ]); + + expect(command.getTemplateVars(), containsPair('org_name', 'Acme')); + }); + group('running the command', () { final generatedFiles = List.filled( 10, @@ -187,6 +203,7 @@ void main() { 'description': '', 'publishable': false, 'workspace': false, + 'org_name': 'com.example.verygoodapp', }, onVarsChanged: any(named: 'onVarsChanged'), ), @@ -199,6 +216,7 @@ void main() { 'description': '', 'publishable': false, 'workspace': false, + 'org_name': 'com.example.verygoodapp', }, logger: logger, ), diff --git a/test/src/commands/create/commands/dart_cli_test.dart b/test/src/commands/create/commands/dart_cli_test.dart index 96a3237d3..7e304d1e5 100644 --- a/test/src/commands/create/commands/dart_cli_test.dart +++ b/test/src/commands/create/commands/dart_cli_test.dart @@ -35,6 +35,8 @@ Usage: very_good create dart_cli [arguments] (defaults to "A Very Good Project created by Very Good CLI.") --publishable Whether the generated project is intended to be published. --[no-]workspace Whether the generated project should resolve its dependencies from a parent Pub workspace. + --org-name The organization for this new project. + (defaults to "Very Good Ventures") --executable-name The CLI executable name (defaults to the project name) Run "very_good help" to see global options.''', @@ -102,6 +104,17 @@ void main() { }), ); + test('passes the org name', () { + final command = CreateDartCLI(logger: logger, generatorFromBundle: null); + command.argResultOverrides = command.argParser.parse([ + 'my_cli', + '--org-name', + 'Acme', + ]); + + expect(command.getTemplateVars(), containsPair('org_name', 'Acme')); + }); + group('running the command', () { final generatedFiles = List.filled( 10, @@ -188,6 +201,7 @@ void main() { 'description': '', 'publishable': false, 'workspace': false, + 'org_name': 'Very Good Ventures', 'executable_name': 'my_executable', }, onVarsChanged: any(named: 'onVarsChanged'), @@ -201,6 +215,7 @@ void main() { 'description': '', 'publishable': false, 'workspace': false, + 'org_name': 'Very Good Ventures', 'executable_name': 'my_executable', }, logger: logger, diff --git a/test/src/commands/create/commands/dart_package_test.dart b/test/src/commands/create/commands/dart_package_test.dart index 91c7baad3..e1132a4f2 100644 --- a/test/src/commands/create/commands/dart_package_test.dart +++ b/test/src/commands/create/commands/dart_package_test.dart @@ -35,6 +35,8 @@ Usage: very_good create dart_package [arguments] (defaults to "A Very Good Project created by Very Good CLI.") --publishable Whether the generated project is intended to be published. --[no-]workspace Whether the generated project should resolve its dependencies from a parent Pub workspace. + --org-name The organization for this new project. + (defaults to "Very Good Ventures") Run "very_good help" to see global options.''', ]; @@ -100,6 +102,20 @@ void main() { }), ); + test('passes the org name', () { + final command = CreateDartPackage( + logger: logger, + generatorFromBundle: null, + ); + command.argResultOverrides = command.argParser.parse([ + 'my_package', + '--org-name', + 'Acme', + ]); + + expect(command.getTemplateVars(), containsPair('org_name', 'Acme')); + }); + group('running the command', () { final generatedFiles = List.filled( 10, @@ -184,6 +200,7 @@ void main() { 'description': '', 'publishable': false, 'workspace': false, + 'org_name': 'Very Good Ventures', }, onVarsChanged: any(named: 'onVarsChanged'), ), @@ -196,6 +213,7 @@ void main() { 'description': '', 'publishable': false, 'workspace': false, + 'org_name': 'Very Good Ventures', }, logger: logger, ), diff --git a/test/src/commands/create/commands/flutter_package_test.dart b/test/src/commands/create/commands/flutter_package_test.dart index 8278cee25..96f9d3ddd 100644 --- a/test/src/commands/create/commands/flutter_package_test.dart +++ b/test/src/commands/create/commands/flutter_package_test.dart @@ -35,6 +35,8 @@ Usage: very_good create flutter_package [arguments] (defaults to "A Very Good Project created by Very Good CLI.") --publishable Whether the generated project is intended to be published. --[no-]workspace Whether the generated project should resolve its dependencies from a parent Pub workspace. + --org-name The organization for this new project. + (defaults to "Very Good Ventures") Run "very_good help" to see global options.''', ]; @@ -103,6 +105,20 @@ void main() { }), ); + test('passes the org name', () { + final command = CreateFlutterPackage( + logger: logger, + generatorFromBundle: null, + ); + command.argResultOverrides = command.argParser.parse([ + 'my_flutter_package', + '--org-name', + 'Acme', + ]); + + expect(command.getTemplateVars(), containsPair('org_name', 'Acme')); + }); + group('running the command', () { final generatedFiles = List.filled( 10, @@ -187,6 +203,7 @@ void main() { 'description': '', 'publishable': false, 'workspace': false, + 'org_name': 'Very Good Ventures', }, onVarsChanged: any(named: 'onVarsChanged'), ), @@ -199,6 +216,7 @@ void main() { 'description': '', 'publishable': false, 'workspace': false, + 'org_name': 'Very Good Ventures', }, logger: logger, ), diff --git a/test/src/commands/create/create_subcommand_test.dart b/test/src/commands/create/create_subcommand_test.dart index 18d43f271..4b923b968 100644 --- a/test/src/commands/create/create_subcommand_test.dart +++ b/test/src/commands/create/create_subcommand_test.dart @@ -62,6 +62,24 @@ class _TestCreateSubCommandWithWorkspace({ required super.generatorFromBundle, }) extends _TestCreateSubCommand with Workspace; +class _TestCreateSubCommandWithFreeFormOrgName({ + required super.template, + required super.logger, + required super.generatorFromBundle, +}) extends _TestCreateSubCommand with FreeFormOrgName { + @override + String get defaultOrgName => 'Default Org'; +} + +class _TestCreateSubCommandWithBothOrgNames({ + required super.template, + required super.logger, + required super.generatorFromBundle, +}) extends _TestCreateSubCommand with OrgName, FreeFormOrgName { + @override + String get defaultOrgName => 'Default Org'; +} + class _TestCreateSubCommandWithNullableVar({ required super.template, required super.logger, @@ -1837,4 +1855,96 @@ Run "runner help" to see global options.'''; }); }); }); + + group('FreeFormOrgName', () { + late Template template; + + setUp(() { + template = _MockTemplate(); + }); + + _TestCreateSubCommandWithFreeFormOrgName parse( + List args, { + VeryGoodCreateConfig createConfig = const VeryGoodCreateConfig(), + }) { + final command = _TestCreateSubCommandWithFreeFormOrgName( + template: template, + logger: logger, + generatorFromBundle: null, + )..createConfig = createConfig; + return command + ..argResultOverrides = command.argParser.parse([ + 'test_project', + ...args, + ]); + } + + test('adds the org-name option', () { + final command = _TestCreateSubCommandWithFreeFormOrgName( + template: template, + logger: logger, + generatorFromBundle: null, + ); + + expect( + command.argParser.options['org-name'], + isA