railstart 0.3.0 → 0.4.0

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: 675d33f74310aa870ad6a167cbd585cc643b3e37096d255732bcf900fa7ab65d
4
+ data.tar.gz: 460628c266ba316d4ad19e148b9e8044912865c98f6c091dd8a1be25653dab71
5
5
  SHA512:
6
- metadata.gz: e180fd3ba85eaad0ca8f0f20726c66b4567e7799000d7bac4e35919d0519deab21772c1f44968d3ff152a74019e33551c8b0f9cba544f814a3f14b006a3ac7de
7
- data.tar.gz: 1733110d3c2f9901d6154a87cd3b479596df1d949fff1b9f02e29cd3415c834eba3eb738d30c7a1f1124c8934338f65ef2fb43e38714775f55c349ae578e7bf7
6
+ metadata.gz: 344c0860107725a5a56e88bf108a7e7a73891da7f0f7678850af5c47469ed63f91c8c22de71743dc68747557f7d34aac2d001814033dee29533f5b343d8b7e0d
7
+ data.tar.gz: 2252beeb778c40a048e1156aebd1786a904fa7cf5edec89fd89d924265b223ff742024ceee8491d5b2a5b459a8a560047be60e43a85c13d771ee96db6edd978a
data/CHANGELOG.md CHANGED
@@ -5,6 +5,32 @@ 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.0] - 2025-11-22
9
+
10
+ ### Added
11
+ - CLI `--preset` flag now accepts explicit `.yaml`/`.yml` file paths in addition to preset names.
12
+ - **Template post-actions**: New `type: template` post-action type for executing full Rails application templates
13
+ - **TemplateRunner**: New `Railstart::TemplateRunner` class wraps Rails' AppGenerator to run templates with proper context
14
+ - **Template variables**: Template actions support `variables` hash for injecting custom instance variables into templates
15
+ - **Built-in variables**: Templates automatically receive `@app_name` and `@answers` instance variables
16
+ - **Template DSL support**: Full access to Rails template DSL (`gem`, `route`, `initializer`, `after_bundle`, etc.)
17
+ - **Error handling**: New `Railstart::TemplateError` for template execution failures with proper error wrapping
18
+ - **Config validation**: Validation for template post-actions (requires `source`, validates `variables` as Hash)
19
+ - **Documentation**: README section explaining template post-actions vs command actions with security guidance
20
+
21
+ ### Changed
22
+ - **Post-action processing**: Refactored to support both command and template execution types
23
+ - **Directory context**: `run_post_actions` now uses block form of `Dir.chdir` for proper scoping
24
+ - **Config validation**: Enhanced `validate_post_action_entry` to handle multiple action types
25
+
26
+ ### Technical
27
+ - New file: `lib/railstart/template_runner.rb` (77 lines, full YARD docs)
28
+ - New test file: `test/template_runner_test.rb` (comprehensive coverage with mocks)
29
+ - Enhanced `lib/railstart/generator.rb` with template execution flow
30
+ - Enhanced `lib/railstart/config.rb` with template action validation
31
+ - Version bump: 0.3.0 → 0.4.0
32
+ - All tests pass (39 runs, 111 assertions, 0 failures)
33
+ - RuboCop clean (20 files inspected, no offenses)
8
34
 
9
35
  ## [0.3.0] - 2025-11-22
10
36
 
data/README.md CHANGED
@@ -309,6 +309,28 @@ post_actions:
309
309
  equals: value # or includes: [array, values]
310
310
  ```
311
311
 
312
+ #### Template Post-Actions
313
+
314
+ Post-actions can now execute full Rails application templates (including [RailsBytes scripts](https://railsbytes.com)) instead of plain shell commands.
315
+
316
+ ```yaml
317
+ post_actions:
318
+ - id: apply_tailwind_dash
319
+ name: "Apply Tailwind dashboard template"
320
+ type: template
321
+ enabled: false # keep disabled unless you trust the source
322
+ prompt: "Run the sample template?"
323
+ source: "https://railsbytes.com/script/zAasQK"
324
+ variables:
325
+ app_label: "internal-tools" # optional instance variables available inside template
326
+ ```
327
+
328
+ Key differences from `command` actions:
329
+
330
+ - 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.
331
+ - `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.
332
+ - Template actions still honor `prompt`, `default`, and `if` just like command actions. Keep remote templates disabled by default unless you explicitly trust them.
333
+
312
334
  ## Development
313
335
 
314
336
  ### 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)
@@ -319,5 +323,14 @@ module Railstart
319
323
  enabled: true
320
324
  YAML
321
325
  end
326
+
327
+ def explicit_preset_path(name)
328
+ return unless name&.match?(/\.ya?ml\z/)
329
+
330
+ expanded = File.expand_path(name)
331
+ return expanded if File.exist?(expanded)
332
+
333
+ raise Railstart::ConfigLoadError, "Preset file '#{name}' not found"
334
+ end
322
335
  end
323
336
  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.
@@ -221,20 +222,30 @@ module Railstart
221
222
  end
222
223
 
223
224
  def run_post_actions
224
- Dir.chdir(@app_name)
225
+ Dir.chdir(@app_name) do
226
+ template_runner = nil
225
227
 
226
- Array(@config["post_actions"]).each { |action| process_post_action(action) }
227
- puts
228
- UI.success("Rails app created successfully at ./#{@app_name}")
228
+ Array(@config["post_actions"]).each do |action|
229
+ template_runner ||= TemplateRunner.new(app_path: Dir.pwd) if template_action?(action)
230
+ process_post_action(action, template_runner)
231
+ end
232
+
233
+ puts
234
+ UI.success("Rails app created successfully at ./#{@app_name}")
235
+ end
229
236
  rescue Errno::ENOENT
230
237
  UI.warning("Could not change to app directory. Post-actions skipped.")
231
238
  end
232
239
 
233
- def process_post_action(action)
240
+ def process_post_action(action, template_runner)
234
241
  return unless should_run_action?(action)
235
242
  return unless confirm_action?(action)
236
243
 
237
- execute_action(action)
244
+ if template_action?(action)
245
+ run_template_action(action, template_runner)
246
+ else
247
+ run_command_action(action)
248
+ end
238
249
  end
239
250
 
240
251
  def confirm_action?(action)
@@ -243,12 +254,33 @@ module Railstart
243
254
  @prompt.yes?(action["prompt"], default: action.fetch("default", true))
244
255
  end
245
256
 
246
- def execute_action(action)
257
+ def run_command_action(action)
247
258
  UI.info(action["name"].to_s)
248
259
  success = system(action["command"])
249
260
  UI.warning("Post-action '#{action["name"]}' failed. Continuing anyway.") unless success
250
261
  end
251
262
 
263
+ def run_template_action(action, template_runner)
264
+ return unless template_runner
265
+
266
+ UI.info(action["name"].to_s)
267
+ source = action["source"]
268
+ variables = template_variables(action)
269
+ template_runner.apply(source, variables: variables)
270
+ rescue TemplateError => e
271
+ UI.warning("Post-action '#{action["name"]}' failed. #{e.message}")
272
+ end
273
+
274
+ def template_variables(action)
275
+ base = { app_name: @app_name, answers: @answers }
276
+ extras = action["variables"].is_a?(Hash) ? action["variables"].transform_keys(&:to_sym) : {}
277
+ base.merge(extras)
278
+ end
279
+
280
+ def template_action?(action)
281
+ action["type"].to_s == "template"
282
+ end
283
+
252
284
  def should_run_action?(action)
253
285
  return false unless action.fetch("enabled", true)
254
286
 
@@ -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.0"
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.0
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