Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .agents/skills/railstart-coder/references/cli-and-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ Important CLI behavior:
- `--preset NAME` resolves user presets first from `~/.config/railstart/presets/NAME.yaml`, then gem presets from `config/presets/NAME.yaml`.
- `--preset` also accepts an explicit `.yaml` or `.yml` path.
- `--default` maps to the `default` preset name. If no `default` preset exists, Railstart falls back to the built-in config.
- `railstart init` copies the full `config/rails8_defaults.yaml` into `~/.config/railstart/config.yaml` and creates `~/.config/railstart/presets/example.yaml`.
- `railstart init` creates a minimal override at `~/.config/railstart/config.yaml` and an example preset at `~/.config/railstart/presets/example.yaml`.
- Keep user overrides minimal. Overriding a `choices` array replaces the full built-in array, so copied full configs can hide choices added by future Railstart releases.
- There is no dry-run mode. Use `CommandBuilder` tests or injected generator tests when you need to verify flags without creating a Rails app.

Current built-in preset files:
Expand Down Expand Up @@ -65,7 +66,7 @@ These are the current questions defined in `config/rails8_defaults.yaml`.
| ID | Type | Built-in choices/default | Rails flag behavior |
| --- | --- | --- | --- |
| `database` | `select` | `sqlite3` default, `postgresql`, `mysql` | Question-level `--database=%<value>s` |
| `css` | `select` | `tailwind` default, `bootstrap`, `bulma`, `postcss`, `sass`, `none` | Question-level `--css=%{value}` except custom choice-level overrides you may add in presets |
| `css` | `select` | `tailwind` default, `bootstrap`, `bulma`, `postcss`, `sass`, `none` | Question-level `--css=%{value}`; built-in `none` overrides it with `--skip-css` |
| `javascript` | `select` | `importmap` default, `bun`, `esbuild`, `rollup`, `webpack`, `none` | Choice-level flags for built-in choices |
| `skip_features` | `multi_select` | empty default | Selected choices emit their own skip flags |
| `api_only` | `yes_no` | `false` | `--api` when true |
Expand Down
21 changes: 19 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ jobs:
ruby:
- "3.4"
- "4.0"
- "4.0.5"

steps:
- uses: actions/checkout@v5
Expand All @@ -26,5 +27,21 @@ jobs:
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
- name: Run the default task
run: bundle exec rake
- name: Run tests
run: bundle exec rake test
- name: Run RuboCop
run: bundle exec rubocop
- name: Generate documentation
run: bundle exec rake yard
- name: Build gem
run: gem build railstart.gemspec
- name: Check executable syntax
run: ruby -c exe/railstart
- name: Smoke test executable
run: bundle exec exe/railstart version
- name: Install Rails for shipped-config contract test
run: gem install rails --version "~> 8.1" --no-document
- name: Validate shipped config against Rails
env:
RAILS_CONTRACT: "1"
run: bundle exec ruby -Itest test/rails_cli_contract_test.rb
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: "3.4.5"
ruby-version: "4.0.5"
bundler-cache: true

- uses: rubygems/release-gem@v1
1 change: 1 addition & 0 deletions .ruby-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
4.0.5
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ bundle exec rake test && bundle exec rubocop
- **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 (see `railstart.gemspec`)
- **Ruby Version:** 3.2.0 minimum; local and release baseline is 4.0.5 (see `railstart.gemspec` and `.ruby-version`)

---

Expand Down
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.5.0] - 2026-07-11

### Added
- **Ruby 4.0.5 coverage**: Pinned development and release workflows to Ruby 4.0.5 and added exact Ruby 4.0.5 coverage to the existing CI matrix
- **Rails contract coverage**: Added opt-in `rails new --pretend` checks for the built-in configuration and every shipped preset
- **Config validation**: Added fail-fast validation for duplicate IDs, invalid defaults, malformed flags, dependency conditions, post-action conditions, and template variable names

### Changed
- **Development dependencies**: Updated Minitest to 6.0.6, Rake to 13.4.2, RuboCop to 1.88.2, and YARD to 0.9.44
- **Safe command execution**: Rails generation now executes an argument vector instead of a shell-interpreted command string
- **Init command**: `railstart init` now generates minimal user overrides so future built-in choices and flags are not shadowed by a copied full configuration
- **CI quality gate**: CI now runs tests, RuboCop, YARD, gem packaging, executable smoke checks, and Rails CLI contract checks

### Fixed
- **API-only preset**: The CSS `none` choice now emits `--skip-css` instead of the invalid `--css=none`
- **Documentation build**: Declared the Markdown documentation dependencies and made YARD fail on warnings

## [0.4.5] - 2026-04-30

### Added
Expand Down
6 changes: 4 additions & 2 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ gemspec

gem "minitest", "~> 6.0"
gem "minitest-mock"
gem "rake", "~> 13.0"
gem "rubocop", "~> 1.86"
gem "rake", "~> 13.4"
gem "rdoc", "~> 6.0"
gem "redcarpet", "~> 3.6"
gem "rubocop", "~> 1.88"
gem "yard", "~> 0.9"
36 changes: 25 additions & 11 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
railstart (0.4.5)
railstart (0.5.0)
thor
tty-box
tty-prompt
Expand All @@ -10,26 +10,36 @@ GEM
remote: https://rubygems.org/
specs:
ast (2.4.3)
date (3.5.1)
drb (2.2.3)
json (2.19.4)
language_server-protocol (3.17.0.5)
erb (6.0.4)
json (2.20.0)
language_server-protocol (3.17.0.6)
lint_roller (1.1.0)
minitest (6.0.3)
minitest (6.0.6)
drb (~> 2.0)
prism (~> 1.5)
minitest-mock (5.27.0)
parallel (1.28.0)
parallel (2.1.0)
parser (3.3.11.1)
ast (~> 2.4.1)
racc
pastel (0.8.0)
tty-color (~> 0.5)
prism (1.9.0)
psych (5.4.0)
date
stringio
racc (1.8.1)
rainbow (3.1.1)
rake (13.3.1)
rake (13.4.2)
rdoc (6.17.0)
erb
psych (>= 4.0.0)
tsort
redcarpet (3.6.1)
regexp_parser (2.12.0)
rubocop (1.86.1)
rubocop (1.88.2)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
Expand All @@ -40,16 +50,18 @@ GEM
rubocop-ast (>= 1.49.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.49.1)
rubocop-ast (1.50.0)
parser (>= 3.3.7.2)
prism (~> 1.7)
ruby-progressbar (1.13.0)
stringio (3.2.0)
strings (0.2.1)
strings-ansi (~> 0.2)
unicode-display_width (>= 1.5, < 3.0)
unicode_utils (~> 1.4)
strings-ansi (0.2.0)
thor (1.5.0)
tsort (0.2.0)
tty-box (0.7.0)
pastel (~> 0.8)
strings (~> 0.2.0)
Expand All @@ -67,7 +79,7 @@ GEM
unicode-display_width (2.6.0)
unicode_utils (1.4.0)
wisper (2.0.1)
yard (0.9.38)
yard (0.9.44)

PLATFORMS
arm64-darwin-24
Expand All @@ -77,8 +89,10 @@ DEPENDENCIES
minitest (~> 6.0)
minitest-mock
railstart!
rake (~> 13.0)
rubocop (~> 1.86)
rake (~> 13.4)
rdoc (~> 6.0)
redcarpet (~> 3.6)
rubocop (~> 1.88)
yard (~> 0.9)

BUNDLED WITH
Expand Down
24 changes: 12 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ Interactive CLI wizard for generating Rails 8 applications with customizable con

Think of it as `rails new` with an opinion and a friendly interactive experience.

Requires Ruby 3.2 or newer. Development and release tooling use Ruby 4.0.5.

## Installation

```bash
Expand Down Expand Up @@ -231,32 +233,27 @@ railstart init
```

This creates:
- `~/.config/railstart/config.yaml` - Complete configuration template (copy of rails8_defaults.yaml with all available options)
- `~/.config/railstart/config.yaml` - Minimal example containing only a few global overrides
- `~/.config/railstart/presets/` - Directory for your presets
- `~/.config/railstart/presets/example.yaml` - Example preset to get started

The generated `config.yaml` shows all available questions, choices, flags, and post-actions. You can delete or comment out sections you don't want to customize, and modify the defaults for sections you do want to change.
Keep `config.yaml` minimal so future built-in choices and flags continue to reach your installation. The full shipped schema remains available in `config/rails8_defaults.yaml`, while `examples/config.yml` provides a larger copyable example.

### 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

You can create `~/.config/railstart/config.yaml` manually or use `railstart init` to generate a complete template file. The template includes all available options, so you can simply modify the defaults you want to change:
You can create `~/.config/railstart/config.yaml` manually or use `railstart init` to generate a minimal starting point. Add only the defaults you want to change:

```yaml
# After running `railstart init`, your config.yaml will contain all options.
# Simply modify the defaults you want to change:
# After running `railstart init`, config.yaml contains a few example overrides.
# Keep only the overrides you want to apply to every run:

questions:
- id: database
choices:
- name: PostgreSQL (recommended)
value: postgresql
default: true # Changed from SQLite to PostgreSQL

# ... other questions with their full configuration ...
default: postgresql

post_actions:
- id: bundle_install
Expand Down Expand Up @@ -356,6 +353,9 @@ Key differences from `command` actions:
### Setup

```bash
# Install the required Ruby version
mise install ruby@4.0.5

# Install dependencies
bundle install

Expand Down Expand Up @@ -394,7 +394,7 @@ bundle exec rubocop
bundle exec rubocop -a

# Full check
bundle exec rake test && bundle exec rubocop
bundle exec rake test && bundle exec rubocop && bundle exec rake yard && gem build railstart.gemspec
```

## Architecture
Expand Down
2 changes: 1 addition & 1 deletion Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ begin
require "yard"
YARD::Rake::YardocTask.new(:yard) do |t|
t.files = ["lib/**/*.rb"]
t.options = ["--output-dir", "doc", "--markup", "markdown"]
t.options = ["--output-dir", "doc", "--markup", "markdown", "--fail-on-warning"]
end
rescue LoadError
# YARD not available
Expand Down
1 change: 1 addition & 0 deletions config/presets/api-only.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ questions:
- name: None
value: none
default: true
rails_flag: "--skip-css"

- id: javascript
choices:
Expand Down
1 change: 1 addition & 0 deletions config/rails8_defaults.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ questions:
value: sass
- name: None (skip CSS setup)
value: none
rails_flag: "--skip-css"
rails_flag: "--css=%{value}"

- id: javascript
Expand Down
23 changes: 20 additions & 3 deletions lib/railstart/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -257,9 +257,26 @@ def preset_file_for(name)
end

def example_user_config
# Copy the full rails8_defaults.yaml as the user config template
defaults_path = File.expand_path("../../config/rails8_defaults.yaml", __dir__)
File.read(defaults_path)
<<~YAML
---
# User overrides for every Railstart run.
#
# Keep this file minimal. Entries merge by id with Railstart's built-in
# configuration. Overriding a choices array replaces the entire array.
# See config/rails8_defaults.yaml in the Railstart repository for the
# full schema and every available question, choice, and post-action.

questions:
- id: database
default: postgresql

- id: skip_docker
default: true

post_actions:
- id: bundle_install
enabled: false
YAML
end

def example_preset_config
Expand Down
22 changes: 20 additions & 2 deletions lib/railstart/command_builder.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require "shellwords"

module Railstart
# Translates configuration and user answers into a `rails new` command string.
#
Expand All @@ -23,12 +25,28 @@ class << self
# @example
# Railstart::CommandBuilder.build("todo", config, answers)
def build(app_name, config, answers)
flags = collect_flags(config["questions"], answers)
"rails new #{app_name} #{flags.join(" ")}".strip
arguments(app_name, config, answers).map { |argument| display_argument(argument) }.join(" ")
end

# Build process arguments for `rails new` without shell interpretation.
#
# @param app_name [String] target Rails app name
# @param config [Hash] merged configuration from {Railstart::Config.load}
# @param answers [Hash] user answers keyed by question id
# @return [Array<String>] executable and arguments suitable for `system(*arguments)`
def arguments(app_name, config, answers)
["rails", "new", app_name.to_s, *collect_flags(config["questions"], answers)]
end

private

def display_argument(argument)
value = argument.to_s
return value if value.match?(%r{\A[\w@%+=:,./-]+\z})

Shellwords.escape(value)
end

def collect_flags(questions, answers)
flags = []
Array(questions).each do |question|
Expand Down
Loading