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,147 @@
1
+ module Rails
2
+ module Metro
3
+ class FeatureRegistry
4
+ class DependencyError < StandardError; end
5
+ class ConflictError < StandardError; end
6
+
7
+ def initialize
8
+ @packs = {}
9
+ end
10
+
11
+ def register(pack_class)
12
+ name = pack_class.pack_name
13
+ raise ArgumentError, "Pack must define a pack_name" unless name
14
+ @packs[name] = pack_class
15
+ end
16
+
17
+ def all
18
+ @packs.values
19
+ end
20
+
21
+ def names
22
+ @packs.keys.sort
23
+ end
24
+
25
+ def find(name)
26
+ @packs[name.to_s]
27
+ end
28
+
29
+ def categories
30
+ all.group_by { |p| p.category }
31
+ end
32
+
33
+ def resolve(selected_names)
34
+ selected = selected_names.map(&:to_s)
35
+ validate_existence!(selected)
36
+ validate_conflicts!(selected)
37
+ expand_dependencies(selected)
38
+ end
39
+
40
+ def as_catalog
41
+ all.sort_by(&:pack_name).map do |pack_class|
42
+ {
43
+ name: pack_class.pack_name,
44
+ description: pack_class.description,
45
+ category: pack_class.category,
46
+ depends_on: pack_class.depends_on,
47
+ conflicts_with: pack_class.conflicts_with
48
+ }
49
+ end
50
+ end
51
+
52
+ def validate_selection(selected_names)
53
+ selected = selected_names.map(&:to_s)
54
+ result = {valid: true, errors: [], auto_added: []}
55
+
56
+ unknown = selected - @packs.keys
57
+ if unknown.any?
58
+ result[:valid] = false
59
+ result[:errors] << "Unknown packs: #{unknown.join(", ")}"
60
+ return result
61
+ end
62
+
63
+ selected.each do |name|
64
+ pack = @packs[name]
65
+ conflicting = pack.conflicts_with & selected
66
+ if conflicting.any?
67
+ result[:valid] = false
68
+ result[:errors] << "Pack '#{name}' conflicts with: #{conflicting.join(", ")}"
69
+ end
70
+ end
71
+
72
+ return result unless result[:valid]
73
+
74
+ resolved = expand_dependencies(selected)
75
+ resolved_names = resolved.map(&:pack_name)
76
+ result[:auto_added] = resolved_names - selected
77
+ result
78
+ rescue DependencyError => e
79
+ result[:valid] = false
80
+ result[:errors] << e.message
81
+ result
82
+ end
83
+
84
+ def self.default
85
+ @default ||= build_default
86
+ end
87
+
88
+ def self.build_default
89
+ registry = new
90
+ load_builtin_packs
91
+ builtin_pack_classes.each { |klass| registry.register(klass) }
92
+ registry
93
+ end
94
+
95
+ def self.load_builtin_packs
96
+ Dir.glob(File.join(__dir__, "packs", "**", "*_pack.rb")).sort.each { |f| require f }
97
+ end
98
+
99
+ def self.builtin_pack_classes
100
+ ObjectSpace.each_object(Class).select { |c|
101
+ c < FeaturePack && c.pack_name && c.name&.start_with?("Rails::Metro::Packs::")
102
+ }
103
+ end
104
+
105
+ private
106
+
107
+ def validate_existence!(names)
108
+ unknown = names - @packs.keys
109
+ if unknown.any?
110
+ raise DependencyError, "Unknown packs: #{unknown.join(", ")}. Available: #{@packs.keys.sort.join(", ")}"
111
+ end
112
+ end
113
+
114
+ def validate_conflicts!(names)
115
+ names.each do |name|
116
+ pack = @packs[name]
117
+ conflicting = pack.conflicts_with & names
118
+ if conflicting.any?
119
+ raise ConflictError, "Pack '#{name}' conflicts with: #{conflicting.join(", ")}"
120
+ end
121
+ end
122
+ end
123
+
124
+ def expand_dependencies(names)
125
+ resolved = []
126
+ visited = Set.new
127
+ names.each { |name| visit(name, resolved, visited, []) }
128
+ resolved
129
+ end
130
+
131
+ def visit(name, resolved, visited, stack)
132
+ return if visited.include?(name)
133
+ if stack.include?(name)
134
+ raise DependencyError, "Circular dependency: #{(stack + [name]).join(" -> ")}"
135
+ end
136
+
137
+ stack.push(name)
138
+ pack = @packs[name]
139
+ pack.depends_on.each { |dep| visit(dep, resolved, visited, stack) }
140
+ stack.pop
141
+
142
+ visited.add(name)
143
+ resolved << @packs[name]
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,29 @@
1
+ module Rails
2
+ module Metro
3
+ module Packs
4
+ class AasmPack < FeaturePack
5
+ pack_name "aasm"
6
+ description "AASM for state machines in ActiveRecord models"
7
+ category "data"
8
+
9
+ def gems
10
+ [
11
+ {name: "aasm"}
12
+ ]
13
+ end
14
+
15
+ def template_lines
16
+ []
17
+ end
18
+
19
+ def post_install_notes
20
+ [
21
+ "AASM: Add `include AASM` to models",
22
+ "AASM: Define states with `aasm do; state :draft, initial: true; state :published; end`",
23
+ "AASM: Define events with `event :publish do; transitions from: :draft, to: :published; end`"
24
+ ]
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,39 @@
1
+ module Rails
2
+ module Metro
3
+ module Packs
4
+ class AbTestingPack < FeaturePack
5
+ pack_name "ab_testing"
6
+ description "Split for A/B testing and experiments"
7
+ category "data"
8
+
9
+ def gems
10
+ [
11
+ {name: "split", version: "~> 4.0"}
12
+ ]
13
+ end
14
+
15
+ def template_lines
16
+ [
17
+ 'create_file "config/initializers/split.rb", <<~RUBY',
18
+ " Split.configure do |config|",
19
+ " config.allow_multiple_experiments = true",
20
+ " config.persistence = Split::Persistence::SessionAdapter",
21
+ " end",
22
+ "RUBY",
23
+ "",
24
+ 'route "mount Split::Dashboard, at: \\"/split\\""'
25
+ ]
26
+ end
27
+
28
+ def post_install_notes
29
+ [
30
+ "Split: Requires Redis for experiment storage",
31
+ "Split: Use `ab_test(\"button_color\", \"red\", \"blue\")` in views/controllers",
32
+ "Split: Call `ab_finished(\"button_color\")` on conversion",
33
+ "Split: Dashboard at /split"
34
+ ]
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,29 @@
1
+ module Rails
2
+ module Metro
3
+ module Packs
4
+ class ActionTextPack < FeaturePack
5
+ pack_name "action_text"
6
+ description "Action Text for rich text editing with Trix"
7
+ category "ui"
8
+
9
+ def gems
10
+ []
11
+ end
12
+
13
+ def template_lines
14
+ [
15
+ 'rails_command "action_text:install"'
16
+ ]
17
+ end
18
+
19
+ def post_install_notes
20
+ [
21
+ "Action Text: Run `bin/rails db:migrate` to create rich text tables",
22
+ "Action Text: Add `has_rich_text :body` to models",
23
+ "Action Text: Use `<%= form.rich_text_area :body %>` in forms"
24
+ ]
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,32 @@
1
+ module Rails
2
+ module Metro
3
+ module Packs
4
+ class ActiveadminPack < FeaturePack
5
+ pack_name "activeadmin"
6
+ description "ActiveAdmin -- classic, battle-tested admin framework"
7
+ category "admin"
8
+ conflicts_with "avo", "administrate", "madmin"
9
+
10
+ def gems
11
+ [
12
+ {name: "activeadmin"}
13
+ ]
14
+ end
15
+
16
+ def template_lines
17
+ [
18
+ 'rails_command "generate active_admin:install --skip-users"'
19
+ ]
20
+ end
21
+
22
+ def post_install_notes
23
+ [
24
+ "ActiveAdmin: Run `bin/rails db:migrate` to create admin tables",
25
+ "ActiveAdmin: Access admin at /admin",
26
+ "ActiveAdmin: Register resources with `bin/rails g active_admin:resource User`"
27
+ ]
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,31 @@
1
+ module Rails
2
+ module Metro
3
+ module Packs
4
+ class ActivityFeedPack < FeaturePack
5
+ pack_name "activity_feed"
6
+ description "Public Activity for activity feeds and timelines"
7
+ category "data"
8
+
9
+ def gems
10
+ [
11
+ {name: "public_activity"}
12
+ ]
13
+ end
14
+
15
+ def template_lines
16
+ [
17
+ 'rails_command "generate public_activity:migration"'
18
+ ]
19
+ end
20
+
21
+ def post_install_notes
22
+ [
23
+ "Activity Feed: Run `bin/rails db:migrate` to create activities table",
24
+ "Activity Feed: Add `include PublicActivity::Model` and `tracked` to models",
25
+ "Activity Feed: Query with `PublicActivity::Activity.order(created_at: :desc)`"
26
+ ]
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,28 @@
1
+ module Rails
2
+ module Metro
3
+ module Packs
4
+ class ActsAsListPack < FeaturePack
5
+ pack_name "acts_as_list"
6
+ description "Acts As List for sortable, orderable records"
7
+ category "data"
8
+
9
+ def gems
10
+ [
11
+ {name: "acts_as_list"}
12
+ ]
13
+ end
14
+
15
+ def template_lines
16
+ []
17
+ end
18
+
19
+ def post_install_notes
20
+ [
21
+ "Acts As List: Add `acts_as_list` to models with a `position` integer column",
22
+ "Acts As List: Use `item.move_to_top`, `item.move_lower`, `item.insert_at(2)`"
23
+ ]
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,32 @@
1
+ module Rails
2
+ module Metro
3
+ module Packs
4
+ class ActsAsVotablePack < FeaturePack
5
+ pack_name "acts_as_votable"
6
+ description "Acts As Votable for likes, upvotes, and reactions"
7
+ category "data"
8
+
9
+ def gems
10
+ [
11
+ {name: "acts_as_votable"}
12
+ ]
13
+ end
14
+
15
+ def template_lines
16
+ [
17
+ 'rails_command "generate acts_as_votable:migration"'
18
+ ]
19
+ end
20
+
21
+ def post_install_notes
22
+ [
23
+ "Votable: Run `bin/rails db:migrate` to create votes table",
24
+ "Votable: Add `acts_as_votable` to models that can be voted on",
25
+ "Votable: Add `acts_as_voter` to User model",
26
+ "Votable: Use `user.likes @post` and `@post.liked_by user`"
27
+ ]
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,31 @@
1
+ module Rails
2
+ module Metro
3
+ module Packs
4
+ class AdministratePack < FeaturePack
5
+ pack_name "administrate"
6
+ description "Administrate -- Thoughtbot's lightweight admin framework"
7
+ category "admin"
8
+ conflicts_with "avo", "activeadmin", "madmin"
9
+
10
+ def gems
11
+ [
12
+ {name: "administrate"}
13
+ ]
14
+ end
15
+
16
+ def template_lines
17
+ [
18
+ 'rails_command "generate administrate:install"'
19
+ ]
20
+ end
21
+
22
+ def post_install_notes
23
+ [
24
+ "Administrate: Access admin at /admin",
25
+ "Administrate: Generate dashboards with `bin/rails g administrate:dashboard User`"
26
+ ]
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,31 @@
1
+ module Rails
2
+ module Metro
3
+ module Packs
4
+ class AfterPartyPack < FeaturePack
5
+ pack_name "after_party"
6
+ description "AfterParty for one-time post-deploy tasks"
7
+ category "ops"
8
+
9
+ def gems
10
+ [
11
+ {name: "after_party"}
12
+ ]
13
+ end
14
+
15
+ def template_lines
16
+ [
17
+ 'rails_command "generate after_party:install"'
18
+ ]
19
+ end
20
+
21
+ def post_install_notes
22
+ [
23
+ "AfterParty: Generate tasks with `bin/rails g after_party:task seed_admin_user`",
24
+ "AfterParty: Run with `bin/rails after_party:run`",
25
+ "AfterParty: Tasks only run once (tracked in task_records table)"
26
+ ]
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,31 @@
1
+ module Rails
2
+ module Metro
3
+ module Packs
4
+ class AhoyPack < FeaturePack
5
+ pack_name "ahoy"
6
+ description "Ahoy for visit and event tracking (first-party analytics)"
7
+ category "analytics"
8
+
9
+ def gems
10
+ [
11
+ {name: "ahoy_matey"}
12
+ ]
13
+ end
14
+
15
+ def template_lines
16
+ [
17
+ 'rails_command "generate ahoy:install"'
18
+ ]
19
+ end
20
+
21
+ def post_install_notes
22
+ [
23
+ "Ahoy: Run `bin/rails db:migrate` to create visits and events tables",
24
+ "Ahoy: Track events with `ahoy.track \"Viewed product\", product_id: product.id`",
25
+ "Ahoy: Visits auto-tracked -- access with `current_visit` in controllers"
26
+ ]
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,45 @@
1
+ module Rails
2
+ module Metro
3
+ module Packs
4
+ class AmplitudePack < FeaturePack
5
+ pack_name "amplitude"
6
+ description "Amplitude for product analytics, funnels, and retention"
7
+ category "analytics"
8
+
9
+ def gems
10
+ [
11
+ {name: "amplitude-api"}
12
+ ]
13
+ end
14
+
15
+ def template_lines
16
+ [
17
+ 'create_file "config/initializers/amplitude.rb", <<~RUBY',
18
+ " AmplitudeAPI.config.api_key = Rails.application.credentials.dig(:amplitude, :api_key) || ENV[\"AMPLITUDE_API_KEY\"]",
19
+ "RUBY",
20
+ "",
21
+ 'create_file "app/views/layouts/_amplitude.html.erb", <<~ERB',
22
+ " <script type=\"text/javascript\">",
23
+ ' !function(){"use strict";!function(e,t){var r=e.amplitude||{_q:[],_iq:{}};if(r.invoked)e.console&&console.error&&console.error("Amplitude snippet has been loaded.");else{var n=function(e,t){e.prototype[t]=function(){return this._q.push({name:t,args:Array.prototype.slice.call(arguments,0)}),this}},s=function(e,t,r){return function(n){e._q.push({name:t,args:Array.prototype.slice.call(arguments,0),resolve:n})}},o=function(e,t,r){e._q.push({name:t,args:Array.prototype.slice.call(arguments,0)})},i=function(e,t,r){e[t]=function(){if(r)return{promise:new Promise(s(e,t,r))};o(e,t,r)}},a=function(e){for(var t=0;t<g.length;t++)i(e,g[t],!1);for(var r=0;r<m.length;r++)i(e,m[r],!0)};r.invoked=!0;var c=t.createElement("script");c.type="text/javascript",c.integrity="sha384-PPfHw98myKtJkA9OdPBMQ6n8yvUaYk0EyUQccFSIQGmB05K6aAMZwvv8d/EmYKhv",c.crossOrigin="anonymous",c.async=!0,c.src="https://cdn.amplitude.com/libs/analytics-browser-2.11.1-min.js.gz",c.onload=function(){e.amplitude.runQueuedFunctions||console.log("[Amplitude] Error: could not load SDK")};var u=t.getElementsByTagName("script")[0];u.parentNode.insertBefore(c,u);for(var l=function(){return this._q=[],this},p=["add","append","clearAll","prepend","set","setOnce","unset","preInsert","postInsert","remove","getUserProperties"],d=0;d<p.length;d++)n(l,p[d]);r.Identify=l;for(var f=function(){return this._q=[],this},v=["getEventProperties","setProductId","setQuantity","setPrice","setRevenue","setRevenueType","setEventProperties"],y=0;y<v.length;y++)n(f,v[y]);r.Revenue=f;var g=["getDeviceId","setDeviceId","getSessionId","setSessionId","getUserId","setUserId","setOptOut","setTransport","reset","extendSession"],m=["init","add","remove","track","logEvent","identify","groupIdentify","setGroup","revenue","flush"];a(r),r.createInstance=function(e){return r._iq[e]={_q:[]},a(r._iq[e]),r._iq[e]},e.amplitude=r}}(window,document)}();',
24
+ ' amplitude.init("<%%= Rails.application.credentials.dig(:amplitude, :api_key) || ENV["AMPLITUDE_API_KEY"] %>");',
25
+ " </script>",
26
+ "ERB",
27
+ "",
28
+ 'inject_into_file "app/views/layouts/application.html.erb", after: "<head>\\n" do',
29
+ ' " <%= render \\"layouts/amplitude\\" %>\\n"',
30
+ "end"
31
+ ]
32
+ end
33
+
34
+ def post_install_notes
35
+ [
36
+ "Amplitude: Add your API key with `bin/rails credentials:edit`:",
37
+ " amplitude:",
38
+ " api_key: your_api_key",
39
+ "Amplitude: Use `AmplitudeAPI.track(event)` server-side for backend events"
40
+ ]
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,30 @@
1
+ module Rails
2
+ module Metro
3
+ module Packs
4
+ class AnnotatePack < FeaturePack
5
+ pack_name "annotate"
6
+ description "Annotate models and routes with schema info"
7
+ category "data"
8
+
9
+ def gems
10
+ [
11
+ {name: "annotaterb", group: :development}
12
+ ]
13
+ end
14
+
15
+ def template_lines
16
+ [
17
+ 'rails_command "generate annotate_rb:install"'
18
+ ]
19
+ end
20
+
21
+ def post_install_notes
22
+ [
23
+ "Annotate: Schema comments are auto-added to models after migrations",
24
+ "Annotate: Run `bin/rails annotate_models` to annotate manually"
25
+ ]
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,41 @@
1
+ module Rails
2
+ module Metro
3
+ module Packs
4
+ class ApiCorsPack < FeaturePack
5
+ pack_name "api_cors"
6
+ description "Rack CORS for cross-origin API requests"
7
+ category "api"
8
+
9
+ def gems
10
+ [
11
+ {name: "rack-cors"}
12
+ ]
13
+ end
14
+
15
+ def template_lines
16
+ [
17
+ 'create_file "config/initializers/cors.rb", <<~RUBY',
18
+ " Rails.application.config.middleware.insert_before 0, Rack::Cors do",
19
+ " allow do",
20
+ ' origins Rails.application.credentials.dig(:cors, :origins) || ENV.fetch("CORS_ORIGINS", "http://localhost:3000")',
21
+ "",
22
+ " resource \"*\",",
23
+ " headers: :any,",
24
+ " methods: %i[get post put patch delete options head],",
25
+ " credentials: true",
26
+ " end",
27
+ " end",
28
+ "RUBY"
29
+ ]
30
+ end
31
+
32
+ def post_install_notes
33
+ [
34
+ "CORS: Update allowed origins in config/initializers/cors.rb",
35
+ "CORS: Set CORS_ORIGINS env var in production (comma-separated domains)"
36
+ ]
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,34 @@
1
+ module Rails
2
+ module Metro
3
+ module Packs
4
+ class ApiDocsPack < FeaturePack
5
+ pack_name "api_docs"
6
+ description "Rswag for Swagger/OpenAPI documentation from tests"
7
+ category "api"
8
+
9
+ def gems
10
+ [
11
+ {name: "rswag-api"},
12
+ {name: "rswag-ui"},
13
+ {name: "rswag-specs", group: :test}
14
+ ]
15
+ end
16
+
17
+ def template_lines
18
+ [
19
+ 'rails_command "generate rswag:api:install"',
20
+ 'rails_command "generate rswag:ui:install"'
21
+ ]
22
+ end
23
+
24
+ def post_install_notes
25
+ [
26
+ "Rswag: Write request specs with `describe 'API', swagger_doc: 'v1/swagger.yaml'`",
27
+ "Rswag: Generate docs with `rake rswag:specs:swaggerize`",
28
+ "Rswag: Swagger UI available at /api-docs"
29
+ ]
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,31 @@
1
+ module Rails
2
+ module Metro
3
+ module Packs
4
+ class ApiGuardPack < FeaturePack
5
+ pack_name "api_guard"
6
+ description "API Guard for JWT authentication in Rails APIs"
7
+ category "api"
8
+
9
+ def gems
10
+ [
11
+ {name: "api_guard"}
12
+ ]
13
+ end
14
+
15
+ def template_lines
16
+ [
17
+ 'rails_command "generate api_guard:initializer"'
18
+ ]
19
+ end
20
+
21
+ def post_install_notes
22
+ [
23
+ "API Guard: Run `bin/rails db:migrate` to create token tables",
24
+ "API Guard: Add `api_guard_routes for: 'users'` to routes",
25
+ "API Guard: Endpoints: POST /users/sign_in, DELETE /users/sign_out, POST /users/tokens"
26
+ ]
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end