AGENTS.md · git:20260711.4af5ac5 · 2026-07-11 · sha256 7a7a3b6ca8af0b27
AGENTS.md git:20260711.4af5ac5A
Immutable. This exact content never changes and is served at /api/v1/blob/7a7a3b6ca8af0b27.
# Railstart Gem - Agent Guidance
## Project Snapshot
**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.
---
## Root Setup Commands
```bash
# 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
```
---
## Universal Conventions
- **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.gemspec` and `.ruby-version`)
---
## Gem Structure & Key Files
### Main Entry Points
- **CLI Executable:** `exe/railstart` - Script that launches the CLI
- **Main Module:** `lib/railstart.rb` - Defines `Railstart` namespace
- **Version:** `lib/railstart/version.rb` - `Railstart::VERSION` constant
### Implementation Files (to create/modify)
- **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 new` flags
### Configuration
- **Built-in Defaults:** `config/rails8_defaults.yaml` - Ships with gem; defines all questions
- **User Overrides:** `~/.railstart/config.yaml` - Optional user customization
### Tests
- **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)
---
## Core Patterns & Conventions
### 1. **Config System (YAML-driven, ID-based merge)**
**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_merge` on question/action arrays (creates unmergeable state)
**Example implementation:** See `lib/railstart/config.rb` - `merge_questions`, `merge_post_actions` methods
**Structure:**
```yaml
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: value
```
### 2. **TTY::Prompt Integration (thin adapter)**
**Rules:**
- ✅ 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:**
```ruby
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
end
```
### 3. **Generator Flow (orchestration)**
**Rules:**
- ✅ 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 run `rails 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
```
### 4. **Command Building (pure function)**
**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:**
```ruby
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
```
---
## Touch Points & Key Files to Know
### Configuration & Setup
- **Gem entry:** `lib/railstart.rb` - Defines `Railstart` module, version
- **Config class:** `lib/railstart/config.rb` - `Railstart::Config.load` returns merged config
- **Built-in config:** `config/rails8_defaults.yaml` - Reference for all available question types
### Interactive Flow
- **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
### Testing Patterns
- **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
---
## Common Gotchas
1. **Config merging is critical** - Naive `deep_merge` on arrays will create unmergeable state. Always merge by `id`.
2. **TTY::Prompt returns unwrapped values** - `select` returns the `value` (not choice object); handle accordingly.
3. **Flag interpolation** - Use `%{value}` (not `#{value}`); interpolate at build time.
4. **Validation must run early** - Check config validity before any prompting to fail fast.
5. **System calls in tests** - Always stub `system()` and `Dir.chdir()`; never actually run `rails new` in tests.
---
## JIT Index - Quick Find Commands
```bash
# 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/
```
---
## Pre-PR Checklist
Before creating a pull request:
```bash
# 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/
```
---
## Definition of Done
- [ ] 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`)