rubymonolith 0.1.5 → 0.1.6

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 (33) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +30 -4
  3. data/Rakefile +2 -0
  4. data/app/assets/images/monolith/logo.svg +6 -0
  5. data/app/assets/stylesheets/monolith/application.tailwind.css +14 -0
  6. data/app/components/monolith/base.rb +6 -0
  7. data/app/components/monolith/table.rb +37 -0
  8. data/app/controllers/monolith/application_controller.rb +68 -2
  9. data/app/controllers/monolith/emails_controller.rb +61 -0
  10. data/app/controllers/monolith/exceptions_controller.rb +370 -0
  11. data/app/controllers/monolith/gems_controller.rb +230 -0
  12. data/app/controllers/monolith/generators_controller.rb +377 -0
  13. data/app/controllers/monolith/home_controller.rb +10 -0
  14. data/app/controllers/monolith/models_controller.rb +319 -0
  15. data/app/controllers/monolith/routes_controller.rb +157 -0
  16. data/app/controllers/monolith/tables_controller.rb +168 -0
  17. data/app/views/monolith/base.rb +3 -0
  18. data/app/views/monolith/layouts/base.rb +23 -0
  19. data/config/routes.rb +14 -0
  20. data/lib/generators/monolith/content/USAGE +8 -0
  21. data/lib/generators/monolith/content/content_generator.rb +25 -0
  22. data/lib/generators/monolith/generators/base.rb +38 -0
  23. data/lib/generators/monolith/install/install_generator.rb +14 -95
  24. data/lib/generators/monolith/view/USAGE +8 -0
  25. data/lib/generators/monolith/view/view_generator.rb +20 -0
  26. data/lib/monolith/cli/template.rb +65 -5
  27. data/lib/monolith/cli.rb +22 -3
  28. data/lib/monolith/engine.rb +31 -0
  29. data/lib/monolith/version.rb +1 -1
  30. data/lib/tasks/monolith_tasks.rake +13 -4
  31. metadata +66 -10
  32. data/app/assets/stylesheets/monolith/application.css +0 -15
  33. data/app/views/layouts/monolith/application.html.erb +0 -15
@@ -0,0 +1,230 @@
1
+ # app/controllers/monolith/gems_controller.rb
2
+ module Monolith
3
+ class GemsController < Monolith::ApplicationController
4
+ def index
5
+ render Index.new.tap { _1.gems = GemInfo.all }
6
+ end
7
+
8
+ def show
9
+ gem_info = GemInfo.find(params[:id].to_s)
10
+ return render plain: "Gem not found", status: :not_found unless gem_info
11
+
12
+ render Show.new.tap { |v| v.gem_info = gem_info }
13
+ end
14
+
15
+ # =======================
16
+ # Inline ActiveModel-like object
17
+ # =======================
18
+ class GemInfo
19
+ include Comparable
20
+ attr_reader :name, :version, :summary, :licenses, :homepage, :source_code_uri,
21
+ :bug_tracker_uri, :changelog_uri, :rubygems_uri, :path, :source_type, :source_info
22
+
23
+ def self.all
24
+ # Bundler.load.specs → Gem::Specification for everything in the bundle
25
+ specs = Bundler.load.specs.sort_by { |s| s.name }
26
+ specs.map { |spec| from_spec(spec) }
27
+ end
28
+
29
+ def self.find(name)
30
+ spec = Bundler.load.specs.find { |s| s.name == name }
31
+ spec && from_spec(spec)
32
+ end
33
+
34
+ def self.from_spec(spec)
35
+ meta = spec.respond_to?(:metadata) ? (spec.metadata || {}) : {}
36
+
37
+ licenses =
38
+ if spec.respond_to?(:licenses) && spec.licenses.any?
39
+ spec.licenses
40
+ elsif spec.respond_to?(:license) && spec.license
41
+ [spec.license]
42
+ else
43
+ Array(meta["license"])
44
+ end
45
+
46
+ homepage = presence(spec.homepage) || meta["homepage_uri"]
47
+ source = meta["source_code_uri"]
48
+ bugs = meta["bug_tracker_uri"]
49
+ change = meta["changelog_uri"]
50
+
51
+ # Detect gem source
52
+ source_type, source_info = detect_source(spec)
53
+
54
+ new(
55
+ name: spec.name,
56
+ version: spec.version.to_s,
57
+ summary: spec.summary,
58
+ licenses: licenses.compact.map(&:to_s).uniq,
59
+ homepage: homepage,
60
+ source_code_uri: source,
61
+ bug_tracker_uri: bugs,
62
+ changelog_uri: change,
63
+ rubygems_uri: "https://rubygems.org/gems/#{spec.name}",
64
+ path: spec.full_gem_path,
65
+ source_type: source_type,
66
+ source_info: source_info
67
+ )
68
+ end
69
+
70
+ def initialize(name:, version:, summary:, licenses:, homepage:, source_code_uri:, bug_tracker_uri:, changelog_uri:, rubygems_uri:, path:, source_type:, source_info:)
71
+ @name, @version, @summary, @licenses, @homepage, @source_code_uri, @bug_tracker_uri,
72
+ @changelog_uri, @rubygems_uri, @path, @source_type, @source_info =
73
+ name, version, summary, licenses, homepage, source_code_uri, bug_tracker_uri, changelog_uri, rubygems_uri, path, source_type, source_info
74
+ end
75
+
76
+ def to_param = name
77
+ def <=>(other) = name <=> other.name
78
+
79
+ private
80
+
81
+ def self.presence(str)
82
+ s = str.to_s.strip
83
+ s.empty? ? nil : s
84
+ end
85
+
86
+ def self.detect_source(spec)
87
+ # Check Bundler's locked specs to find the source
88
+ locked_spec = Bundler.locked_gems&.specs&.find { |s| s.name == spec.name }
89
+
90
+ if locked_spec && locked_spec.source
91
+ case locked_spec.source
92
+ when Bundler::Source::Rubygems
93
+ [:rubygems, "https://rubygems.org/gems/#{spec.name}"]
94
+ when Bundler::Source::Git
95
+ git_uri = locked_spec.source.uri
96
+ git_ref = locked_spec.source.ref || locked_spec.source.branch || locked_spec.source.revision
97
+ if git_uri.to_s =~ /github\.com/
98
+ [:github, "#{git_uri} @ #{git_ref}"]
99
+ else
100
+ [:git, "#{git_uri} @ #{git_ref}"]
101
+ end
102
+ when Bundler::Source::Path
103
+ [:path, locked_spec.source.path.to_s]
104
+ else
105
+ [:unknown, locked_spec.source.class.name]
106
+ end
107
+ else
108
+ # Fallback: check if path looks like it's from RubyGems cache
109
+ if spec.full_gem_path.to_s.include?('/gems/')
110
+ [:rubygems, "https://rubygems.org/gems/#{spec.name}"]
111
+ elsif spec.full_gem_path.to_s.include?('/bundler/gems/')
112
+ [:git, spec.full_gem_path.to_s]
113
+ else
114
+ [:path, spec.full_gem_path.to_s]
115
+ end
116
+ end
117
+ rescue => e
118
+ [:unknown, "Error: #{e.message}"]
119
+ end
120
+ end
121
+
122
+ class View < View
123
+
124
+ protected
125
+
126
+ def gem_origin_link(g)
127
+ case g.source_type
128
+ when :rubygems
129
+ ext_link g.rubygems_uri
130
+ when :github
131
+ git_uri = g.source_info.split(' @ ').first
132
+ git_ref = g.source_info.split(' @ ').last
133
+ # Convert GitHub URI to branch/commit URL
134
+ normalized_uri = git_uri.to_s.sub(/\.git$/, '')
135
+ branch_url = "#{normalized_uri}/tree/#{git_ref}"
136
+ ext_link branch_url
137
+ when :git
138
+ code { g.source_info }
139
+ when :path
140
+ if defined?(ActiveSupport::Editor) && (editor = ActiveSupport::Editor.current)
141
+ # Try to link to the path in the editor
142
+ absolute_path = File.expand_path(g.source_info)
143
+ if absolute_path && File.exist?(absolute_path)
144
+ ext_link editor.url_for(absolute_path, 1), "Local Path: #{g.source_info}"
145
+ else
146
+ code { g.source_info }
147
+ end
148
+ else
149
+ code { g.source_info }
150
+ end
151
+ else
152
+ code { "#{g.source_type}: #{g.source_info}" }
153
+ end
154
+ end
155
+ end
156
+
157
+ # =======================
158
+ # Phlex views
159
+ # =======================
160
+ #
161
+ class Index < View
162
+ attr_writer :gems
163
+
164
+ def view_template
165
+ div(class: "p-6 space-y-4") do
166
+ h1(class: "text-2xl font-bold") { "Gems" }
167
+ p(class: "text-sm") { "From your current Bundler context (Gemfile.lock)." }
168
+
169
+ Table @gems do
170
+ it.row("Gem") {
171
+ nav_link it.name, controller: "/monolith/gems", action: :show, id: it.name
172
+ }
173
+ it.row("Version") {
174
+ it.version
175
+ }
176
+ it.row("Licenses") {
177
+ it.licenses.any? ? it.licenses.join(", ") : em { "—" }
178
+ }
179
+ it.row("Origin") {
180
+ gem_origin_link it
181
+ }
182
+ it.row("Description") {
183
+ it.summary.to_s.strip.empty? ? em { "—" } : it.summary
184
+ }
185
+ end
186
+ end
187
+ end
188
+ end
189
+
190
+ class Show < View
191
+ attr_writer :gem_info
192
+
193
+ def view_template
194
+ g = @gem_info
195
+
196
+ div(class: "p-6 space-y-4") do
197
+ h1(class: "text-2xl font-bold") { "#{g.name} (#{g.version})" }
198
+ p { g.summary.to_s.strip.empty? ? em { "No summary" } : g.summary }
199
+
200
+ dl(class: "grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-2") do
201
+ dt(class: "font-semibold") { "Licenses" }
202
+ dd { g.licenses.any? ? g.licenses.join(", ") : em { "—" } }
203
+
204
+ dt(class: "font-semibold") { "Homepage" }
205
+ dd { ext_link g.homepage, g.homepage&.sub(%r{\Ahttps?://}, "") }
206
+
207
+ dt(class: "font-semibold") { "Source" }
208
+ dd { ext_link g.source_code_uri, g.source_code_uri&.sub(%r{\Ahttps?://}, "") }
209
+
210
+ dt(class: "font-semibold") { "Bugs" }
211
+ dd { ext_link g.bug_tracker_uri, g.bug_tracker_uri&.sub(%r{\Ahttps?://}, "") }
212
+
213
+ dt(class: "font-semibold") { "Changelog" }
214
+ dd { ext_link g.changelog_uri, g.changelog_uri&.sub(%r{\Ahttps?://}, "") }
215
+
216
+ dt(class: "font-semibold") { "Origin" }
217
+ dd {
218
+ gem_origin_link g
219
+ }
220
+
221
+ dt(class: "font-semibold") { "Path" }
222
+ dd { code { g.path } }
223
+ end
224
+
225
+ div(class: "pt-4") { nav_link "← All gems", controller: "/monolith/gems", action: :index }
226
+ end
227
+ end
228
+ end
229
+ end
230
+ end
@@ -0,0 +1,377 @@
1
+ # app/controllers/monolith/generators_controller.rb
2
+ module Monolith
3
+ class GeneratorsController < Monolith::ApplicationController
4
+ def index
5
+ render Index.new.tap { _1.generators = Generator.all }
6
+ end
7
+
8
+ def show
9
+ generator = Generator.find(params[:id].to_s)
10
+ return render plain: "Generator not found", status: :not_found unless generator
11
+
12
+ render Show.new.tap { |v| v.generator = generator }
13
+ end
14
+
15
+ def create
16
+ generator = Generator.find(params[:id].to_s)
17
+ return render plain: "Generator not found", status: :not_found unless generator
18
+
19
+ args = params[:args].to_s.split
20
+ options = params[:options].to_h.reject { |k, v| v.blank? }
21
+
22
+ result = generator.invoke(args, options)
23
+
24
+ render Create.new.tap { |v|
25
+ v.generator = generator
26
+ v.result = result
27
+ v.args = args
28
+ v.options = options
29
+ }
30
+ rescue => e
31
+ render plain: "Error: #{e.message}\n\n#{e.backtrace.join("\n")}", status: :unprocessable_entity
32
+ end
33
+
34
+ # =======================
35
+ # Inline ActiveModel-like object
36
+ # =======================
37
+ class Generator
38
+ attr_reader :name, :namespace, :klass
39
+
40
+ def self.all
41
+ Rails.application.load_generators
42
+
43
+ # Get all generator namespaces
44
+ namespaces = Rails::Generators.hidden_namespaces + Rails::Generators.public_namespaces
45
+ namespaces = namespaces.uniq.sort
46
+
47
+ # Find and instantiate each generator
48
+ generators = namespaces.map do |namespace|
49
+ begin
50
+ klass = Rails::Generators.find_by_namespace(namespace)
51
+ klass && !klass.name.nil? ? new(klass) : nil
52
+ rescue
53
+ nil
54
+ end
55
+ end.compact
56
+
57
+ generators
58
+ end
59
+
60
+ def self.find(namespace)
61
+ Rails.application.load_generators
62
+
63
+ klass = Rails::Generators.find_by_namespace(namespace)
64
+ klass && new(klass)
65
+ end
66
+
67
+ def initialize(klass)
68
+ @klass = klass
69
+ @namespace = klass.namespace
70
+ @name = @namespace.split(':').last
71
+ end
72
+
73
+ def to_param
74
+ namespace
75
+ end
76
+
77
+ def description
78
+ klass.desc rescue nil
79
+ end
80
+
81
+ def arguments
82
+ return [] unless klass.respond_to?(:arguments)
83
+
84
+ klass.arguments.map do |arg|
85
+ {
86
+ name: arg.name,
87
+ type: arg.type,
88
+ required: arg.required?,
89
+ description: arg.description,
90
+ default: arg.default
91
+ }
92
+ end
93
+ rescue
94
+ []
95
+ end
96
+
97
+ def class_options
98
+ return {} unless klass.respond_to?(:class_options)
99
+
100
+ klass.class_options.transform_values do |option|
101
+ {
102
+ type: option.type,
103
+ default: option.default,
104
+ description: option.description,
105
+ aliases: option.aliases,
106
+ banner: option.banner
107
+ }
108
+ end
109
+ rescue
110
+ {}
111
+ end
112
+
113
+ def source_location
114
+ return nil unless klass.respond_to?(:instance_methods)
115
+
116
+ method = klass.instance_method(:initialize) rescue nil
117
+ return nil unless method
118
+
119
+ file, line = method.source_location
120
+ file
121
+ rescue
122
+ nil
123
+ end
124
+
125
+ def invoke(args, options = {})
126
+ require 'stringio'
127
+
128
+ # Capture output
129
+ output = StringIO.new
130
+ original_stdout = $stdout
131
+ original_stderr = $stderr
132
+ $stdout = output
133
+ $stderr = output
134
+
135
+ begin
136
+ # Convert options hash to array format Thor expects
137
+ thor_args = args.dup
138
+ options.each do |key, value|
139
+ next if value.blank?
140
+ thor_args << "--#{key}=#{value}"
141
+ end
142
+
143
+ # Invoke the generator
144
+ Rails::Generators.invoke(namespace, thor_args, behavior: :invoke, destination_root: Rails.root)
145
+
146
+ {
147
+ success: true,
148
+ output: output.string,
149
+ error: nil
150
+ }
151
+ rescue => e
152
+ {
153
+ success: false,
154
+ output: output.string,
155
+ error: "#{e.class}: #{e.message}"
156
+ }
157
+ ensure
158
+ $stdout = original_stdout
159
+ $stderr = original_stderr
160
+ end
161
+ end
162
+ end
163
+
164
+ # =======================
165
+ # Phlex views
166
+ # =======================
167
+ class Index < View
168
+ attr_writer :generators
169
+
170
+ def view_template
171
+ div(class: "p-6 space-y-4") do
172
+ h1(class: "text-2xl font-bold") { "Generators" }
173
+ p(class: "text-sm") { "#{@generators.size} Rails generators available." }
174
+
175
+ Table @generators do
176
+ it.row("Generator") {
177
+ nav_link it.name, controller: "/monolith/generators", action: :show, id: it.to_param
178
+ }
179
+ it.row("Namespace") { |g|
180
+ code { g.namespace }
181
+ }
182
+ it.row("Description") {
183
+ it.description || em { "—" }
184
+ }
185
+ end
186
+ end
187
+ end
188
+ end
189
+
190
+ class Show < View
191
+ attr_writer :generator
192
+
193
+ def view_template
194
+ g = @generator
195
+
196
+ div(class: "p-6 space-y-6") do
197
+ h1(class: "text-2xl font-bold") { g.name }
198
+ p { code { "rails generate #{g.namespace}" } }
199
+
200
+ if g.description
201
+ p { g.description }
202
+ end
203
+
204
+ # Arguments
205
+ section do
206
+ h2(class: "text-xl font-bold mb-2") { "Arguments" }
207
+ if g.arguments.any?
208
+ ul(class: "list-disc pl-6 space-y-1") do
209
+ g.arguments.each do |arg|
210
+ li do
211
+ code { arg[:name] }
212
+ plain " (#{arg[:type]}"
213
+ plain ", required" if arg[:required]
214
+ plain ")"
215
+ if arg[:description]
216
+ plain " - #{arg[:description]}"
217
+ end
218
+ if arg[:default]
219
+ plain " [default: #{arg[:default]}]"
220
+ end
221
+ end
222
+ end
223
+ end
224
+ else
225
+ p { em { "No arguments" } }
226
+ end
227
+ end
228
+
229
+ # Options
230
+ section do
231
+ h2(class: "text-xl font-bold mb-2") { "Options" }
232
+ if g.class_options.any?
233
+ ul(class: "list-disc pl-6 space-y-1") do
234
+ g.class_options.each do |name, option|
235
+ li do
236
+ code { "--#{name}" }
237
+ if option[:aliases]&.any?
238
+ plain " ("
239
+ code { option[:aliases].join(", ") }
240
+ plain ")"
241
+ end
242
+ plain " - #{option[:type]}"
243
+ if option[:description]
244
+ plain " - #{option[:description]}"
245
+ end
246
+ if option[:default]
247
+ plain " [default: #{option[:default]}]"
248
+ end
249
+ end
250
+ end
251
+ end
252
+ else
253
+ p { em { "No options" } }
254
+ end
255
+ end
256
+
257
+ # Invoke Form
258
+ section do
259
+ h2(class: "text-xl font-bold mb-2") { "Run Generator" }
260
+ form(method: "post", action: url_for(controller: "/monolith/generators", action: :create, id: g.to_param)) do
261
+ div(class: "space-y-3") do
262
+ # CSRF token
263
+ input(type: "hidden", name: "authenticity_token", value: form_authenticity_token)
264
+
265
+ # Arguments input
266
+ div do
267
+ label(class: "block font-semibold mb-1") { "Arguments (space-separated)" }
268
+ input(
269
+ type: "text",
270
+ name: "args",
271
+ placeholder: g.arguments.map { |a| a[:name] }.join(" "),
272
+ class: "w-full border px-3 py-2"
273
+ )
274
+ if g.arguments.any?
275
+ p(class: "text-sm mt-1") do
276
+ plain "Expected: "
277
+ g.arguments.each_with_index do |arg, idx|
278
+ plain " " if idx > 0
279
+ code { arg[:name] }
280
+ end
281
+ end
282
+ end
283
+ end
284
+
285
+ # Options
286
+ if g.class_options.any?
287
+ div do
288
+ label(class: "block font-semibold mb-2") { "Options" }
289
+ div(class: "space-y-2") do
290
+ g.class_options.each do |name, option|
291
+ div(class: "flex items-center gap-2") do
292
+ label(class: "w-48") {
293
+ code { "--#{name}" }
294
+ }
295
+ case option[:type]
296
+ when :boolean
297
+ input(type: "checkbox", name: "options[#{name}]", value: "true")
298
+ else
299
+ input(
300
+ type: "text",
301
+ name: "options[#{name}]",
302
+ placeholder: option[:default]&.to_s || option[:banner]&.to_s,
303
+ class: "flex-1 border px-2 py-1"
304
+ )
305
+ end
306
+ end
307
+ end
308
+ end
309
+ end
310
+ end
311
+
312
+ # Submit button
313
+ div do
314
+ button(
315
+ type: "submit",
316
+ class: "px-4 py-2 border font-semibold hover:bg-gray-100"
317
+ ) { "Generate" }
318
+ end
319
+ end
320
+ end
321
+ end
322
+
323
+ # Source location
324
+ if g.source_location
325
+ section do
326
+ h2(class: "text-xl font-bold mb-2") { "Source" }
327
+ p { code { g.source_location } }
328
+ end
329
+ end
330
+
331
+ div(class: "pt-4") { nav_link "← All generators", controller: "/monolith/generators", action: :index }
332
+ end
333
+ end
334
+ end
335
+
336
+ class Create < View
337
+ attr_writer :generator, :result, :args, :options
338
+
339
+ def view_template
340
+ g = @generator
341
+ r = @result
342
+
343
+ div(class: "p-6 space-y-6") do
344
+ h1(class: "text-2xl font-bold") { "Generator Result: #{g.name}" }
345
+
346
+ if r[:success]
347
+ div(class: "border border-green-600 p-4") do
348
+ h2(class: "font-bold text-green-600 mb-2") { "✓ Success" }
349
+ p do
350
+ plain "Invoked: "
351
+ code { "rails generate #{g.namespace} #{@args.join(' ')}" }
352
+ end
353
+ end
354
+ else
355
+ div(class: "border border-red-600 p-4") do
356
+ h2(class: "font-bold text-red-600 mb-2") { "✗ Error" }
357
+ p { r[:error] }
358
+ end
359
+ end
360
+
361
+ # Output
362
+ section do
363
+ h2(class: "text-xl font-bold mb-2") { "Output" }
364
+ pre(class: "border p-4 overflow-x-auto text-xs") do
365
+ code { r[:output] }
366
+ end
367
+ end
368
+
369
+ div(class: "pt-4 space-x-4") do
370
+ nav_link "← Back to generator", controller: "/monolith/generators", action: :show, id: g.to_param
371
+ nav_link "All generators", controller: "/monolith/generators", action: :index
372
+ end
373
+ end
374
+ end
375
+ end
376
+ end
377
+ end
@@ -0,0 +1,10 @@
1
+ # app/controllers/monolith/gems_controller.rb
2
+ module Monolith
3
+ class HomeController < Monolith::ApplicationController
4
+ class Show < View
5
+ def view_template
6
+ h1(class: "text-6xl") { "Welcome to Monolith!" }
7
+ end
8
+ end
9
+ end
10
+ end