Type: Single Ruby gem (interactive Rails application starter)
Tech Stack: Ruby 3.2+, Rails 8+, Thor (CLI), TTY::Prompt (interactive prompts), Minitest (testing)
Purpose: Provides an opinionated, interactive CLI wizard (railstart new) that guides developers through Rails 8 project setup with customizable defaults and post-generation hooks.
# Install dependencies
bundle install
# Run tests
bundle exec rake test
# Lint code
bundle exec rubocop
# Full check (lint + test)
bundle exec rake test && bundle exec rubocop- Code Style: Ruby conventions, frozen_string_literal in all files
- Linting: RuboCop (see
.rubocop.yml) - Testing Framework: Minitest (prefer assertions over mocks; integration > unit)
- Commits: Conventional Commits (feat:, fix:, test:, docs:, refactor:)
- Ruby Version: 3.2.0 minimum; local and release baseline is 4.0.5 (see
railstart.gemspecand.ruby-version)
- CLI Executable:
exe/railstart- Script that launches the CLI - Main Module:
lib/railstart.rb- DefinesRailstartnamespace - Version:
lib/railstart/version.rb-Railstart::VERSIONconstant
- CLI Interface:
lib/railstart/cli.rb- Thor commands (railstart new) - Config System:
lib/railstart/config.rb- Load/merge/validate YAML configs - Generator:
lib/railstart/generator.rb- Orchestrate interactive flow - Command Builder:
lib/railstart/command_builder.rb- Translate answers →rails newflags
- Built-in Defaults:
config/rails8_defaults.yaml- Ships with gem; defines all questions - User Overrides:
~/.railstart/config.yaml- Optional user customization
- Config Tests:
test/config_test.rb- Config loading, merging, validation - Command Builder Tests:
test/command_builder_test.rb- Flag translation - Generator Tests:
test/generator_test.rb- Flow orchestration (stubs system calls) - Integration:
test/railstart_test.rb- Current test file (expand as needed)
Rules:
- ✅ DO: Merge configs by unique
id(questions, post-actions); never naive array merge - ✅ DO: User config can override/add questions; built-in choices are replaced entirely if user specifies
- ❌ DON'T: Naive
deep_mergeon question/action arrays (creates unmergeable state)
Example implementation: See lib/railstart/config.rb - merge_questions, merge_post_actions methods
Structure:
questions:
- id: database # unique identifier
type: select|multi_select|yes_no|input
prompt: "User-facing question"
choices:
- name: "Display name"
value: "internal_value"
default: true # at most one per select
rails_flag: "--flag=%{value}"
rails_flag: "--database=%{value}" # interpolated with %{value}
post_actions:
- id: init_git # unique identifier
name: "Human readable name"
enabled: true # can be disabled by user config
command: "git init && ..."
if: # optional condition
question: question_id
equals: valueRules:
- ✅ DO: Each question type maps to one TTY::Prompt call (
select,multi_select,yes?,ask) - ✅ DO: Extract defaults from config before calling TTY
- ❌ DON'T: Add logic inside prompt calls; pre-compute everything
Pattern:
def ask_question(question, answers)
case question['type']
when 'select'
choices = question['choices'].map { |c| [c['name'], c['value']] }
@prompt.select(question['prompt'], choices, default: find_default(question))
when 'multi_select'
choices = question['choices'].map { |c| [c['name'], c['value']] }
@prompt.multi_select(question['prompt'], choices, default: question['default'] || [])
when 'yes_no'
@prompt.yes?(question['prompt'], default: question.fetch('default', false))
when 'input'
@prompt.ask(question['prompt'], default: question['default'])
end
endRules:
- ✅ DO: Separate concerns: load config → select mode → ask questions → build command → execute → post-actions
- ✅ DO: Validate config early (before prompting)
- ✅ DO: Stub
system()calls in tests (don't actually runrails new)
Flow:
1. Load & validate config
2. Prompt for app name (if not provided)
3. Ask mode: Default or Customize?
4. If Default: use all config defaults, skip questions
5. If Customize: ask each question (respecting depends_on conditions)
6. Show summary, confirm
7. Build `rails new` command from answers
8. Execute (abort on failure)
9. chdir into app, run enabled post-actions
Rules:
- ✅ DO: Keep this pure (no side effects); test with simple input/output
- ✅ DO: Support
%{value}interpolation in rails_flag strings - ✅ DO: Handle multi_select by iterating selected choices and applying their individual flags
Pattern:
def build_rails_command(app_name, answers)
flags = []
config['questions'].each do |q|
answer = answers[q['id']]
next unless answer
case q['type']
when 'multi_select'
q['choices'].each do |choice|
if answer.include?(choice['value'])
add_flags(flags, choice, choice['value'])
end
end
else
add_flags(flags, q, answer)
end
end
"rails new #{app_name} #{flags.join(' ')}"
end
private
def add_flags(flags, source, value)
flag_list = source['rails_flags'] || [source['rails_flag']].compact
flag_list.each do |flag|
interpolated = flag.gsub('%{value}', value.to_s)
flags << interpolated
end
end- Gem entry:
lib/railstart.rb- DefinesRailstartmodule, version - Config class:
lib/railstart/config.rb-Railstart::Config.loadreturns merged config - Built-in config:
config/rails8_defaults.yaml- Reference for all available question types
- CLI wrapper:
lib/railstart/cli.rb- Thor commands - Generator:
lib/railstart/generator.rb- Main orchestration logic - Command builder:
lib/railstart/command_builder.rb- Pure translation layer
- Config test: Tests for merge correctness, validation, override behavior
- Builder test: Tests for flag generation with various input combinations
- Generator test (stubs): Mock
system(),Dir.chdir(), TTY::Prompt
- Config merging is critical - Naive
deep_mergeon arrays will create unmergeable state. Always merge byid. - TTY::Prompt returns unwrapped values -
selectreturns thevalue(not choice object); handle accordingly. - Flag interpolation - Use
%{value}(not#{value}); interpolate at build time. - Validation must run early - Check config validity before any prompting to fail fast.
- System calls in tests - Always stub
system()andDir.chdir(); never actually runrails newin tests.
# Find config loading logic
rg -n "def load" lib/railstart/config.rb
# Find question type handling
rg -n "when.*select|multi_select|yes_no|input" lib/railstart/
# Find flag building
rg -n "rails_flag" lib/railstart/command_builder.rb
# Find all tests
find test -name "*.rb" -type f
# Search for TODOs
rg -n "TODO|FIXME" lib/ test/Before creating a pull request:
# Run full test suite
bundle exec rake test
# Run linter and auto-fix
bundle exec rubocop -a
# Verify no broken requires
ruby -c exe/railstart
# Check for unused variables/code
bundle exec rubocop --lint lib/ test/- Code changes pass linting (
bundle exec rubocop) - All tests pass (
bundle exec rake test) - New feature has corresponding tests
- Config changes validated (no merge ambiguities)
- README/CHANGELOG updated if user-facing changes
- Gem still installs cleanly (
gem build)