railstart 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3b7aaa542b7abf7c01dbbbb6bfcb71ce8dede1ae7b1535683e4538ac0aad138e
4
- data.tar.gz: 150539a6c55e31e18b33a0852f2e0840057518f5789644073320e060190399bd
3
+ metadata.gz: 5dd1449c02711ec50bf8b11e6ccfc0465768a4ad9271b0d8f08f7c9246aeb69f
4
+ data.tar.gz: 56a1956411fb506f35216355bc68be172d7f20dddc537504b31fe2b7e509fd9b
5
5
  SHA512:
6
- metadata.gz: e180fd3ba85eaad0ca8f0f20726c66b4567e7799000d7bac4e35919d0519deab21772c1f44968d3ff152a74019e33551c8b0f9cba544f814a3f14b006a3ac7de
7
- data.tar.gz: 1733110d3c2f9901d6154a87cd3b479596df1d949fff1b9f02e29cd3415c834eba3eb738d30c7a1f1124c8934338f65ef2fb43e38714775f55c349ae578e7bf7
6
+ metadata.gz: 142b9a8f8ed2fee2f0647ef2cde5c8403080443ad7c312797268874677bc61ac6ab268321bbebc2910a611f1577f1b5d7198e2e8f2fa43666e8602f4838f5416
7
+ data.tar.gz: f1a77fbfdc5bcaffeb5a35db065583f8aab6cbaf9eb74c9b378bfeb7017448aee1d369dd6a063890ea416c478c1cc161617955702c4e2f9700694c4219029b36
data/CHANGELOG.md CHANGED
@@ -5,6 +5,48 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.4.1] - 2025-11-23
9
+
10
+ ### Changed
11
+ - **Init command**: `railstart init` now copies the complete `config/rails8_defaults.yaml` as `~/.config/railstart/config.yaml` instead of generating a minimal example with only 2 questions. Users now see all available configuration options immediately.
12
+ - **README**: Updated preset usage documentation to show both interactive and non-interactive modes immediately after custom preset creation example
13
+ - **multi_select architecture**: `ask_multi_select` now transforms value-based defaults (e.g., "action_mailer") to name-based defaults (e.g., "Action Mailer") when calling TTY::Prompt, allowing configs to use stable IDs while maintaining correct UI display
14
+
15
+ ### Fixed
16
+ - **CRITICAL ARCHITECTURE FIX**: Fixed `multi_select` question defaults to use stable choice **values** (internal IDs like "action_mailer") instead of fragile display **names** (like "Action Mailer"). This prevents preset breakage when display text changes.
17
+ - Updated `lib/railstart/generator.rb` `ask_multi_select` method to transform values → names at the TTY::Prompt boundary
18
+ - Updated `config/presets/api-only.yaml` to use values in defaults
19
+ - Updated all examples in `docs/railstart-preset-builder/SKILL.md` to use values
20
+ - Added comprehensive test coverage (3 new tests) validating transformation and storage behavior
21
+ - **Documentation**: Corrected SKILL.md to explain values-based approach with stability rationale
22
+ - Improved discoverability of configuration options - users no longer need to guess what can be configured
23
+
24
+ ## [0.4.0] - 2025-11-22
25
+
26
+ ### Added
27
+ - CLI `--preset` flag now accepts explicit `.yaml`/`.yml` file paths in addition to preset names.
28
+ - **Template post-actions**: New `type: template` post-action type for executing full Rails application templates
29
+ - **TemplateRunner**: New `Railstart::TemplateRunner` class wraps Rails' AppGenerator to run templates with proper context
30
+ - **Template variables**: Template actions support `variables` hash for injecting custom instance variables into templates
31
+ - **Built-in variables**: Templates automatically receive `@app_name` and `@answers` instance variables
32
+ - **Template DSL support**: Full access to Rails template DSL (`gem`, `route`, `initializer`, `after_bundle`, etc.)
33
+ - **Error handling**: New `Railstart::TemplateError` for template execution failures with proper error wrapping
34
+ - **Config validation**: Validation for template post-actions (requires `source`, validates `variables` as Hash)
35
+ - **Documentation**: README section explaining template post-actions vs command actions with security guidance
36
+
37
+ ### Changed
38
+ - **Post-action processing**: Refactored to support both command and template execution types
39
+ - **Directory context**: `run_post_actions` now uses block form of `Dir.chdir` for proper scoping
40
+ - **Config validation**: Enhanced `validate_post_action_entry` to handle multiple action types
41
+
42
+ ### Technical
43
+ - New file: `lib/railstart/template_runner.rb` (77 lines, full YARD docs)
44
+ - New test file: `test/template_runner_test.rb` (comprehensive coverage with mocks)
45
+ - Enhanced `lib/railstart/generator.rb` with template execution flow
46
+ - Enhanced `lib/railstart/config.rb` with template action validation
47
+ - Version bump: 0.3.0 → 0.4.0
48
+ - All tests pass (39 runs, 111 assertions, 0 failures)
49
+ - RuboCop clean (20 files inspected, no offenses)
8
50
 
9
51
  ## [0.3.0] - 2025-11-22
10
52
 
data/README.md CHANGED
@@ -183,7 +183,11 @@ post_actions:
183
183
  Then use it:
184
184
 
185
185
  ```bash
186
+ # Interactive mode - prompts for each question
186
187
  railstart new myapp --preset my-team
188
+
189
+ # Non-interactive mode - uses all preset defaults
190
+ railstart new myapp --preset my-team --default
187
191
  ```
188
192
 
189
193
  ### Built-in Presets
@@ -209,18 +213,18 @@ See the comprehensive **[Creating Presets Guide](docs/railstart-preset-builder/S
209
213
 
210
214
  ### Initialize Configuration Files
211
215
 
212
- The easiest way to get started with custom configuration is to generate example files:
216
+ The easiest way to get started with custom configuration is to generate template files:
213
217
 
214
218
  ```bash
215
219
  railstart init
216
220
  ```
217
221
 
218
222
  This creates:
219
- - `~/.config/railstart/config.yaml` - Example user config with common customizations
223
+ - `~/.config/railstart/config.yaml` - Complete configuration template (copy of rails8_defaults.yaml with all available options)
220
224
  - `~/.config/railstart/presets/` - Directory for your presets
221
225
  - `~/.config/railstart/presets/example.yaml` - Example preset to get started
222
226
 
223
- You can then edit these files to match your preferences.
227
+ 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.
224
228
 
225
229
  ### Built-in Defaults
226
230
 
@@ -228,24 +232,29 @@ Railstart ships with sensible Rails 8 defaults defined in `config/rails8_default
228
232
 
229
233
  ### Customize for Your Team
230
234
 
231
- You can create `~/.config/railstart/config.yaml` manually or use `railstart init` to generate an example file:
235
+ 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:
232
236
 
233
237
  ```yaml
238
+ # After running `railstart init`, your config.yaml will contain all options.
239
+ # Simply modify the defaults you want to change:
240
+
234
241
  questions:
235
242
  - id: database
236
243
  choices:
237
244
  - name: PostgreSQL (recommended)
238
245
  value: postgresql
239
- default: true # Your team's preference
246
+ default: true # Changed from SQLite to PostgreSQL
247
+
248
+ # ... other questions with their full configuration ...
240
249
 
241
250
  post_actions:
242
251
  - id: bundle_install
243
- enabled: false # Your team manages gems differently
244
-
252
+ enabled: false # Disabled - your team manages gems differently
253
+
245
254
  - id: setup_auth
246
255
  name: "Setup authentication"
247
256
  enabled: true
248
- command: "bundle exec rails generate devise:install"
257
+ command: "bundle exec rails generate devise:install" # New custom action
249
258
  ```
250
259
 
251
260
  **Merge behavior:**
@@ -309,6 +318,28 @@ post_actions:
309
318
  equals: value # or includes: [array, values]
310
319
  ```
311
320
 
321
+ #### Template Post-Actions
322
+
323
+ Post-actions can now execute full Rails application templates (including [RailsBytes scripts](https://railsbytes.com)) instead of plain shell commands.
324
+
325
+ ```yaml
326
+ post_actions:
327
+ - id: apply_tailwind_dash
328
+ name: "Apply Tailwind dashboard template"
329
+ type: template
330
+ enabled: false # keep disabled unless you trust the source
331
+ prompt: "Run the sample template?"
332
+ source: "https://railsbytes.com/script/zAasQK"
333
+ variables:
334
+ app_label: "internal-tools" # optional instance variables available inside template
335
+ ```
336
+
337
+ Key differences from `command` actions:
338
+
339
+ - Set `type: template` and provide a `source` (local path or URL). Railstart streams that template into Rails' own `apply` helper, so all standard DSL commands (`gem`, `route`, `after_bundle`, etc.) are available.
340
+ - `variables` is optional; when present, its keys become instance variables accessible from the template (e.g., `@app_label`). Railstart always exposes `@app_name` and `@answers` for convenience.
341
+ - Template actions still honor `prompt`, `default`, and `if` just like command actions. Keep remote templates disabled by default unless you explicitly trust them.
342
+
312
343
  ## Development
313
344
 
314
345
  ### Setup
data/lib/railstart/cli.rb CHANGED
@@ -237,6 +237,10 @@ module Railstart
237
237
  end
238
238
 
239
239
  def preset_file_for(name)
240
+ if (direct_path = explicit_preset_path(name))
241
+ return direct_path
242
+ end
243
+
240
244
  # Check user presets first
241
245
  user_path = File.join(PRESET_DIR, "#{name}.yaml")
242
246
  return user_path if File.exist?(user_path)
@@ -253,40 +257,9 @@ module Railstart
253
257
  end
254
258
 
255
259
  def example_user_config
256
- <<~YAML
257
- ---
258
- # Railstart User Configuration
259
- # This file overrides built-in defaults for all your Rails projects.
260
- #
261
- # Merge behavior: questions and post_actions are merged by 'id'.
262
- # Override individual fields or add new entries.
263
-
264
- questions:
265
- # Example: Change database default to PostgreSQL
266
- - id: database
267
- choices:
268
- - name: PostgreSQL
269
- value: postgresql
270
- default: true
271
-
272
- # Example: Change CSS default to Tailwind
273
- - id: css
274
- choices:
275
- - name: Tailwind
276
- value: tailwind
277
- default: true
278
-
279
- post_actions:
280
- # Example: Disable bundle install (manage gems manually)
281
- # - id: bundle_install
282
- # enabled: false
283
-
284
- # Example: Add custom post-action
285
- # - id: setup_linting
286
- # name: "Setup RuboCop and StandardRB"
287
- # enabled: true
288
- # command: "bundle add rubocop rubocop-rails standard --group development"
289
- YAML
260
+ # Copy the full rails8_defaults.yaml as the user config template
261
+ defaults_path = File.expand_path("../../config/rails8_defaults.yaml", __dir__)
262
+ File.read(defaults_path)
290
263
  end
291
264
 
292
265
  def example_preset_config
@@ -319,5 +292,14 @@ module Railstart
319
292
  enabled: true
320
293
  YAML
321
294
  end
295
+
296
+ def explicit_preset_path(name)
297
+ return unless name&.match?(/\.ya?ml\z/)
298
+
299
+ expanded = File.expand_path(name)
300
+ return expanded if File.exist?(expanded)
301
+
302
+ raise Railstart::ConfigLoadError, "Preset file '#{name}' not found"
303
+ end
322
304
  end
323
305
  end
@@ -197,12 +197,7 @@ module Railstart
197
197
 
198
198
  issues.concat(validate_question_choices(entry, id || index)) if CHOICE_REQUIRED_TYPES.include?(type)
199
199
  elsif name == "post_actions"
200
- if entry.fetch("enabled", true)
201
- command = entry["command"] || entry[:command]
202
- if command.nil? || command.to_s.strip.empty?
203
- issues << "Post-action #{id || index} is enabled but missing a command"
204
- end
205
- end
200
+ issues.concat(validate_post_action_entry(entry, id || index)) if entry.fetch("enabled", true)
206
201
 
207
202
  if_condition = entry["if"] || entry[:if]
208
203
  if if_condition.is_a?(Hash)
@@ -246,6 +241,32 @@ module Railstart
246
241
  issues
247
242
  end
248
243
 
244
+ def validate_post_action_entry(entry, identifier)
245
+ action_type = (entry["type"] || entry[:type] || "command").to_s
246
+
247
+ case action_type
248
+ when "command"
249
+ command = entry["command"] || entry[:command]
250
+ if command.nil? || command.to_s.strip.empty?
251
+ ["Post-action #{identifier} is enabled but missing a command"]
252
+ else
253
+ []
254
+ end
255
+ when "template"
256
+ issues = []
257
+ source = entry["source"] || entry[:source]
258
+ if source.nil? || source.to_s.strip.empty?
259
+ issues << "Post-action #{identifier} is a template but missing a source"
260
+ end
261
+
262
+ variables = entry["variables"] || entry[:variables]
263
+ issues << "Post-action #{identifier} template variables must be a Hash" if variables && !variables.is_a?(Hash)
264
+ issues
265
+ else
266
+ ["Post-action #{identifier} has unsupported type '#{action_type}'"]
267
+ end
268
+ end
269
+
249
270
  def deep_dup(value)
250
271
  case value
251
272
  when Hash
@@ -25,4 +25,7 @@ module Railstart
25
25
  super(detail)
26
26
  end
27
27
  end
28
+
29
+ # Raised when applying Rails templates fails.
30
+ class TemplateError < Error; end
28
31
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "tty-prompt"
4
4
  require_relative "ui"
5
+ require_relative "template_runner"
5
6
 
6
7
  module Railstart
7
8
  # Orchestrates the interactive Rails app generation flow.
@@ -136,9 +137,16 @@ module Railstart
136
137
  choices = question["choices"].each_with_object({}) do |choice, hash|
137
138
  hash[choice["name"]] = choice["value"]
138
139
  end
139
- defaults = question["default"] || []
140
140
 
141
- @prompt.multi_select(question["prompt"], choices, default: defaults)
141
+ # Transform value-based defaults to name-based defaults for TTY::Prompt
142
+ # Config uses stable values (e.g., "action_mailer"), TTY::Prompt needs display names
143
+ value_defaults = question["default"] || []
144
+ name_defaults = value_defaults.map do |value|
145
+ choice = question["choices"].find { |c| c["value"] == value }
146
+ choice ? choice["name"] : nil
147
+ end.compact
148
+
149
+ @prompt.multi_select(question["prompt"], choices, default: name_defaults)
142
150
  end
143
151
 
144
152
  def ask_yes_no?(question)
@@ -221,20 +229,30 @@ module Railstart
221
229
  end
222
230
 
223
231
  def run_post_actions
224
- Dir.chdir(@app_name)
232
+ Dir.chdir(@app_name) do
233
+ template_runner = nil
225
234
 
226
- Array(@config["post_actions"]).each { |action| process_post_action(action) }
227
- puts
228
- UI.success("Rails app created successfully at ./#{@app_name}")
235
+ Array(@config["post_actions"]).each do |action|
236
+ template_runner ||= TemplateRunner.new(app_path: Dir.pwd) if template_action?(action)
237
+ process_post_action(action, template_runner)
238
+ end
239
+
240
+ puts
241
+ UI.success("Rails app created successfully at ./#{@app_name}")
242
+ end
229
243
  rescue Errno::ENOENT
230
244
  UI.warning("Could not change to app directory. Post-actions skipped.")
231
245
  end
232
246
 
233
- def process_post_action(action)
247
+ def process_post_action(action, template_runner)
234
248
  return unless should_run_action?(action)
235
249
  return unless confirm_action?(action)
236
250
 
237
- execute_action(action)
251
+ if template_action?(action)
252
+ run_template_action(action, template_runner)
253
+ else
254
+ run_command_action(action)
255
+ end
238
256
  end
239
257
 
240
258
  def confirm_action?(action)
@@ -243,12 +261,33 @@ module Railstart
243
261
  @prompt.yes?(action["prompt"], default: action.fetch("default", true))
244
262
  end
245
263
 
246
- def execute_action(action)
264
+ def run_command_action(action)
247
265
  UI.info(action["name"].to_s)
248
266
  success = system(action["command"])
249
267
  UI.warning("Post-action '#{action["name"]}' failed. Continuing anyway.") unless success
250
268
  end
251
269
 
270
+ def run_template_action(action, template_runner)
271
+ return unless template_runner
272
+
273
+ UI.info(action["name"].to_s)
274
+ source = action["source"]
275
+ variables = template_variables(action)
276
+ template_runner.apply(source, variables: variables)
277
+ rescue TemplateError => e
278
+ UI.warning("Post-action '#{action["name"]}' failed. #{e.message}")
279
+ end
280
+
281
+ def template_variables(action)
282
+ base = { app_name: @app_name, answers: @answers }
283
+ extras = action["variables"].is_a?(Hash) ? action["variables"].transform_keys(&:to_sym) : {}
284
+ base.merge(extras)
285
+ end
286
+
287
+ def template_action?(action)
288
+ action["type"].to_s == "template"
289
+ end
290
+
252
291
  def should_run_action?(action)
253
292
  return false unless action.fetch("enabled", true)
254
293
 
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+ require_relative "errors"
5
+
6
+ module Railstart
7
+ # Executes Rails application templates (including RailsBytes scripts)
8
+ # inside a generated application directory.
9
+ #
10
+ # Wraps Rails' own AppGenerator so existing template DSL helpers such as
11
+ # `gem`, `initializer`, `route`, etc. are available without reimplementing
12
+ # them in Railstart.
13
+ class TemplateRunner
14
+ # @param app_path [String] absolute path to the Rails application
15
+ # @param generator_factory [#call] optional factory for injecting a
16
+ # generator (mainly used in tests)
17
+ # @param shell [Thor::Shell] Thor shell instance for output
18
+ def initialize(app_path:, generator_factory: nil, shell: Thor::Base.shell.new)
19
+ @app_path = app_path
20
+ @shell = shell
21
+ @generator_factory = generator_factory
22
+ end
23
+
24
+ # Apply a Rails template located at +source+.
25
+ #
26
+ # @param source [String] file path or URL
27
+ # @param variables [Hash] instance variables injected into the template
28
+ # @return [void]
29
+ # @raise [Railstart::TemplateError] when Rails cannot be loaded or
30
+ # template execution fails
31
+ def apply(source, variables: {})
32
+ raise TemplateError, "Template source must be provided" if source.to_s.strip.empty?
33
+
34
+ generator = build_generator
35
+ assign_variables(generator, variables)
36
+ generator.apply(source)
37
+ rescue TemplateError
38
+ raise
39
+ rescue LoadError => e
40
+ raise TemplateError, "Rails must be installed to run template post-actions: #{e.message}"
41
+ rescue StandardError => e
42
+ raise TemplateError, "Failed to apply template #{source}: #{e.message}"
43
+ end
44
+
45
+ private
46
+
47
+ def assign_variables(generator, variables)
48
+ Array(variables).each do |key, value|
49
+ generator.instance_variable_set(:"@#{key}", value)
50
+ end
51
+ end
52
+
53
+ def build_generator
54
+ generator = generator_factory.call(@app_path)
55
+ if generator.respond_to?(:destination_root=)
56
+ generator.destination_root = @app_path
57
+ elsif generator.respond_to?(:destination_root)
58
+ generator.instance_variable_set(:@destination_root, @app_path)
59
+ end
60
+ generator
61
+ end
62
+
63
+ def generator_factory
64
+ @generator_factory ||= default_generator_factory
65
+ end
66
+
67
+ def default_generator_factory
68
+ require "rails/generators"
69
+ require "rails/generators/rails/app/app_generator"
70
+
71
+ shell = @shell
72
+ lambda do |_app_path|
73
+ Rails::Generators::AppGenerator.new([], {}, shell: shell)
74
+ end
75
+ end
76
+ end
77
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Railstart
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.1"
5
5
  end
data/lib/railstart.rb CHANGED
@@ -5,6 +5,7 @@ require_relative "railstart/errors"
5
5
  require_relative "railstart/config"
6
6
  require_relative "railstart/command_builder"
7
7
  require_relative "railstart/generator"
8
+ require_relative "railstart/template_runner"
8
9
  require_relative "railstart/cli"
9
10
 
10
11
  # Main namespace for the Railstart gem.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: railstart
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - dpaluy
@@ -78,6 +78,7 @@ files:
78
78
  - lib/railstart/config.rb
79
79
  - lib/railstart/errors.rb
80
80
  - lib/railstart/generator.rb
81
+ - lib/railstart/template_runner.rb
81
82
  - lib/railstart/ui.rb
82
83
  - lib/railstart/version.rb
83
84
  homepage: https://github.com/dpaluy/railstart