diff --git a/.gitignore b/.gitignore index 3d4562f..1d898ed 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ CLAUDE.md .repoprompt/ .factory/ .claude/ +*.gem diff --git a/Gemfile.lock b/Gemfile.lock index 30d26bd..cb1a450 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - railstart (0.1.0) + railstart (0.2.0) thor tty-prompt diff --git a/README.md b/README.md index 7a4a2b7..4e5b4fa 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,13 @@ gem install railstart ## Usage -### Generate a new Rails app +### Quick Start ```bash +# Generate config files (optional, for customization) +railstart init + +# Generate a new Rails app railstart new my_app # Or run without arguments for help @@ -79,25 +83,88 @@ Creating Rails app... ✨ Rails app created successfully at ./my_app ``` -### Skip interactive mode (use defaults) +### Use Presets + +Presets are configuration overlays that let you define different defaults and even different questions/post-actions for specific use cases. + +**Important:** When you use `--default` without `--preset`, Railstart automatically applies the `default` preset (from `~/.config/railstart/presets/default.yaml` or the gem's built-in version). This means defaults may differ from the base `rails8_defaults.yaml` config. -Use the `--default` flag to skip all questions and apply built-in defaults: +**Modes:** +- **Interactive** (default): prompts for each question from the config schema +- **With --default**: skips questions, loads "default" preset, shows summary and confirms +- **With --preset**: loads specified preset as config overlay (can be interactive or with --default) ```bash +# Interactive mode (builtin defaults) +railstart new my_app + +# Non-interactive with "default" preset (asks no questions, shows summary + confirms) +# Note: --default automatically loads the "default" preset (user or gem) railstart new my_app --default + +# Interactive with custom preset +railstart new my_app --preset api-only + +# Non-interactive with custom preset +railstart new my_app --preset api-only --default +``` + +**Create custom presets** at `~/.config/railstart/presets/my-preset.yaml`: + +Presets use the same YAML schema as config files - they can override question defaults, change choices, add new questions, or modify post-actions: + +```yaml +# ~/.config/railstart/presets/api-only.yaml +# Presets merge on top of user config (and built-in config) +questions: + - id: database + choices: + - name: PostgreSQL + value: postgresql + default: true # Different default for this preset + + - id: api_only + default: true # Override default to true for API preset + +post_actions: + - id: init_git + enabled: false # Disable git init for this preset ``` -This creates a PostgreSQL + Tailwind + Importmap Rails app instantly. +Then use it: + +```bash +# Interactive with api-only config +railstart new my_app --preset api-only + +# Non-interactive with api-only config +railstart new my_app --preset api-only --default +``` ## Configuration +### Initialize Configuration Files + +The easiest way to get started with custom configuration is to generate example files: + +```bash +railstart init +``` + +This creates: +- `~/.config/railstart/config.yaml` - Example user config with common customizations +- `~/.config/railstart/presets/` - Directory for your presets +- `~/.config/railstart/presets/example.yaml` - Example preset to get started + +You can then edit these files to match your preferences. + ### Built-in Defaults Railstart ships with sensible Rails 8 defaults defined in `config/rails8_defaults.yaml`. These drive the interactive questions and their defaults. ### Customize for Your Team -Create a `~/.config/railstart/config.yaml` file to override defaults: +You can create `~/.config/railstart/config.yaml` manually or use `railstart init` to generate an example file: ```yaml questions: @@ -204,7 +271,7 @@ bin/console # Install locally to test as a real gem gem build railstart.gemspec -gem install railstart-0.1.0.gem +gem install railstart-[version].gem railstart new my_app ``` @@ -226,10 +293,28 @@ bundle exec rake test && bundle exec rubocop ## Architecture -- **Config System** (`lib/railstart/config.rb`) - Loads and merges YAML configurations +### Three-Layer Configuration System + +Railstart merges configuration from three sources (in order): + +1. **Built-in config**: `config/rails8_defaults.yaml` (shipped with gem) +2. **User config**: `~/.config/railstart/config.yaml` (optional global overrides) +3. **Preset** (optional): `~/.config/railstart/presets/NAME.yaml` (per-run overlay) + +Each layer can: +- Override question defaults +- Replace choice lists entirely (by question ID) +- Add new questions +- Add/modify post-actions +- Enable/disable post-actions + +Merging is by `id` for both `questions` and `post_actions`, allowing surgical overrides without duplicating entire configs. + +### Core Components + - **Generator** (`lib/railstart/generator.rb`) - Orchestrates interactive flow - **Command Builder** (`lib/railstart/command_builder.rb`) - Translates answers to `rails new` flags -- **CLI** (`lib/railstart/cli.rb`) - Thor command interface +- **CLI** (`lib/railstart/cli.rb`) - Thor command interface with `--preset` option ## Contributing diff --git a/config/presets/api-only.yaml b/config/presets/api-only.yaml new file mode 100644 index 0000000..a927f21 --- /dev/null +++ b/config/presets/api-only.yaml @@ -0,0 +1,46 @@ +--- +# API-Only Preset - Minimal Rails app for JSON APIs + +questions: + - id: database + choices: + - name: PostgreSQL + value: postgresql + default: true + + - id: css + choices: + - name: None + value: none + default: true + + - id: javascript + choices: + - name: Importmap (default) + value: importmap + default: true + + - id: skip_features + default: + - action_mailer + - action_text + - hotwire + + - id: api_only + default: true + + - id: skip_git + default: false + + - id: skip_docker + default: false + + - id: skip_bundle + default: false + +post_actions: + - id: init_git + enabled: true + + - id: bundle_install + enabled: true diff --git a/config/presets/default.yaml b/config/presets/default.yaml new file mode 100644 index 0000000..c0edbdb --- /dev/null +++ b/config/presets/default.yaml @@ -0,0 +1,45 @@ +--- +# Default Preset - Sensible defaults for quick start +# This preset is automatically loaded when using --default flag without --preset +# Can be overridden by user preset at ~/.config/railstart/presets/default.yaml + +questions: + - id: database + choices: + - name: PostgreSQL + value: postgresql + default: true + + - id: css + choices: + - name: Tailwind + value: tailwind + default: true + + - id: javascript + choices: + - name: Importmap (default) + value: importmap + default: true + + - id: skip_features + default: [] + + - id: api_only + default: false + + - id: skip_git + default: false + + - id: skip_docker + default: false + + - id: skip_bundle + default: false + +post_actions: + - id: init_git + enabled: true + + - id: bundle_install + enabled: true diff --git a/lib/railstart/cli.rb b/lib/railstart/cli.rb index 3898307..f509d8c 100644 --- a/lib/railstart/cli.rb +++ b/lib/railstart/cli.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "thor" +require "fileutils" require_relative "generator" module Railstart @@ -11,22 +12,206 @@ module Railstart # @example Print version # Railstart::CLI.start(%w[version]) class CLI < Thor - desc "new [APP_NAME]", "Start a new interactive Rails app setup" - option :default, type: :boolean, default: false, desc: "Use default configuration without prompting" + def self.exit_on_failure? + true + end + + # Show help by default when no command is given + def self.start(given_args = ARGV, config = {}) + if given_args.empty? + # Show command list instead of requiring a command argument + puts "Railstart - Interactive Rails 8 application generator" + puts "" + puts "Usage:" + puts " railstart init # Generate config files" + puts " railstart new [APP_NAME] [OPTIONS] # Generate a new Rails app" + puts " railstart version # Show version" + puts " railstart help [COMMAND] # Show help for a command" + puts "" + puts "Quick Start:" + puts " railstart init # Create config files (optional)" + puts " railstart new my_app # Interactive mode" + puts " railstart new my_app --default # Use defaults" + puts " railstart new my_app --preset api-only # Use preset" + puts "" + puts "Run 'railstart help init' or 'railstart help new' for details" + return + end + super + end + + # Custom banner to show available options + class << self + # rubocop:disable Style/OptionalBooleanParameter + def banner(command, _namespace = nil, _subcommand = false) + "#{basename} #{command.usage}" + end + + # Override to show only positive form of boolean options + def help(shell, subcommand = false) + # rubocop:enable Style/OptionalBooleanParameter + list = printable_commands(true, subcommand) + Thor::Util.thor_classes_in(self).each do |klass| + list += klass.printable_commands(false) + end + + shell.say "Commands:" + shell.print_table(list, indent: 2, truncate: true) + shell.say + class_options_help(shell) + end + end + + # Override to customize option display + # rubocop:disable Style/OptionalBooleanParameter + def help(command = nil, subcommand = false) + # rubocop:enable Style/OptionalBooleanParameter + if command + if self.class.subcommands.include?(command) + self.class.subcommand_classes[command].help(shell, subcommand) + else + cmd = self.class.all_commands[command] + raise Thor::UndefinedCommandError, "Could not find command '#{command}'." unless cmd + + shell.say "Usage:" + shell.say " #{self.class.banner(cmd)}" + shell.say + if cmd.long_description + shell.say "Description:" + shell.print_wrapped(cmd.long_description, indent: 2) + else + shell.say cmd.description + end + + print_custom_options(cmd) + end + else + super + end + end + + no_commands do + def print_custom_options(cmd) + return unless cmd.options.any? + + shell.say + shell.say "Options:" + cmd.options.each do |name, option| + print_option(name, option) + end + end + + def print_option(name, option) + # For boolean options, only show the positive form + if option.type == :boolean + shell.say " [--#{name}]#{" " * [20 - name.length, 0].max}# #{option.description}" + else + print_non_boolean_option(name, option) + end + end + + def print_non_boolean_option(name, option) + banner_text = option.banner || name.to_s.upcase + padding = [20 - (name.length + banner_text.length + 3), 0].max + shell.say " [--#{name}=#{banner_text}]#{" " * padding}# #{option.description}" + end + end + desc "new [APP_NAME]", "Generate a new Rails 8 application" + long_desc <<~DESC + Generate a new Rails 8 application with an interactive wizard. + + Modes: + - Interactive (default): prompts for each question + - With preset: uses preset config (different questions/defaults), interactive or non-interactive + + Examples: + railstart new my_app # Interactive mode + railstart new my_app --default # Non-interactive with default preset (if exists) + railstart new my_app --preset api-only # Interactive with api-only preset config + railstart new my_app --preset api-only --default # Non-interactive with api-only preset + + Presets are stored in: ~/.config/railstart/presets/*.yaml + DESC + option :default, type: :boolean, default: false, desc: "Use defaults non-interactively" + option :preset, type: :string, desc: "Preset name from ~/.config/railstart/presets/", banner: "NAME" # # @param app_name [String, nil] desired Rails app name, prompted if omitted # @return [void] # @raise [Railstart::Error] when generation fails due to configuration or runtime errors # @example Start wizard with prompts # Railstart::CLI.start(%w[new my_app]) + # @example Use preset + # Railstart::CLI.start(%w[new my_app --preset api-only]) + # @example Use default preset non-interactively + # Railstart::CLI.start(%w[new my_app --default]) def new(app_name = nil) - generator = Generator.new(app_name, use_defaults: options[:default]) + preset_name = determine_preset_name + preset_path = preset_name ? preset_file_for(preset_name) : nil + + config = Config.load(preset_path: preset_path) + + generator = Generator.new( + app_name, + config: config, + use_defaults: options[:default] + ) + generator.run rescue Railstart::Error => e puts "Error: #{e.message}" exit 1 end + desc "init", "Generate config directory and starter files" + long_desc <<~DESC + Creates ~/.config/railstart directory structure with example configuration files. + + This generates: + - ~/.config/railstart/config.yaml (example user config) + - ~/.config/railstart/presets/ directory + - ~/.config/railstart/presets/example.yaml (example preset) + + You can then customize these files for your preferences. + DESC + option :force, type: :boolean, default: false, desc: "Overwrite existing files" + # + # @return [void] + # @example Generate config files + # Railstart::CLI.start(%w[init]) + def init + config_dir = File.expand_path("~/.config/railstart") + presets_dir = File.join(config_dir, "presets") + + # Create directories + FileUtils.mkdir_p(presets_dir) + puts "✓ Created #{config_dir}" + puts "✓ Created #{presets_dir}" + + # Generate example user config + user_config_path = File.join(config_dir, "config.yaml") + if File.exist?(user_config_path) && !options[:force] + puts "⊗ Skipped #{user_config_path} (already exists, use --force to overwrite)" + else + File.write(user_config_path, example_user_config) + puts "✓ Created #{user_config_path}" + end + + # Generate example preset + example_preset_path = File.join(presets_dir, "example.yaml") + if File.exist?(example_preset_path) && !options[:force] + puts "⊗ Skipped #{example_preset_path} (already exists, use --force to overwrite)" + else + File.write(example_preset_path, example_preset_config) + puts "✓ Created #{example_preset_path}" + end + + puts "\n✨ Configuration files initialized!" + puts "\nNext steps:" + puts " 1. Edit ~/.config/railstart/config.yaml to customize defaults" + puts " 2. Create custom presets in ~/.config/railstart/presets/" + puts " 3. Use with: railstart new my_app --preset example" + end + desc "version", "Print Railstart version" # # @return [void] @@ -35,5 +220,104 @@ def new(app_name = nil) def version puts "Railstart v#{Railstart::VERSION}" end + + PRESET_DIR = File.expand_path("~/.config/railstart/presets") + GEM_PRESET_DIR = File.expand_path("../../config/presets", __dir__) + + private + + def determine_preset_name + # Explicit --preset flag takes priority + return options[:preset] if options[:preset] + + # --default maps to "default" preset + return "default" if options[:default] + + nil + end + + def preset_file_for(name) + # Check user presets first + user_path = File.join(PRESET_DIR, "#{name}.yaml") + return user_path if File.exist?(user_path) + + # Fall back to built-in gem presets + gem_path = File.join(GEM_PRESET_DIR, "#{name}.yaml") + return gem_path if File.exist?(gem_path) + + # If explicit --preset was used, raise error + raise Railstart::ConfigLoadError, "Preset '#{name}' not found in #{PRESET_DIR} or gem presets" if options[:preset] + + # For --default with missing preset, return nil (fall back to builtin config) + nil + end + + def example_user_config + <<~YAML + --- + # Railstart User Configuration + # This file overrides built-in defaults for all your Rails projects. + # + # Merge behavior: questions and post_actions are merged by 'id'. + # Override individual fields or add new entries. + + questions: + # Example: Change database default to PostgreSQL + - id: database + choices: + - name: PostgreSQL + value: postgresql + default: true + + # Example: Change CSS default to Tailwind + - id: css + choices: + - name: Tailwind + value: tailwind + default: true + + post_actions: + # Example: Disable bundle install (manage gems manually) + # - id: bundle_install + # enabled: false + + # Example: Add custom post-action + # - id: setup_linting + # name: "Setup RuboCop and StandardRB" + # enabled: true + # command: "bundle add rubocop rubocop-rails standard --group development" + YAML + end + + def example_preset_config + <<~YAML + --- + # Example Preset - Customize this for your use case + # Use with: railstart new my_app --preset example + + questions: + - id: database + choices: + - name: PostgreSQL + value: postgresql + default: true + + - id: css + choices: + - name: Tailwind + value: tailwind + default: true + + - id: api_only + default: false + + post_actions: + - id: init_git + enabled: true + + - id: bundle_install + enabled: true + YAML + end end end diff --git a/lib/railstart/config.rb b/lib/railstart/config.rb index 743661e..c901495 100644 --- a/lib/railstart/config.rb +++ b/lib/railstart/config.rb @@ -17,19 +17,25 @@ class Config class << self # - # Load, merge, and validate configuration from built-in and user sources. + # Load, merge, and validate configuration from built-in, user, and preset sources. # # @param builtin_path [String] path to default config YAML shipped with the gem # @param user_path [String] optional user override YAML path + # @param preset_path [String] optional preset YAML path (third overlay) # @return [Hash] deep-copied, merged, validated configuration hash # @raise [Railstart::ConfigLoadError] when YAML files are missing or unreadable # @raise [Railstart::ConfigValidationError] when validation fails # @example # config = Railstart::Config.load - def load(builtin_path: BUILTIN_CONFIG_PATH, user_path: USER_CONFIG_PATH) + # @example With preset + # config = Railstart::Config.load(preset_path: "~/.config/railstart/presets/api-only.yaml") + def load(builtin_path: BUILTIN_CONFIG_PATH, user_path: USER_CONFIG_PATH, preset_path: nil) builtin = read_yaml(builtin_path, required: true) user = read_yaml(user_path, required: false) + preset = preset_path ? read_yaml(preset_path, required: false) : {} + merged = merge_config(builtin, user) + merged = merge_config(merged, preset) unless preset.empty? validate!(merged) merged end diff --git a/lib/railstart/generator.rb b/lib/railstart/generator.rb index 995435f..383298d 100644 --- a/lib/railstart/generator.rb +++ b/lib/railstart/generator.rb @@ -11,11 +11,13 @@ module Railstart # @example Run generator with provided config # config = Railstart::Config.load # Railstart::Generator.new("blog", config: config).run + # @example Run generator non-interactively + # Railstart::Generator.new("blog", use_defaults: true).run class Generator # # @param app_name [String, nil] preset app name, prompted if nil # @param config [Hash, nil] injected config for testing, defaults to Config.load - # @param use_defaults [Boolean] skip interactive mode and use all defaults + # @param use_defaults [Boolean] skip interactive questions, use config defaults # @param prompt [TTY::Prompt] injectable prompt for testing def initialize(app_name = nil, config: nil, use_defaults: false, prompt: nil) @app_name = app_name @@ -28,13 +30,25 @@ def initialize(app_name = nil, config: nil, use_defaults: false, prompt: nil) # # Run the complete generation flow, prompting the user and invoking Rails. # + # Mode selection: + # - use_defaults: false (default) → interactive wizard + # - use_defaults: true → collect config defaults, show summary, confirm, run + # # @return [void] # @raise [Railstart::ConfigError, Railstart::ConfigValidationError] when configuration is invalid - # @example Run interactively using defaults or custom answers - # Railstart::Generator.new.run + # @example Run interactively + # Railstart::Generator.new("blog").run + # @example Run with defaults (noninteractive questions) + # Railstart::Generator.new("blog", use_defaults: true).run def run ask_app_name unless @app_name - ask_questions + + if @use_defaults + collect_defaults + else + ask_interactive_questions + end + show_summary return unless confirm_proceed? @@ -50,18 +64,13 @@ def ask_app_name end end - def ask_questions - if @use_defaults - collect_defaults - else - ask_interactive_questions - end - end - def collect_defaults Array(@config["questions"]).each do |question| + next if should_skip_question?(question) + + question_id = question["id"] default_value = find_default(question) - @answers[question["id"]] = default_value unless default_value.nil? + @answers[question_id] = default_value unless default_value.nil? end end @@ -160,8 +169,6 @@ def show_summary end def confirm_proceed? - return true if @use_defaults - @prompt.yes?("Proceed with app generation?") end @@ -178,6 +185,7 @@ def generate_app def run_post_actions Dir.chdir(@app_name) + Array(@config["post_actions"]).each { |action| process_post_action(action) } puts "\n✨ Rails app created successfully at ./#{@app_name}" rescue Errno::ENOENT diff --git a/lib/railstart/version.rb b/lib/railstart/version.rb index 6fbf8af..81b70f7 100644 --- a/lib/railstart/version.rb +++ b/lib/railstart/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Railstart - VERSION = "0.1.0" + VERSION = "0.2.0" end diff --git a/test/config_test.rb b/test/config_test.rb index 500d267..2b561d9 100644 --- a/test/config_test.rb +++ b/test/config_test.rb @@ -250,6 +250,116 @@ def test_deep_copy_behavior end end + def test_preset_overlay_merges_on_top_of_user_config + Dir.mktmpdir do |dir| + builtin = { + "questions" => [ + { + "id" => "database", + "type" => "select", + "prompt" => "Which database?", + "choices" => [{ "name" => "SQLite", "value" => "sqlite", "default" => true }], + "rails_flag" => "--database=%s" + } + ] + } + user = { + "questions" => [ + { + "id" => "database", + "choices" => [{ "name" => "PostgreSQL", "value" => "postgresql", "default" => true }] + } + ] + } + preset = { + "questions" => [ + { + "id" => "database", + "choices" => [{ "name" => "MySQL", "value" => "mysql", "default" => true }] + } + ] + } + + builtin_path = write_yaml(dir, "builtin.yaml", builtin) + user_path = write_yaml(dir, "user.yaml", user) + preset_path = write_yaml(dir, "preset.yaml", preset) + + config = Config.load(builtin_path: builtin_path, user_path: user_path, preset_path: preset_path) + + database_question = config["questions"].find { |q| q["id"] == "database" } + # Preset should override user config + assert_equal(["MySQL"], database_question["choices"].map { |c| c["name"] }) + end + end + + def test_preset_changes_generator_default_behavior + Dir.mktmpdir do |dir| + # Builtin config defines database with SQLite default + builtin = { + "questions" => [ + { + "id" => "database", + "type" => "select", + "prompt" => "Which database?", + "choices" => [ + { "name" => "SQLite", "value" => "sqlite3", "default" => true }, + { "name" => "PostgreSQL", "value" => "postgresql" } + ], + "rails_flag" => "--database=%s" + }, + { + "id" => "api_only", + "type" => "yes_no", + "prompt" => "API only?", + "default" => false, + "rails_flag" => "--api" + } + ], + "post_actions" => [] + } + + # Preset overrides database to PostgreSQL and api_only to true + preset = { + "questions" => [ + { + "id" => "database", + "choices" => [ + { "name" => "PostgreSQL", "value" => "postgresql", "default" => true } + ] + }, + { + "id" => "api_only", + "default" => true + } + ] + } + + builtin_path = write_yaml(dir, "builtin.yaml", builtin) + preset_path = write_yaml(dir, "preset.yaml", preset) + + # Load config with preset overlay + config = Config.load(builtin_path: builtin_path, user_path: nil, preset_path: preset_path) + + # Create generator in default mode (use_defaults: true) + fake_prompt = Object.new + fake_prompt.define_singleton_method(:yes?) { |_question, **_kwargs| true } # Confirm generation + + generator = Generator.new("testapp", config: config, use_defaults: true, prompt: fake_prompt) + + # Stub system calls + generator.stub :system, true do + Dir.stub :chdir, nil do + generator.run + end + end + + # Verify preset changed the collected defaults + answers = generator.instance_variable_get(:@answers) + assert_equal "postgresql", answers["database"], "Preset should override database default to PostgreSQL" + assert_equal true, answers["api_only"], "Preset should override api_only default to true" + end + end + private def merged_config(dir, builtin:, user: nil) diff --git a/test/generator_test.rb b/test/generator_test.rb index 9084ed3..3b62043 100644 --- a/test/generator_test.rb +++ b/test/generator_test.rb @@ -31,9 +31,8 @@ def setup def test_default_mode_uses_defaults_without_prompting prompt = Minitest::Mock.new - - # In default mode, NO interactive methods should be called - # (no select, multi_select, yes?, or ask) + # Mock only the final confirmation + prompt.expect :yes?, true, ["Proceed with app generation?"] generator = Generator.new( "testapp", @@ -42,23 +41,20 @@ def test_default_mode_uses_defaults_without_prompting prompt: prompt ) - # Stub system call to avoid actually running rails new generator.stub :system, true do - # Stub Dir.chdir to avoid directory changes Dir.stub :chdir, nil do - # Capture output to verify summary is shown output = capture_io do generator.run end - # Verify summary was shown (transparency) + # Verify summary was shown assert_match(/Summary/, output[0]) assert_match(/testapp/, output[0]) assert_match(/sqlite3/, output[0]) # Default value shown end end - # Verify no interactive prompts were called + # Verify only confirmation was called (no question prompts) prompt.verify end @@ -81,7 +77,6 @@ def test_interactive_mode_asks_questions_and_confirms generator = Generator.new( "testapp", config: @config, - use_defaults: false, prompt: fake_prompt ) @@ -145,7 +140,7 @@ def test_skip_bundle_false_excludes_flag_and_skips_post_action refute_includes command, "--skip-bundle" # Post-action should NOT run (condition fails: skip_bundle != true) - generator = Generator.new("testapp", config: config, use_defaults: true) + generator = Generator.new("testapp", config: config) generator.instance_variable_set(:@answers, answers) actions_run = [] @@ -202,7 +197,7 @@ def test_skip_bundle_true_includes_flag_and_runs_post_action fake_prompt = Object.new fake_prompt.define_singleton_method(:yes?) { |_prompt, **_kwargs| true } - generator = Generator.new("testapp", config: config, use_defaults: false, prompt: fake_prompt) + generator = Generator.new("testapp", config: config, prompt: fake_prompt) generator.instance_variable_set(:@answers, answers) actions_run = []