rails-metro 0.1.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.
Files changed (150) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +17 -0
  3. data/LICENSE +21 -0
  4. data/README.md +267 -0
  5. data/exe/metro +3 -0
  6. data/lib/rails/metro/cli.rb +162 -0
  7. data/lib/rails/metro/config.rb +40 -0
  8. data/lib/rails/metro/feature_pack.rb +59 -0
  9. data/lib/rails/metro/feature_registry.rb +147 -0
  10. data/lib/rails/metro/packs/aasm_pack.rb +29 -0
  11. data/lib/rails/metro/packs/ab_testing_pack.rb +39 -0
  12. data/lib/rails/metro/packs/action_text_pack.rb +29 -0
  13. data/lib/rails/metro/packs/activeadmin_pack.rb +32 -0
  14. data/lib/rails/metro/packs/activity_feed_pack.rb +31 -0
  15. data/lib/rails/metro/packs/acts_as_list_pack.rb +28 -0
  16. data/lib/rails/metro/packs/acts_as_votable_pack.rb +32 -0
  17. data/lib/rails/metro/packs/administrate_pack.rb +31 -0
  18. data/lib/rails/metro/packs/after_party_pack.rb +31 -0
  19. data/lib/rails/metro/packs/ahoy_pack.rb +31 -0
  20. data/lib/rails/metro/packs/amplitude_pack.rb +45 -0
  21. data/lib/rails/metro/packs/annotate_pack.rb +30 -0
  22. data/lib/rails/metro/packs/api_cors_pack.rb +41 -0
  23. data/lib/rails/metro/packs/api_docs_pack.rb +34 -0
  24. data/lib/rails/metro/packs/api_guard_pack.rb +31 -0
  25. data/lib/rails/metro/packs/api_serialization_pack.rb +38 -0
  26. data/lib/rails/metro/packs/app_linting_pack.rb +35 -0
  27. data/lib/rails/metro/packs/audit_trail_pack.rb +31 -0
  28. data/lib/rails/metro/packs/authentication_pack.rb +31 -0
  29. data/lib/rails/metro/packs/authorization_pack.rb +31 -0
  30. data/lib/rails/metro/packs/avo_pack.rb +32 -0
  31. data/lib/rails/metro/packs/aws_ses_pack.rb +45 -0
  32. data/lib/rails/metro/packs/background_jobs_pack.rb +34 -0
  33. data/lib/rails/metro/packs/blazer_pack.rb +62 -0
  34. data/lib/rails/metro/packs/breadcrumbs_pack.rb +31 -0
  35. data/lib/rails/metro/packs/caching_pack.rb +30 -0
  36. data/lib/rails/metro/packs/charting_pack.rb +67 -0
  37. data/lib/rails/metro/packs/circuit_breaker_pack.rb +34 -0
  38. data/lib/rails/metro/packs/clicky_pack.rb +37 -0
  39. data/lib/rails/metro/packs/cloudinary_pack.rb +42 -0
  40. data/lib/rails/metro/packs/components_pack.rb +30 -0
  41. data/lib/rails/metro/packs/counter_culture_pack.rb +29 -0
  42. data/lib/rails/metro/packs/data_migrate_pack.rb +29 -0
  43. data/lib/rails/metro/packs/datadog_pack.rb +41 -0
  44. data/lib/rails/metro/packs/deployment_pack.rb +30 -0
  45. data/lib/rails/metro/packs/devise_pack.rb +33 -0
  46. data/lib/rails/metro/packs/doorkeeper_pack.rb +32 -0
  47. data/lib/rails/metro/packs/dotenv_pack.rb +36 -0
  48. data/lib/rails/metro/packs/elasticsearch_pack.rb +31 -0
  49. data/lib/rails/metro/packs/encryption_pack.rb +37 -0
  50. data/lib/rails/metro/packs/error_tracking_pack.rb +40 -0
  51. data/lib/rails/metro/packs/event_sourcing_pack.rb +31 -0
  52. data/lib/rails/metro/packs/faraday_pack.rb +42 -0
  53. data/lib/rails/metro/packs/fathom_pack.rb +36 -0
  54. data/lib/rails/metro/packs/feature_flags_pack.rb +34 -0
  55. data/lib/rails/metro/packs/friendly_id_pack.rb +31 -0
  56. data/lib/rails/metro/packs/geocoder_pack.rb +38 -0
  57. data/lib/rails/metro/packs/good_job_pack.rb +34 -0
  58. data/lib/rails/metro/packs/google_analytics_pack.rb +42 -0
  59. data/lib/rails/metro/packs/google_tag_manager_pack.rb +51 -0
  60. data/lib/rails/metro/packs/graphql_pack.rb +31 -0
  61. data/lib/rails/metro/packs/health_check_pack.rb +34 -0
  62. data/lib/rails/metro/packs/heap_pack.rb +39 -0
  63. data/lib/rails/metro/packs/honeybadger_pack.rb +37 -0
  64. data/lib/rails/metro/packs/hotwire_livereload_pack.rb +28 -0
  65. data/lib/rails/metro/packs/icons_pack.rb +28 -0
  66. data/lib/rails/metro/packs/invisible_captcha_pack.rb +35 -0
  67. data/lib/rails/metro/packs/kredis_pack.rb +31 -0
  68. data/lib/rails/metro/packs/lemon_squeezy_pack.rb +36 -0
  69. data/lib/rails/metro/packs/letter_opener_pack.rb +31 -0
  70. data/lib/rails/metro/packs/letter_opener_web_pack.rb +32 -0
  71. data/lib/rails/metro/packs/logging_pack.rb +36 -0
  72. data/lib/rails/metro/packs/madmin_pack.rb +32 -0
  73. data/lib/rails/metro/packs/mailgun_pack.rb +44 -0
  74. data/lib/rails/metro/packs/maintenance_mode_pack.rb +30 -0
  75. data/lib/rails/metro/packs/maintenance_tasks_pack.rb +32 -0
  76. data/lib/rails/metro/packs/markdown_pack.rb +48 -0
  77. data/lib/rails/metro/packs/matomo_pack.rb +48 -0
  78. data/lib/rails/metro/packs/meilisearch_pack.rb +37 -0
  79. data/lib/rails/metro/packs/mixpanel_pack.rb +47 -0
  80. data/lib/rails/metro/packs/mobility_pack.rb +32 -0
  81. data/lib/rails/metro/packs/mollie_pack.rb +36 -0
  82. data/lib/rails/metro/packs/multitenancy_pack.rb +35 -0
  83. data/lib/rails/metro/packs/newrelic_pack.rb +52 -0
  84. data/lib/rails/metro/packs/notifications_pack.rb +31 -0
  85. data/lib/rails/metro/packs/omniauth_pack.rb +49 -0
  86. data/lib/rails/metro/packs/paddle_pack.rb +37 -0
  87. data/lib/rails/metro/packs/pagination_pack.rb +44 -0
  88. data/lib/rails/metro/packs/passwordless_pack.rb +32 -0
  89. data/lib/rails/metro/packs/payments_pack.rb +35 -0
  90. data/lib/rails/metro/packs/paypal_pack.rb +35 -0
  91. data/lib/rails/metro/packs/pdf_pack.rb +45 -0
  92. data/lib/rails/metro/packs/performance_pack.rb +37 -0
  93. data/lib/rails/metro/packs/pghero_pack.rb +31 -0
  94. data/lib/rails/metro/packs/plausible_pack.rb +36 -0
  95. data/lib/rails/metro/packs/posthog_pack.rb +40 -0
  96. data/lib/rails/metro/packs/postmark_pack.rb +39 -0
  97. data/lib/rails/metro/packs/pretender_pack.rb +33 -0
  98. data/lib/rails/metro/packs/profiling_pack.rb +30 -0
  99. data/lib/rails/metro/packs/pundit_pack.rb +50 -0
  100. data/lib/rails/metro/packs/qr_code_pack.rb +37 -0
  101. data/lib/rails/metro/packs/r2_storage_pack.rb +48 -0
  102. data/lib/rails/metro/packs/rack_timeout_pack.rb +34 -0
  103. data/lib/rails/metro/packs/rails_i18n_pack.rb +31 -0
  104. data/lib/rails/metro/packs/ransack_pack.rb +29 -0
  105. data/lib/rails/metro/packs/rate_limiting_pack.rb +39 -0
  106. data/lib/rails/metro/packs/recaptcha_pack.rb +38 -0
  107. data/lib/rails/metro/packs/resend_pack.rb +37 -0
  108. data/lib/rails/metro/packs/revenuecat_pack.rb +36 -0
  109. data/lib/rails/metro/packs/rolify_pack.rb +32 -0
  110. data/lib/rails/metro/packs/rollbar_pack.rb +38 -0
  111. data/lib/rails/metro/packs/s3_storage_pack.rb +45 -0
  112. data/lib/rails/metro/packs/scenic_pack.rb +28 -0
  113. data/lib/rails/metro/packs/scheduling_pack.rb +46 -0
  114. data/lib/rails/metro/packs/search_pack.rb +32 -0
  115. data/lib/rails/metro/packs/security_pack.rb +30 -0
  116. data/lib/rails/metro/packs/sendgrid_pack.rb +44 -0
  117. data/lib/rails/metro/packs/sent_dm_pack.rb +36 -0
  118. data/lib/rails/metro/packs/seo_pack.rb +40 -0
  119. data/lib/rails/metro/packs/shoulda_matchers_pack.rb +37 -0
  120. data/lib/rails/metro/packs/sidekiq_pack.rb +42 -0
  121. data/lib/rails/metro/packs/simple_calendar_pack.rb +29 -0
  122. data/lib/rails/metro/packs/simple_form_pack.rb +30 -0
  123. data/lib/rails/metro/packs/simplecov_pack.rb +38 -0
  124. data/lib/rails/metro/packs/slack_notifier_pack.rb +36 -0
  125. data/lib/rails/metro/packs/soft_deletes_pack.rb +30 -0
  126. data/lib/rails/metro/packs/solidus_pack.rb +31 -0
  127. data/lib/rails/metro/packs/spreadsheets_pack.rb +29 -0
  128. data/lib/rails/metro/packs/statcounter_pack.rb +41 -0
  129. data/lib/rails/metro/packs/stimulus_components_pack.rb +33 -0
  130. data/lib/rails/metro/packs/storage_validations_pack.rb +28 -0
  131. data/lib/rails/metro/packs/strong_migrations_pack.rb +30 -0
  132. data/lib/rails/metro/packs/tagging_pack.rb +31 -0
  133. data/lib/rails/metro/packs/test_mocking_pack.rb +41 -0
  134. data/lib/rails/metro/packs/testing_pack.rb +39 -0
  135. data/lib/rails/metro/packs/twilio_pack.rb +38 -0
  136. data/lib/rails/metro/packs/vite_rails_pack.rb +31 -0
  137. data/lib/rails/metro/packs/web_push_pack.rb +39 -0
  138. data/lib/rails/metro/packs/webhooks_pack.rb +45 -0
  139. data/lib/rails/metro/packs/websockets_pack.rb +29 -0
  140. data/lib/rails/metro/template_compiler.rb +85 -0
  141. data/lib/rails/metro/tui/app.rb +119 -0
  142. data/lib/rails/metro/tui/steps/app_name_step.rb +57 -0
  143. data/lib/rails/metro/tui/steps/database_step.rb +53 -0
  144. data/lib/rails/metro/tui/steps/pack_step.rb +248 -0
  145. data/lib/rails/metro/tui/steps/review_step.rb +94 -0
  146. data/lib/rails/metro/tui/styles.rb +47 -0
  147. data/lib/rails/metro/tui.rb +37 -0
  148. data/lib/rails/metro/version.rb +5 -0
  149. data/lib/rails/metro.rb +9 -0
  150. metadata +206 -0
@@ -0,0 +1,41 @@
1
+ module Rails
2
+ module Metro
3
+ module Packs
4
+ class TestMockingPack < FeaturePack
5
+ pack_name "test_mocking"
6
+ description "VCR + WebMock for recording and replaying HTTP interactions in tests"
7
+ category "testing"
8
+
9
+ def gems
10
+ [
11
+ {name: "vcr", group: :test},
12
+ {name: "webmock", group: :test}
13
+ ]
14
+ end
15
+
16
+ def template_lines
17
+ [
18
+ 'create_file "test/support/vcr.rb", <<~RUBY',
19
+ ' require "vcr"',
20
+ "",
21
+ " VCR.configure do |config|",
22
+ ' config.cassette_library_dir = "test/cassettes"',
23
+ " config.hook_into :webmock",
24
+ " config.ignore_localhost = true",
25
+ " config.default_cassette_options = { record: :once }",
26
+ " end",
27
+ "RUBY"
28
+ ]
29
+ end
30
+
31
+ def post_install_notes
32
+ [
33
+ "VCR: Wrap HTTP tests with `VCR.use_cassette(\"name\") { }` to record/replay",
34
+ "VCR: Cassettes stored in test/cassettes/",
35
+ "WebMock: All external HTTP is blocked in tests by default -- use VCR or stub explicitly"
36
+ ]
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,39 @@
1
+ module Rails
2
+ module Metro
3
+ module Packs
4
+ class TestingPack < FeaturePack
5
+ pack_name "testing"
6
+ description "FactoryBot + Faker + Capybara for testing"
7
+ category "testing"
8
+
9
+ def gems
10
+ [
11
+ {name: "factory_bot_rails", group: :test},
12
+ {name: "faker", group: :test},
13
+ {name: "capybara", group: :test},
14
+ {name: "selenium-webdriver", group: :test}
15
+ ]
16
+ end
17
+
18
+ def template_lines
19
+ [
20
+ 'create_file "test/support/factory_bot.rb", <<~RUBY',
21
+ " FactoryBot::SyntaxMethods",
22
+ "",
23
+ " class ActiveSupport::TestCase",
24
+ " include FactoryBot::Syntax::Methods",
25
+ " end",
26
+ "RUBY"
27
+ ]
28
+ end
29
+
30
+ def post_install_notes
31
+ [
32
+ "Testing: Generate factories with `bin/rails g factory_bot:model User`",
33
+ "Testing: Use `build(:user)`, `create(:user)` in tests"
34
+ ]
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,38 @@
1
+ module Rails
2
+ module Metro
3
+ module Packs
4
+ class TwilioPack < FeaturePack
5
+ pack_name "twilio"
6
+ description "Twilio for SMS, voice, and WhatsApp messaging"
7
+ category "notifications"
8
+
9
+ def gems
10
+ [
11
+ {name: "twilio-ruby"}
12
+ ]
13
+ end
14
+
15
+ def template_lines
16
+ [
17
+ 'create_file "config/initializers/twilio.rb", <<~RUBY',
18
+ " TWILIO_CLIENT = Twilio::REST::Client.new(",
19
+ ' Rails.application.credentials.dig(:twilio, :account_sid) || ENV["TWILIO_ACCOUNT_SID"],',
20
+ ' Rails.application.credentials.dig(:twilio, :auth_token) || ENV["TWILIO_AUTH_TOKEN"]',
21
+ " )",
22
+ "RUBY"
23
+ ]
24
+ end
25
+
26
+ def post_install_notes
27
+ [
28
+ "Twilio: Add your credentials with `bin/rails credentials:edit`:",
29
+ " twilio:",
30
+ " account_sid: your_account_sid",
31
+ " auth_token: your_auth_token",
32
+ " phone_number: \"+1234567890\""
33
+ ]
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,31 @@
1
+ module Rails
2
+ module Metro
3
+ module Packs
4
+ class ViteRailsPack < FeaturePack
5
+ pack_name "vite_rails"
6
+ description "Vite Rails for fast frontend bundling with HMR"
7
+ category "ui"
8
+
9
+ def gems
10
+ [
11
+ {name: "vite_rails"}
12
+ ]
13
+ end
14
+
15
+ def template_lines
16
+ [
17
+ 'run "bundle exec vite install"'
18
+ ]
19
+ end
20
+
21
+ def post_install_notes
22
+ [
23
+ "Vite: Run `bin/vite dev` alongside `bin/rails s` for development",
24
+ "Vite: Hot Module Replacement (HMR) for instant updates",
25
+ "Vite: Build for production with `bin/vite build`"
26
+ ]
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,39 @@
1
+ module Rails
2
+ module Metro
3
+ module Packs
4
+ class WebPushPack < FeaturePack
5
+ pack_name "web_push"
6
+ description "Web Push for browser push notifications"
7
+ category "notifications"
8
+
9
+ def gems
10
+ [
11
+ {name: "web-push"}
12
+ ]
13
+ end
14
+
15
+ def template_lines
16
+ [
17
+ 'create_file "config/initializers/web_push.rb", <<~RUBY',
18
+ " WebPush.configure do |config|",
19
+ ' config.vapid_public_key = Rails.application.credentials.dig(:vapid, :public_key) || ENV["VAPID_PUBLIC_KEY"]',
20
+ ' config.vapid_private_key = Rails.application.credentials.dig(:vapid, :private_key) || ENV["VAPID_PRIVATE_KEY"]',
21
+ " end",
22
+ "RUBY"
23
+ ]
24
+ end
25
+
26
+ def post_install_notes
27
+ [
28
+ "Web Push: Generate VAPID keys with `WebPush.generate_key`",
29
+ "Web Push: Add keys with `bin/rails credentials:edit`:",
30
+ " vapid:",
31
+ " public_key: your_public_key",
32
+ " private_key: your_private_key",
33
+ "Web Push: Register a service worker in your app for push subscriptions"
34
+ ]
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,45 @@
1
+ module Rails
2
+ module Metro
3
+ module Packs
4
+ class WebhooksPack < FeaturePack
5
+ pack_name "webhooks"
6
+ description "Incoming and outgoing webhook handling"
7
+ category "api"
8
+
9
+ def gems
10
+ [
11
+ {name: "webhook_system"}
12
+ ]
13
+ end
14
+
15
+ def template_lines
16
+ [
17
+ 'create_file "app/controllers/webhooks_controller.rb", <<~RUBY',
18
+ " class WebhooksController < ActionController::API",
19
+ " skip_before_action :verify_authenticity_token",
20
+ "",
21
+ " def receive",
22
+ " payload = JSON.parse(request.body.read)",
23
+ ' signature = request.headers["X-Webhook-Signature"]',
24
+ "",
25
+ " # Verify and process webhook",
26
+ ' Rails.logger.info "[Webhook] Received: \#{payload}"',
27
+ " head :ok",
28
+ " end",
29
+ " end",
30
+ "RUBY",
31
+ "",
32
+ 'route "post \\"/webhooks/:provider\\", to: \\"webhooks#receive\\""'
33
+ ]
34
+ end
35
+
36
+ def post_install_notes
37
+ [
38
+ "Webhooks: Incoming endpoint at POST /webhooks/:provider",
39
+ "Webhooks: Add signature verification per provider in WebhooksController"
40
+ ]
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,29 @@
1
+ module Rails
2
+ module Metro
3
+ module Packs
4
+ class WebsocketsPack < FeaturePack
5
+ pack_name "websockets"
6
+ description "Solid Cable (Rails 8 default, database-backed ActionCable)"
7
+ category "core"
8
+
9
+ def gems
10
+ [
11
+ {name: "solid_cable"}
12
+ ]
13
+ end
14
+
15
+ def template_lines
16
+ [
17
+ 'rails_command "solid_cable:install"'
18
+ ]
19
+ end
20
+
21
+ def post_install_notes
22
+ [
23
+ "WebSockets: Solid Cable is configured as the ActionCable adapter"
24
+ ]
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,85 @@
1
+ require "tempfile"
2
+
3
+ module Rails
4
+ module Metro
5
+ class TemplateCompiler
6
+ attr_reader :config, :packs
7
+
8
+ def initialize(config:, packs:)
9
+ @config = config
10
+ @packs = packs
11
+ end
12
+
13
+ def compile
14
+ lines = []
15
+ lines << header
16
+ lines << ""
17
+ lines << gem_declarations
18
+ lines << ""
19
+ lines << after_bundle_block
20
+ lines.flatten.join("\n")
21
+ end
22
+
23
+ def compile_to_file
24
+ content = compile
25
+ tmpfile = Tempfile.new(["rails-metro-template", ".rb"])
26
+ tmpfile.write(content)
27
+ tmpfile.close
28
+ tmpfile.path
29
+ end
30
+
31
+ def post_install_notes
32
+ packs.flat_map { |p| p.new.post_install_notes }.compact
33
+ end
34
+
35
+ private
36
+
37
+ def header
38
+ pack_names = packs.map(&:pack_name).join(", ")
39
+ [
40
+ "# Generated by rails-metro v#{VERSION}",
41
+ "# Packs: #{pack_names}",
42
+ "#",
43
+ "# Apply with: rails new APP_NAME -m this_template.rb",
44
+ "# Or: bin/rails app:template LOCATION=this_template.rb"
45
+ ]
46
+ end
47
+
48
+ def gem_declarations
49
+ all_gems = packs.flat_map { |p| p.new.gems }.uniq { |g| g[:name] }
50
+ return [] if all_gems.empty?
51
+
52
+ grouped = all_gems.group_by { |g| g[:group] }
53
+ lines = []
54
+
55
+ ungrouped = grouped.delete(nil) || []
56
+ ungrouped.each do |g|
57
+ version = g[:version] ? ", \"#{g[:version]}\"" : ""
58
+ lines << "gem \"#{g[:name]}\"#{version}"
59
+ end
60
+
61
+ grouped.each do |group, gems|
62
+ lines << ""
63
+ lines << "gem_group :#{group} do"
64
+ gems.each do |g|
65
+ version = g[:version] ? ", \"#{g[:version]}\"" : ""
66
+ lines << " gem \"#{g[:name]}\"#{version}"
67
+ end
68
+ lines << "end"
69
+ end
70
+
71
+ lines
72
+ end
73
+
74
+ def after_bundle_block
75
+ template_lines = packs.flat_map { |p| p.new.template_lines }
76
+ return [] if template_lines.empty?
77
+
78
+ lines = ["", "after_bundle do"]
79
+ template_lines.each { |line| lines << " #{line}" }
80
+ lines << "end"
81
+ lines
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,119 @@
1
+ module Rails
2
+ module Metro
3
+ module Tui
4
+ class App
5
+ include Bubbletea::Model
6
+
7
+ STEP_ORDER = %i[app_name database packs review].freeze
8
+
9
+ attr_reader :step_index, :config, :steps
10
+
11
+ def initialize
12
+ @config = Config.new
13
+ @registry = FeatureRegistry.default
14
+ @step_index = 0
15
+ @steps = {
16
+ app_name: Steps::AppNameStep.new,
17
+ database: Steps::DatabaseStep.new,
18
+ packs: Steps::PackStep.new(registry: @registry),
19
+ review: nil
20
+ }
21
+ @quitting = false
22
+ @final_action = nil
23
+ end
24
+
25
+ def init
26
+ nil
27
+ end
28
+
29
+ def update(msg)
30
+ case msg
31
+ when Bubbletea::KeyMessage
32
+ if msg.esc? || (msg.ctrl? && msg.char == "c")
33
+ @quitting = true
34
+ return self, Bubbletea::QuitCommand.new
35
+ end
36
+ end
37
+
38
+ step = current_step
39
+ step, action = step.update(msg)
40
+ @steps[current_step_name] = step
41
+
42
+ case action
43
+ when :next
44
+ advance_step
45
+ when :back
46
+ go_back
47
+ end
48
+
49
+ [self, nil]
50
+ end
51
+
52
+ def view
53
+ return "" if @quitting
54
+
55
+ lines = []
56
+ lines << header
57
+ lines << ""
58
+ lines << current_step.view
59
+ lines.join("\n")
60
+ end
61
+
62
+ def current_step_name
63
+ STEP_ORDER[@step_index]
64
+ end
65
+
66
+ def current_step
67
+ @steps[current_step_name]
68
+ end
69
+
70
+ attr_reader :final_action
71
+
72
+ private
73
+
74
+ def header
75
+ progress = STEP_ORDER.each_with_index.map { |name, i|
76
+ label = name.to_s.tr("_", " ").capitalize
77
+ if i < @step_index
78
+ Styles.success.render("✓ #{label}")
79
+ elsif i == @step_index
80
+ Styles.selected.render("● #{label}")
81
+ else
82
+ Styles.dimmed.render("○ #{label}")
83
+ end
84
+ }.join(" ")
85
+ " #{progress}"
86
+ end
87
+
88
+ def advance_step
89
+ sync_config
90
+
91
+ if @step_index >= STEP_ORDER.length - 1
92
+ @final_action = @steps[:review]&.action
93
+ @quitting = true
94
+ return self, Bubbletea::QuitCommand.new
95
+ end
96
+
97
+ @step_index += 1
98
+
99
+ if current_step_name == :review
100
+ @steps[:review] = Steps::ReviewStep.new(config: @config, registry: @registry)
101
+ end
102
+
103
+ [self, nil]
104
+ end
105
+
106
+ def go_back
107
+ @step_index = [@step_index - 1, 0].max
108
+ [self, nil]
109
+ end
110
+
111
+ def sync_config
112
+ @config.app_name = @steps[:app_name].app_name
113
+ @config.database = @steps[:database].selected
114
+ @config.selected_packs = @steps[:packs].selected_packs.to_a.sort
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,57 @@
1
+ module Rails
2
+ module Metro
3
+ module Tui
4
+ module Steps
5
+ class AppNameStep
6
+ attr_reader :app_name, :error
7
+
8
+ def initialize(app_name: "")
9
+ @app_name = app_name
10
+ @error = nil
11
+ end
12
+
13
+ def update(msg)
14
+ case msg
15
+ when Bubbletea::KeyMessage
16
+ if msg.enter?
17
+ if @app_name.strip.empty?
18
+ @error = "App name cannot be empty"
19
+ elsif @app_name.match?(/[^a-zA-Z0-9_-]/)
20
+ @error = "App name can only contain letters, numbers, hyphens, and underscores"
21
+ else
22
+ @error = nil
23
+ return self, :next
24
+ end
25
+ elsif msg.backspace?
26
+ @app_name = @app_name[0...-1]
27
+ @error = nil
28
+ elsif msg.runes?
29
+ @app_name += msg.to_s
30
+ @error = nil
31
+ end
32
+ end
33
+ [self, nil]
34
+ end
35
+
36
+ def view
37
+ lines = []
38
+ lines << Styles.title.render(" Create a new Rails app")
39
+ lines << ""
40
+ lines << " App name: #{@app_name}█"
41
+ lines << ""
42
+ if @error
43
+ lines << " #{Styles.error_style.render(@error)}"
44
+ lines << ""
45
+ end
46
+ lines << Styles.help.render(" Enter to continue • Ctrl+C to quit")
47
+ lines.join("\n")
48
+ end
49
+
50
+ def complete?
51
+ @app_name.strip.length > 0 && @error.nil?
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,53 @@
1
+ module Rails
2
+ module Metro
3
+ module Tui
4
+ module Steps
5
+ class DatabaseStep
6
+ DATABASES = %w[sqlite3 postgresql mysql2].freeze
7
+
8
+ attr_reader :cursor, :selected
9
+
10
+ def initialize(selected: "sqlite3")
11
+ @cursor = DATABASES.index(selected) || 0
12
+ @selected = selected
13
+ end
14
+
15
+ def update(msg)
16
+ case msg
17
+ when Bubbletea::KeyMessage
18
+ if msg.enter?
19
+ @selected = DATABASES[@cursor]
20
+ return self, :next
21
+ elsif msg.up? || msg.char == "k"
22
+ @cursor = (@cursor - 1) % DATABASES.length
23
+ elsif msg.down? || msg.char == "j"
24
+ @cursor = (@cursor + 1) % DATABASES.length
25
+ end
26
+ end
27
+ [self, nil]
28
+ end
29
+
30
+ def view
31
+ lines = []
32
+ lines << Styles.title.render(" Select database")
33
+ lines << ""
34
+ DATABASES.each_with_index do |db, i|
35
+ lines << if i == @cursor
36
+ Styles.selected.render(" > #{db}")
37
+ else
38
+ Styles.dimmed.render(" #{db}")
39
+ end
40
+ end
41
+ lines << ""
42
+ lines << Styles.help.render(" j/k or arrows to move • Enter to select • Ctrl+C to quit")
43
+ lines.join("\n")
44
+ end
45
+
46
+ def complete?
47
+ DATABASES.include?(@selected)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end