rails-frontend-cli 1.0.4

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.
@@ -0,0 +1,1357 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'fileutils'
5
+ require 'optparse'
6
+
7
+ class RailsFrontendCLI
8
+ VERSION = "1.0.4"
9
+ AUTHOR = "Levent Özbilgiç"
10
+ LINKEDIN = "https://www.linkedin.com/in/leventozbilgic/"
11
+ GITHUB = "https://github.com/ozbilgic"
12
+
13
+ def initialize
14
+ @project_name = nil
15
+ @page_name = nil
16
+ @command = nil
17
+ @clean_mode = false
18
+ end
19
+
20
+ def run(args)
21
+ if args.empty?
22
+ show_help
23
+ exit 0
24
+ end
25
+
26
+ @command = args[0]
27
+
28
+ case @command
29
+ when 'new', 'n'
30
+ @project_name = args[1]
31
+ if @project_name.nil? || @project_name.empty?
32
+ error_message("Project name not specified. Usage: rails-frontend new PROJECT_NAME [--clean]")
33
+ end
34
+ # Check for --clean parameter
35
+ @clean_mode = args.include?('--clean')
36
+ create_new_project
37
+ when 'add-page', 'ap'
38
+ @page_name = args[1]
39
+ if @page_name.nil? || @page_name.empty?
40
+ error_message("Page name not specified. Usage: rails-frontend add-page PAGE_NAME")
41
+ end
42
+ add_page
43
+ when 'remove-page', 'rp'
44
+ @page_name = args[1]
45
+ if @page_name.nil? || @page_name.empty?
46
+ error_message("Page name not specified. Usage: rails-frontend remove-page PAGE_NAME")
47
+ end
48
+ remove_page
49
+ when 'add-stimulus', 'as'
50
+ @controller_name = args[1]
51
+ if @controller_name.nil? || @controller_name.empty?
52
+ error_message("Controller name not specified. Usage: rails-frontend add-stimulus CONTROLLER_NAME")
53
+ end
54
+ add_stimulus
55
+ when 'remove-stimulus', 'rs'
56
+ @controller_name = args[1]
57
+ if @controller_name.nil? || @controller_name.empty?
58
+ error_message("Controller name not specified. Usage: rails-frontend remove-stimulus CONTROLLER_NAME")
59
+ end
60
+ remove_stimulus
61
+ when 'add-layout', 'al'
62
+ @layout_name = args[1]
63
+ if @layout_name.nil? || @layout_name.empty?
64
+ error_message("Layout name not specified. Usage: rails-frontend add-layout LAYOUT_NAME")
65
+ end
66
+ add_layout
67
+ when 'remove-layout', 'rl'
68
+ @layout_name = args[1]
69
+ if @layout_name.nil? || @layout_name.empty?
70
+ error_message("Layout name not specified. Usage: rails-frontend remove-layout LAYOUT_NAME")
71
+ end
72
+ remove_layout
73
+ when 'add-pin', 'pin'
74
+ @pin_name = args[1]
75
+ if @pin_name.nil? || @pin_name.empty?
76
+ error_message("Pin name not specified. Usage: rails-frontend add-pin PACKAGE_NAME")
77
+ end
78
+ add_pin
79
+ when 'remove-pin', 'unpin'
80
+ @pin_name = args[1]
81
+ if @pin_name.nil? || @pin_name.empty?
82
+ error_message("Pin name not specified. Usage: rails-frontend remove-pin PACKAGE_NAME")
83
+ end
84
+ remove_pin
85
+ when 'run', 'r'
86
+ run_server
87
+ when 'build', 'b'
88
+ build_static_site
89
+ when 'version', '-v', '--version'
90
+ puts "Rails Frontend CLI v#{VERSION}"
91
+ exit 0
92
+ when 'help', '-h', '--help'
93
+ show_help
94
+ exit 0
95
+ else
96
+ error_message("Unknown command: #{@command}")
97
+ end
98
+ end
99
+
100
+ private
101
+
102
+ def create_new_project
103
+ show_title("Creating New Rails Frontend Project: #{@project_name}")
104
+
105
+ # Check if Rails project already exists
106
+ if Dir.exist?(@project_name)
107
+ error_message("'#{@project_name}' directory already exists!")
108
+ end
109
+
110
+ # Create Rails project
111
+ show_message("Creating Rails project...")
112
+
113
+ # Build command based on clean mode
114
+ if @clean_mode
115
+ rails_command = "rails new #{@project_name} --css=tailwind --javascript=importmap " \
116
+ "--skip-test --skip-system-test --skip-action-mailer " \
117
+ "--skip-action-mailbox --skip-action-text --skip-active-job " \
118
+ "--skip-action-cable --skip-active-storage --skip-active-record "\
119
+ "--skip-solid --skip-kamal --skip-docker"
120
+ else
121
+ rails_command = "rails new #{@project_name} --css=tailwind --javascript=importmap"
122
+ end
123
+
124
+ unless system(rails_command)
125
+ error_message("Failed to create Rails project!")
126
+ end
127
+ success_message("Rails project created")
128
+
129
+ # Change to project directory
130
+ project_directory = File.expand_path(@project_name)
131
+ Dir.chdir(project_directory) do
132
+ # Clean unnecessary files (if --clean parameter exists)
133
+ if @clean_mode
134
+ show_message("Cleaning unnecessary files...")
135
+ clean_unnecessary_files
136
+ success_message("Unnecessary files cleaned")
137
+ end
138
+
139
+ # Create Home controller and view
140
+ show_message("Creating Home controller and view...")
141
+ create_home_controller
142
+ success_message("Home controller and view created")
143
+
144
+ # Create shared components
145
+ show_message("Creating shared components...")
146
+ create_shared_components
147
+ success_message("Shared components created")
148
+
149
+ # Create CSS files
150
+ show_message("Creating CSS files...")
151
+ create_css_files
152
+ success_message("CSS files created")
153
+
154
+ # Create asset folders
155
+ show_message("Creating asset folders...")
156
+ create_asset_folders
157
+ success_message("Asset folders created")
158
+
159
+ # Update layout file
160
+ show_message("Updating layout file...")
161
+ update_layout
162
+ success_message("Layout file updated")
163
+
164
+ # Configure routes
165
+ show_message("Configuring routes...")
166
+ update_routes('home', 'index', root: true)
167
+ success_message("Routes configured")
168
+
169
+ # Configure Procfile.dev
170
+ show_message("Configuring Procfile.dev...")
171
+ update_procfile
172
+ success_message("Procfile.dev configured")
173
+ end
174
+
175
+ completion_message
176
+ end
177
+
178
+ def add_page
179
+ # Check if current directory is a Rails project
180
+ is_rails_project?
181
+
182
+ show_title("Adding New Page: #{@page_name}")
183
+
184
+ # Normalize page name (convert Turkish characters)
185
+ page_name_normalized = normalize_name(@page_name)
186
+
187
+ # Create view (in home folder)
188
+ show_message("Creating view file...")
189
+ create_view(page_name_normalized)
190
+ success_message("View file created")
191
+
192
+ # Create CSS file
193
+ show_message("Creating CSS file...")
194
+ create_css(page_name_normalized)
195
+ success_message("CSS file created")
196
+
197
+ # Add action to Home controller
198
+ show_message("Updating Home controller...")
199
+ add_home_controller_action(page_name_normalized)
200
+ success_message("Home controller updated")
201
+
202
+ # Add route
203
+ show_message("Adding route...")
204
+ update_routes(page_name_normalized, page_name_normalized)
205
+ success_message("Route added")
206
+
207
+ puts "\n #{colorize('Page successfully added!', :green)}"
208
+ puts "Page URL: #{colorize("/#{page_name_normalized}", :blue)}"
209
+ end
210
+
211
+ def remove_page
212
+ is_rails_project?
213
+
214
+ show_title("Removing Page: #{@page_name}")
215
+
216
+ page_name_normalized = normalize_name(@page_name)
217
+
218
+ # Prevent deletion of home/index page
219
+ if page_name_normalized == 'home' || page_name_normalized == 'index'
220
+ error_message("Home page (home/index) cannot be deleted!")
221
+ end
222
+
223
+ # Check file existence
224
+ view_path = "app/views/home/#{page_name_normalized}.html.erb"
225
+ unless File.exist?(view_path)
226
+ error_message("'#{page_name_normalized}' page not found!")
227
+ end
228
+
229
+ # Get confirmation
230
+ print "#{colorize('Are you sure?', :yellow)} '#{page_name_normalized}' page will be deleted (y/n): "
231
+ confirmation = STDIN.gets.chomp.downcase
232
+ unless confirmation == 'y' || confirmation == 'yes'
233
+ puts "Operation cancelled."
234
+ exit 0
235
+ end
236
+
237
+ # Delete view file
238
+ show_message("Deleting view file...")
239
+ FileUtils.rm_f(view_path)
240
+ success_message("View file deleted")
241
+
242
+ # Delete CSS file
243
+ show_message("Deleting CSS file...")
244
+ FileUtils.rm_f("app/assets/stylesheets/#{page_name_normalized}.css")
245
+ success_message("CSS file deleted")
246
+
247
+ # Remove action from Home controller
248
+ show_message("Updating Home controller...")
249
+ remove_home_controller_action(page_name_normalized)
250
+ success_message("Home controller updated")
251
+
252
+ # Remove route
253
+ show_message("Removing route...")
254
+ remove_route(page_name_normalized)
255
+ success_message("Route removed")
256
+
257
+ puts "\n #{colorize('Page successfully deleted!', :green)}"
258
+ end
259
+
260
+ def add_stimulus
261
+ # Check if current directory is a Rails project
262
+ is_rails_project?
263
+
264
+ show_title("Creating Stimulus Controller: #{@controller_name}")
265
+
266
+ # Normalize controller name
267
+ controller_name_normalized = normalize_name(@controller_name)
268
+ controller_file = "app/javascript/controllers/#{controller_name_normalized}_controller.js"
269
+
270
+ # Check if controller file already exists
271
+ if File.exist?(controller_file)
272
+ error_message("Stimulus controller already exists: #{controller_file}")
273
+ end
274
+
275
+ # Create Stimulus controller
276
+ show_message("Creating Stimulus controller...")
277
+ create_stimulus_controller(controller_name_normalized)
278
+ success_message("Stimulus controller created")
279
+
280
+ puts "\n #{colorize('Stimulus controller successfully created!', :green)}"
281
+ puts "File: #{colorize("app/javascript/controllers/#{controller_name_normalized}_controller.js", :blue)}"
282
+ end
283
+
284
+ def remove_stimulus
285
+ # Check if current directory is a Rails project
286
+ is_rails_project?
287
+
288
+ show_title("Removing Stimulus Controller: #{@controller_name}")
289
+
290
+ # Normalize controller name
291
+ controller_name_normalized = normalize_name(@controller_name)
292
+ controller_file = "app/javascript/controllers/#{controller_name_normalized}_controller.js"
293
+
294
+ # Check controller file existence
295
+ unless File.exist?(controller_file)
296
+ error_message("Stimulus controller not found: #{controller_file}")
297
+ end
298
+
299
+ # Check usage in view files
300
+ show_message("Checking usage in view files...")
301
+ used_files = []
302
+
303
+ if Dir.exist?('app/views')
304
+ Dir.glob('app/views/**/*.html.erb').each do |view_file|
305
+ content = File.read(view_file)
306
+ # Check for data-controller="controller_name" or data-controller='controller_name'
307
+ # Also check for data: { controller: "controller_name" } pattern
308
+ if content.match?(/data-controller=["'].*#{controller_name_normalized}.*["']/) ||
309
+ content.match?(/data:\s*\{[^}]*controller:\s*["']#{controller_name_normalized}["'][^}]*\}/)
310
+ used_files << view_file
311
+ end
312
+ end
313
+ end
314
+
315
+ if used_files.any?
316
+ puts "\n"
317
+ puts colorize("WARNING: This controller is being used in the following files:", :yellow, bold: true)
318
+ used_files.each do |file|
319
+ puts " - #{file}"
320
+ end
321
+ puts "\n"
322
+ print colorize("Do you still want to delete it? (y/n): ", :yellow)
323
+ answer = STDIN.gets.chomp.downcase
324
+ unless answer == 'y' || answer == 'yes'
325
+ puts "\nOperation cancelled."
326
+ exit 0
327
+ end
328
+ end
329
+ success_message("Check completed")
330
+
331
+ # Delete controller
332
+ show_message("Deleting Stimulus controller...")
333
+ FileUtils.rm_f(controller_file)
334
+ success_message("Stimulus controller deleted")
335
+
336
+ puts "\n #{colorize('Stimulus controller successfully deleted!', :green)}"
337
+ end
338
+
339
+ def add_layout
340
+ # Check if current directory is a Rails project
341
+ is_rails_project?
342
+
343
+ show_title("Creating Layout: #{@layout_name}")
344
+
345
+ # Normalize layout name
346
+ layout_name_normalized = normalize_name(@layout_name)
347
+
348
+ # Check if layout file already exists
349
+ layout_file = "app/views/layouts/#{layout_name_normalized}.html.erb"
350
+ if File.exist?(layout_file)
351
+ error_message("Layout already exists: #{layout_file}")
352
+ end
353
+
354
+ # Scan files in app/views/home folder
355
+ show_message("Scanning view files...")
356
+ home_views = list_home_views
357
+ success_message("View files scanned")
358
+
359
+ # Check for matching view file
360
+ view_name = nil
361
+ if home_views.include?(layout_name_normalized)
362
+ # Matching view exists
363
+ view_name = layout_name_normalized
364
+ puts "\n#{colorize("Matching view file found: #{view_name}.html.erb", :green)}"
365
+ else
366
+ # No matching view, ask user
367
+ if home_views.empty?
368
+ error_message("No view files found in app/views/home folder!")
369
+ end
370
+
371
+ puts "\n#{colorize("Which view will this layout be used with?", :yellow, bold: true)}"
372
+ home_views.each_with_index do |view, index|
373
+ puts " #{index + 1}. #{view}"
374
+ end
375
+ print "\nChoice (1-#{home_views.length}): "
376
+ choice = STDIN.gets.chomp.to_i
377
+
378
+ if choice < 1 || choice > home_views.length
379
+ error_message("Invalid choice!")
380
+ end
381
+
382
+ view_name = home_views[choice - 1]
383
+ end
384
+
385
+ # Check for existing layout for the same view
386
+ show_message("Checking for existing layout...")
387
+ existing_layout = find_existing_layout_for_view(view_name)
388
+ if existing_layout
389
+ success_message("Check completed")
390
+ error_message("A layout is already defined for '#{view_name}' view: '#{existing_layout}'\nRemove the existing layout first: rails-frontend remove-layout #{existing_layout}")
391
+ end
392
+ success_message("Check completed")
393
+
394
+ # Create layout file
395
+ show_message("Creating layout file...")
396
+ create_layout_file(layout_name_normalized)
397
+ success_message("Layout file created")
398
+
399
+ # Add layout directive to controller
400
+ show_message("Updating Home controller...")
401
+ add_layout_directive(layout_name_normalized, view_name)
402
+ success_message("Home controller updated")
403
+
404
+ puts "\n #{colorize('Layout successfully created!', :green)}"
405
+ puts "Layout file: #{colorize(layout_file, :blue)}"
406
+ puts "Will be used for view: #{colorize("#{view_name}.html.erb", :blue)}"
407
+ end
408
+
409
+ def remove_layout
410
+ # Check if current directory is a Rails project
411
+ is_rails_project?
412
+
413
+ show_title("Removing Layout: #{@layout_name}")
414
+
415
+ # Normalize layout name
416
+ layout_name_normalized = normalize_name(@layout_name)
417
+ layout_file = "app/views/layouts/#{layout_name_normalized}.html.erb"
418
+
419
+ # Check layout file existence
420
+ unless File.exist?(layout_file)
421
+ error_message("Layout not found: #{layout_file}")
422
+ end
423
+
424
+ # Request confirmation
425
+ print colorize("Are you sure you want to delete '#{layout_name_normalized}' layout? (y/n): ", :yellow)
426
+ answer = STDIN.gets.chomp.downcase
427
+ unless answer == 'y' || answer == 'yes'
428
+ puts "\nOperation cancelled."
429
+ exit 0
430
+ end
431
+
432
+ # Remove layout directive from controller
433
+ show_message("Updating Home controller...")
434
+ remove_layout_directive(layout_name_normalized)
435
+ success_message("Home controller updated")
436
+
437
+ # Delete layout file
438
+ show_message("Deleting layout file...")
439
+ FileUtils.rm_f(layout_file)
440
+ success_message("Layout file deleted")
441
+
442
+ puts "\n #{colorize('Layout successfully deleted!', :green)}"
443
+ end
444
+
445
+ def add_pin
446
+ # Check if current directory is a Rails project
447
+ is_rails_project?
448
+
449
+ show_title("Adding Importmap Pin: #{@pin_name}")
450
+
451
+ # Check bin/importmap file existence
452
+ unless File.exist?('bin/importmap')
453
+ error_message("bin/importmap not found! This project may not be using importmap.")
454
+ end
455
+
456
+ # Run bin/importmap pin command
457
+ show_message("Adding pin...")
458
+ output = `bin/importmap pin #{@pin_name} 2>&1`
459
+
460
+ # Check for errors in output
461
+ if output.include?("Couldn't find") || output.include?("error") || output.include?("Error")
462
+ puts "" # New line
463
+ error_message("Failed to add pin! Package not found: #{@pin_name}")
464
+ end
465
+
466
+ success_message("Pin added")
467
+
468
+ puts "\n #{colorize('Pin successfully added!', :green)}"
469
+ puts " #{colorize('Don\'t forget to import it in your project!', :green)}"
470
+ puts "Package: #{colorize(@pin_name, :blue)}"
471
+ end
472
+
473
+ def remove_pin
474
+ # Check if current directory is a Rails project
475
+ is_rails_project?
476
+
477
+ show_title("Removing Importmap Pin: #{@pin_name}")
478
+
479
+ # Check bin/importmap file existence
480
+ unless File.exist?('bin/importmap')
481
+ error_message("bin/importmap not found! This project may not be using importmap.")
482
+ end
483
+
484
+ # Check usage in JavaScript and HTML files
485
+ show_message("Checking usage...")
486
+ used_files = check_pin_usage(@pin_name)
487
+ success_message("Check completed")
488
+
489
+ if used_files.any?
490
+ puts "\n"
491
+ puts colorize("WARNING: This package is being used in the following files:", :yellow, bold: true)
492
+ used_files.each do |file|
493
+ puts " - #{file}"
494
+ end
495
+ puts "\n"
496
+ print colorize("Do you still want to delete it? (y/n): ", :yellow)
497
+ answer = STDIN.gets.chomp.downcase
498
+ unless answer == 'y' || answer == 'yes'
499
+ puts "\nOperation cancelled."
500
+ exit 0
501
+ end
502
+ end
503
+
504
+ # Check pin existence
505
+ show_message("Checking pin...")
506
+ importmap_file = 'config/importmap.rb'
507
+ unless File.exist?(importmap_file)
508
+ error_message("config/importmap.rb not found!")
509
+ end
510
+
511
+ importmap_content = File.read(importmap_file)
512
+ unless importmap_content.match?(/pin\s+["']#{Regexp.escape(@pin_name)}["']/)
513
+ puts "" # New line
514
+ error_message("Pin not found! '#{@pin_name}' is not defined in importmap.")
515
+ end
516
+ success_message("Pin found")
517
+
518
+ # Run bin/importmap unpin command
519
+ show_message("Removing pin...")
520
+ output = `bin/importmap unpin #{@pin_name} 2>&1`
521
+ success_message("Pin removed")
522
+
523
+ puts "\n #{colorize('Pin successfully removed!', :green)}"
524
+ end
525
+
526
+ # Helper methods
527
+ def is_rails_project?
528
+ unless File.exist?('config/routes.rb') && File.exist?('Gemfile')
529
+ error_message("This directory is not a Rails project! Please run inside a Rails project.")
530
+ end
531
+ end
532
+
533
+ def run_server
534
+ is_rails_project?
535
+
536
+ unless File.exist?('bin/dev')
537
+ error_message("bin/dev file not found! This project may not have been created with Rails 7+.")
538
+ end
539
+
540
+ puts "\n#{colorize('Starting Rails server...', :green, bold: true)}"
541
+ puts "#{colorize('Use Ctrl+C to stop', :yellow)}\n\n"
542
+
543
+ exec('bin/dev')
544
+ end
545
+
546
+ def build_static_site
547
+ is_rails_project?
548
+ show_title("Building Static Files")
549
+
550
+ # Server check
551
+ show_message("Checking server...")
552
+ server = check_server
553
+ unless server[:running]
554
+ error_message("Rails server is not running! Start it first with 'rails-frontend run'.")
555
+ end
556
+ success_message("Server is running")
557
+
558
+ # Mirror with wget
559
+ wget_mirror(server[:port])
560
+
561
+ # Prepare build folder
562
+ show_message("Preparing build folder...")
563
+ configure_build
564
+ success_message("Build folder prepared")
565
+
566
+ # Move files
567
+ show_message("Organizing files...")
568
+ move_asset_files
569
+ success_message("Files organized")
570
+
571
+ # Path corrections
572
+ show_message("Fixing paths...")
573
+ fix_html_paths(server[:port])
574
+ fix_css_paths
575
+ success_message("Path fixes completed")
576
+
577
+ # Cleanup
578
+ show_message("Removing unnecessary components...")
579
+ clean_html_files
580
+ success_message("Unnecessary components removed")
581
+
582
+ puts "\n#{colorize('✓ Static site successfully built!', :green)}"
583
+ puts "Folder: #{colorize('build/', :blue)}"
584
+ puts "\nTo test:"
585
+ puts " cd build && python3 -m http.server or npx http-server"
586
+ end
587
+
588
+ # Build helper methods
589
+ def check_server
590
+ pid_file = 'tmp/pids/server.pid'
591
+ return { running: false, port: nil } unless File.exist?(pid_file)
592
+
593
+ pid = File.read(pid_file).strip.to_i
594
+
595
+ begin
596
+ # Find port number
597
+ cmd = `ps -p #{pid} -o args=`.strip
598
+ port = cmd[/tcp:\/\/[^:]+:(\d+)/, 1]
599
+
600
+ { running: true, port: port.to_i }
601
+ rescue Errno::ESRCH, Errno::EPERM
602
+ # Process not found or no access
603
+ { running: false, port: nil }
604
+ end
605
+ end
606
+
607
+ def wget_mirror(port)
608
+ # Delete previous build folder
609
+ FileUtils.rm_rf('build')
610
+
611
+ # Run wget silently
612
+ system("wget --mirror --convert-links --adjust-extension --page-requisites --no-parent --directory-prefix=build http://localhost:#{port}/ > /dev/null 2>&1")
613
+
614
+ # Move localhost:3000 folder into build/
615
+ if Dir.exist?("build/localhost:#{port}")
616
+ Dir.glob("build/localhost:#{port}/*").each do |file|
617
+ FileUtils.mv(file, 'build/')
618
+ end
619
+ FileUtils.rm_rf("build/localhost:#{port}")
620
+ end
621
+ end
622
+
623
+ def configure_build
624
+ ['img', 'js', 'css', 'fonts'].each do |dir|
625
+ FileUtils.mkdir_p("build/assets/#{dir}")
626
+ end
627
+ end
628
+
629
+ def move_asset_files
630
+ extensions = [ "{jpg,jpeg,png,gif,svg,webp,ico}", "js", "css", "{woff,woff2,ttf,eot,otf}" ]
631
+ folders = [ "img", "js", "css", "fonts" ]
632
+
633
+ extensions.zip(folders).each do |extension, folder|
634
+ Dir.glob("build/**/*.#{extension}").each do |file|
635
+ basename = File.basename(file)
636
+ dest = "build/assets/#{folder}/#{basename}"
637
+
638
+ # Move if file exists and destination doesn't
639
+ if File.exist?(file) && !File.exist?(dest)
640
+ FileUtils.mv(file, dest)
641
+ end
642
+ end
643
+ end
644
+
645
+ # If controllers folder exists, move contents and delete
646
+ if Dir.exist?("build/assets/controllers")
647
+ Dir.glob("build/assets/controllers/*").each do |file|
648
+ FileUtils.mv(file, 'build/assets/js/')
649
+ end
650
+ FileUtils.rm_rf("build/assets/controllers")
651
+ end
652
+
653
+ # Delete turbo files (commented out for now as it may be needed for stimulus)
654
+ # Dir.glob('**/*turbo.min*').each do |file|
655
+ # FileUtils.rm_rf(file)
656
+ # end
657
+ end
658
+
659
+ def fix_html_paths(port)
660
+ Dir.glob('build/**/*.html').each do |file|
661
+ content = File.read(file)
662
+
663
+ # 1. Add "js/" after "assets/" for lines containing "assets/" and ending with .js
664
+ # Example: assets/application-bfcdf840.js -> assets/js/application-bfcdf840.js
665
+ content.gsub!(/assets\/([^\/\s"']+\.js)/, 'assets/js/\1')
666
+
667
+ # 2. Add "js/" after "assets/controllers/" for lines containing "assets/controllers/" and ending with .js
668
+ # Example: assets/controllers/index-ee64e1f1.js -> assets/js/index-ee64e1f1.js
669
+ content.gsub!(/assets\/controllers\/([^\/\s"']+\.js)/, 'assets/js/\1')
670
+
671
+ # 3. Add "css/" after "assets/" for lines containing "assets/" and ending with .css
672
+ # Example: assets/application-72587725.css -> assets/css/application-72587725.css
673
+ content.gsub!(/assets\/([^\/\s"']+\.css)/, 'assets/css/\1')
674
+
675
+ # 4. Add "img/" after "assets/" for lines containing "assets/" and ending with image extensions
676
+ # Example: assets/app-image-72587725.jpg -> assets/img/app-image-72587725.jpg
677
+ content.gsub!(/assets\/([^\/\s"']+\.(jpg|jpeg|png|gif|svg|webp))/, 'assets/img/\1')
678
+ content.gsub!(/http:\/\/localhost:#{port}\/([^\/\s"']+\.(jpg|jpeg|png|gif|svg|webp))/, 'assets/img/\1')
679
+
680
+ File.write(file, content)
681
+ end
682
+ end
683
+
684
+ def fix_css_paths
685
+ Dir.glob('build/assets/css/**/*.css').each do |file|
686
+ content = File.read(file)
687
+
688
+ # Font paths - use absolute path
689
+ # Example: url("LavishlyYours-Regular-c6da7860.ttf") -> url("/assets/fonts/LavishlyYours-Regular-c6da7860.ttf")
690
+ content.gsub!(/url\(["']?([^\/]["'()]*\.(woff2?|ttf|eot|otf))["']?\)/, 'url("/assets/fonts/\1")')
691
+
692
+ # Image paths - use absolute path
693
+ # Example: url("A-13904566-1761601378-8017-2b819c09.jpg") -> url("/assets/img/A-13904566-1761601378-8017-2b819c09.jpg")
694
+ content.gsub!(/url\(["']?([^\/]["'()]*\.(jpg|jpeg|png|gif|svg|webp))["']?\)/, 'url("/assets/img/\1")')
695
+
696
+ File.write(file, content)
697
+ end
698
+ end
699
+
700
+ def clean_html_files
701
+ Dir.glob('build/**/*.html').each do |file|
702
+ content = File.read(file)
703
+
704
+ # index.html links
705
+ content.gsub!(/href="[^"]*\/index\.html"/, 'href="/"')
706
+ content.gsub!(/href="index\.html"/, 'href="/"')
707
+
708
+ # Remove Turbo (commented out for now as it may be needed for stimulus)
709
+ # content.gsub!(/^.*turbo\.min.*$\n?/, '')
710
+
711
+ # Remove CSRF
712
+ content.gsub!(/<meta name="csrf-param"[^>]*>/, '')
713
+ content.gsub!(/<meta name="csrf-token"[^>]*>/, '')
714
+
715
+ File.write(file, content)
716
+ end
717
+ end
718
+
719
+ # Helper methods
720
+ def add_home_controller_action(page_name)
721
+ controller_path = 'app/controllers/home_controller.rb'
722
+ return unless File.exist?(controller_path)
723
+
724
+ # Add small delay (for consecutive operations)
725
+ sleep(0.1)
726
+
727
+ controller_content = File.read(controller_path)
728
+
729
+ # Don't add if action already exists - check with word boundary
730
+ # "def products" and "def product" should be detected separately
731
+ return if controller_content.match?(/^\s*def\s+#{Regexp.escape(page_name)}\s*$/)
732
+
733
+ # Find class definition and add before last end
734
+ lines = controller_content.split("\n")
735
+
736
+ # Find index of last end line
737
+ last_end_index = lines.rindex { |line| line.strip == 'end' }
738
+
739
+ if last_end_index
740
+ # Add new action before last end
741
+ new_action_lines = [
742
+ " def #{page_name}",
743
+ " end",
744
+ ""
745
+ ]
746
+
747
+ lines.insert(last_end_index, *new_action_lines)
748
+ controller_content = lines.join("\n")
749
+ File.write(controller_path, controller_content)
750
+ end
751
+ end
752
+
753
+ def remove_home_controller_action(page_name)
754
+ controller_path = 'app/controllers/home_controller.rb'
755
+ return unless File.exist?(controller_path)
756
+
757
+ controller_content = File.read(controller_path)
758
+
759
+ # Remove action
760
+ # Find block starting with " def page_name" and ending with " end"
761
+ controller_content.gsub!(/^\s*def #{Regexp.escape(page_name)}\s*$.*?^\s*end\s*$/m, '')
762
+
763
+ # Clean excessive blank lines (reduce to 2 if more than 3 consecutive blank lines)
764
+ controller_content.gsub!(/\n{3,}/, "\n\n")
765
+
766
+ File.write(controller_path, controller_content)
767
+ end
768
+
769
+ def clean_unnecessary_files
770
+ # Delete files and folders unnecessary for frontend
771
+ unnecessary_files = [
772
+ '.github',
773
+ 'app/models',
774
+ 'app/javascript/controllers/hello_controller.js',
775
+ 'config/environments/production.rb',
776
+ 'config/environments/test.rb',
777
+ 'lib',
778
+ 'public',
779
+ 'script'
780
+ ]
781
+
782
+ unnecessary_files.each do |file|
783
+ # Don't error if non-existent folder deletion
784
+ FileUtils.rm_rf(file) if File.exist?(file) || Dir.exist?(file)
785
+ end
786
+ end
787
+
788
+ def normalize_name(name)
789
+ # Convert Turkish characters and lowercase
790
+ tr_map = {
791
+ 'ç' => 'c', 'Ç' => 'c',
792
+ 'ğ' => 'g', 'Ğ' => 'g',
793
+ 'ı' => 'i', 'İ' => 'i',
794
+ 'ö' => 'o', 'Ö' => 'o',
795
+ 'ş' => 's', 'Ş' => 's',
796
+ 'ü' => 'u', 'Ü' => 'u'
797
+ }
798
+
799
+ normalized = name.downcase
800
+ tr_map.each { |tr, en| normalized.gsub!(tr, en) }
801
+ normalized.gsub(/[^a-z0-9_]/, '_')
802
+ end
803
+
804
+ def get_application_name
805
+ # Read application name from config/application.rb file
806
+ app_config_path = 'config/application.rb'
807
+ if File.exist?(app_config_path)
808
+ content = File.read(app_config_path)
809
+ # Search for "module ApplicationName" pattern
810
+ match = content.match(/module\s+([A-Z][a-zA-Z0-9_]*)/)
811
+ if match
812
+ # Convert CamelCase to title (e.g. MyApp -> My App)
813
+ return match[1].gsub(/([A-Z]+)([A-Z][a-z])/, '\1 \2')
814
+ .gsub(/([a-z\d])([A-Z])/, '\1 \2')
815
+ end
816
+ end
817
+
818
+ # Fallback: Use current directory name
819
+ File.basename(Dir.pwd).split('_').map(&:capitalize).join(' ')
820
+ end
821
+
822
+ def create_home_controller
823
+ controller_content = <<~RUBY
824
+ class HomeController < ApplicationController
825
+ def index
826
+ end
827
+ end
828
+ RUBY
829
+
830
+ FileUtils.mkdir_p('app/controllers')
831
+ File.write('app/controllers/home_controller.rb', controller_content)
832
+
833
+ # Create view folder and file
834
+ FileUtils.mkdir_p('app/views/home')
835
+ view_content = <<~HTML
836
+ <div>
837
+ <div class="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
838
+ <div class="container mx-auto px-4 py-16">
839
+ <div class="text-center">
840
+ <h1 class="text-5xl font-bold text-gray-900 mb-4">
841
+ Welcome! 👋
842
+ </h1>
843
+ <p class="text-xl text-gray-600 mb-8">
844
+ Created with Rails Frontend CLI
845
+ </p>
846
+ <div class="inline-block bg-white rounded-lg shadow-lg p-8">
847
+ <p class="text-gray-700 mb-4">
848
+ Your project has been successfully created and is ready to use!
849
+ </p>
850
+ <p class="text-sm text-gray-500">
851
+ You can start developing with Tailwind CSS and Stimulus.
852
+ </p>
853
+ </div>
854
+ </div>
855
+ </div>
856
+ </div>
857
+ </div>
858
+ HTML
859
+
860
+ File.write('app/views/home/index.html.erb', view_content)
861
+ end
862
+
863
+ def create_shared_components
864
+ FileUtils.mkdir_p('app/views/shared')
865
+
866
+ # Header
867
+ header_content = <<~HTML
868
+ <header class="bg-white shadow-sm">
869
+ <div class="container mx-auto px-4 py-4">
870
+ <div class="flex items-center justify-between">
871
+ <div class="text-2xl font-bold text-indigo-600">
872
+ Logo
873
+ </div>
874
+ <%= render 'shared/navbar' %>
875
+ </div>
876
+ </div>
877
+ </header>
878
+ HTML
879
+ File.write('app/views/shared/_header.html.erb', header_content)
880
+
881
+ # Navbar
882
+ navbar_content = <<~HTML
883
+ <nav class="hidden md:flex space-x-6">
884
+ <%= link_to "Home", root_path, class: "text-gray-700 hover:text-indigo-600 transition" %>
885
+ <!-- Other menu items will be added here -->
886
+ </nav>
887
+ HTML
888
+ File.write('app/views/shared/_navbar.html.erb', navbar_content)
889
+
890
+ # Footer - Simple and full width
891
+ footer_content = <<~HTML
892
+ <footer class="bg-gray-800 text-white py-6 text-center">
893
+ <p class="text-gray-400">
894
+ © <%= Time.current.year %> All rights reserved.
895
+ </p>
896
+ </footer>
897
+ HTML
898
+ File.write('app/views/shared/_footer.html.erb', footer_content)
899
+ end
900
+
901
+ def create_css_files
902
+ FileUtils.mkdir_p('app/assets/stylesheets')
903
+
904
+ # Home CSS
905
+ home_css = <<~CSS
906
+ /* Home page custom styles */
907
+ .home-container {
908
+ /* Custom styles for home page can be added here */
909
+ }
910
+ CSS
911
+ File.write('app/assets/stylesheets/home.css', home_css)
912
+
913
+ # Header CSS
914
+ header_css = <<~CSS
915
+ /* Header custom styles */
916
+ header {
917
+ /* Custom styles for header can be added here */
918
+ }
919
+ CSS
920
+ File.write('app/assets/stylesheets/header.css', header_css)
921
+
922
+ # Navbar CSS
923
+ navbar_css = <<~CSS
924
+ /* Navbar custom styles */
925
+ nav {
926
+ /* Custom styles for navbar can be added here */
927
+ }
928
+ CSS
929
+ File.write('app/assets/stylesheets/navbar.css', navbar_css)
930
+
931
+ # Footer CSS
932
+ footer_css = <<~CSS
933
+ /* Footer custom styles */
934
+ footer {
935
+ /* Custom styles for footer can be added here */
936
+ }
937
+ CSS
938
+ File.write('app/assets/stylesheets/footer.css', footer_css)
939
+ end
940
+
941
+ def create_stimulus_controller(page_name)
942
+ FileUtils.mkdir_p('app/javascript/controllers')
943
+
944
+ controller_content = <<~JS
945
+ import { Controller } from "@hotwired/stimulus"
946
+
947
+ // Stimulus controller for #{page_name.capitalize} page
948
+ export default class extends Controller {
949
+ connect() {
950
+ console.log("#{page_name.capitalize} controller connected")
951
+ }
952
+
953
+ disconnect() {
954
+ console.log("#{page_name.capitalize} controller disconnected")
955
+ }
956
+
957
+ // Custom methods can be added here
958
+ }
959
+ JS
960
+
961
+ File.write("app/javascript/controllers/#{page_name}_controller.js", controller_content)
962
+ end
963
+
964
+ def create_asset_folders
965
+ # Images folder
966
+ FileUtils.mkdir_p('app/assets/images')
967
+ File.write('app/assets/images/.keep', '')
968
+
969
+ # Fonts folder
970
+ FileUtils.mkdir_p('app/assets/fonts')
971
+ File.write('app/assets/fonts/.keep', '')
972
+ end
973
+
974
+ def update_layout
975
+ layout_path = 'app/views/layouts/application.html.erb'
976
+ return unless File.exist?(layout_path)
977
+
978
+ layout_content = File.read(layout_path)
979
+
980
+ # Add UTF-8 charset meta tag (if not present)
981
+ if !layout_content.match?(/<meta\s+charset\s*=\s*["']utf-8["']/i)
982
+ layout_content.gsub!(/<\/title>/) do
983
+ <<~HTML.chomp
984
+ </title>
985
+ <meta charset="utf-8">
986
+ HTML
987
+ end
988
+ end
989
+
990
+ # First clean existing main tags
991
+ layout_content.gsub!(/<main[^>]*>/, '')
992
+ layout_content.gsub!(/<\/main>/, '')
993
+
994
+ # Add shared components inside body
995
+ if layout_content.include?('<body>')
996
+ new_layout = layout_content.gsub(/<body>/) do
997
+ <<~HTML.chomp
998
+ <body>
999
+ <%= render 'shared/header' %>
1000
+ <main class="min-h-screen">
1001
+ HTML
1002
+ end
1003
+
1004
+ # Add footer after yield
1005
+ new_layout = new_layout.gsub(/\s*<%= yield %>/) do
1006
+ <<~HTML.chomp
1007
+ <%= yield %>
1008
+ </main>
1009
+ <%= render 'shared/footer' %>
1010
+ HTML
1011
+ end
1012
+
1013
+ File.write(layout_path, new_layout)
1014
+ end
1015
+ end
1016
+
1017
+ def update_routes(page_name, action, root: false)
1018
+ routes_path = 'config/routes.rb'
1019
+ routes_content = File.read(routes_path)
1020
+
1021
+ if root
1022
+ # Add root route
1023
+ new_route = " root \"home##{action}\"\n"
1024
+
1025
+ # Replace if existing root route exists, otherwise add
1026
+ if routes_content.match?(/^\s*root/)
1027
+ routes_content.gsub!(/^\s*root.*$/, new_route.strip)
1028
+ else
1029
+ routes_content.gsub!(/Rails\.application\.routes\.draw do\n/) do |match|
1030
+ "#{match}#{new_route}"
1031
+ end
1032
+ end
1033
+ else
1034
+ # Add normal route (use home controller)
1035
+ new_route = " get \"/#{page_name}\", to: \"home##{action}\"\n"
1036
+
1037
+ # Don't add if route already exists
1038
+ unless routes_content.include?(new_route.strip)
1039
+ routes_content.gsub!(/Rails\.application\.routes\.draw do\n/) do |match|
1040
+ "#{match}#{new_route}"
1041
+ end
1042
+ end
1043
+ end
1044
+
1045
+ File.write(routes_path, routes_content)
1046
+ end
1047
+
1048
+ def remove_route(page_name)
1049
+ routes_path = 'config/routes.rb'
1050
+ routes_content = File.read(routes_path)
1051
+
1052
+ # Remove route line
1053
+ routes_content.gsub!(/^\s*get\s+['"]\/#{page_name}['"].*\n/, '')
1054
+
1055
+ File.write(routes_path, routes_content)
1056
+ end
1057
+
1058
+ def create_controller(page_name)
1059
+ controller_content = <<~RUBY
1060
+ class #{page_name.capitalize}Controller < ApplicationController
1061
+ def index
1062
+ end
1063
+ end
1064
+ RUBY
1065
+
1066
+ File.write("app/controllers/#{page_name}_controller.rb", controller_content)
1067
+ end
1068
+
1069
+ def create_view(page_name)
1070
+ # Create view in Home folder
1071
+ FileUtils.mkdir_p("app/views/home")
1072
+
1073
+ view_content = <<~HTML
1074
+ <div>
1075
+ <div class="container mx-auto px-4 py-16">
1076
+ <h1 class="text-4xl font-bold text-gray-900 mb-4">
1077
+ #{page_name.capitalize}
1078
+ </h1>
1079
+ <p class="text-gray-600">
1080
+ #{page_name.capitalize} page content will go here.
1081
+ </p>
1082
+ </div>
1083
+ </div>
1084
+ HTML
1085
+
1086
+ File.write("app/views/home/#{page_name}.html.erb", view_content)
1087
+ end
1088
+
1089
+ def create_css(page_name)
1090
+ css_content = <<~CSS
1091
+ /* #{page_name.capitalize} page custom styles */
1092
+ .#{page_name}-container {
1093
+ /* Custom styles for #{page_name} page can be added here */
1094
+ }
1095
+ CSS
1096
+
1097
+ File.write("app/assets/stylesheets/#{page_name}.css", css_content)
1098
+ end
1099
+
1100
+ def update_procfile
1101
+ procfile_path = 'Procfile.dev'
1102
+
1103
+ procfile_content = <<~PROCFILE
1104
+ web: bin/rails server -b 0.0.0.0
1105
+ css: bin/rails tailwindcss:watch
1106
+ PROCFILE
1107
+
1108
+ File.write(procfile_path, procfile_content)
1109
+ end
1110
+
1111
+ def list_home_views
1112
+ views = []
1113
+ if Dir.exist?('app/views/home')
1114
+ Dir.glob('app/views/home/*.html.erb').each do |file|
1115
+ basename = File.basename(file, '.html.erb')
1116
+ # Exclude index
1117
+ views << basename unless basename == 'index'
1118
+ end
1119
+ end
1120
+ views.sort
1121
+ end
1122
+
1123
+ def create_layout_file(layout_name)
1124
+ FileUtils.mkdir_p('app/views/layouts')
1125
+
1126
+ layout_content = <<~HTML
1127
+ <!DOCTYPE html>
1128
+ <html>
1129
+ <head>
1130
+ <title><%= content_for(:title) || "#{layout_name.capitalize}" %></title>
1131
+ <meta charset="utf-8">
1132
+ <meta name="viewport" content="width=device-width,initial-scale=1">
1133
+ <meta name="apple-mobile-web-app-capable" content="yes">
1134
+ <meta name="application-name" content="#{get_application_name}">
1135
+ <meta name="mobile-web-app-capable" content="yes">
1136
+ <%= yield :head %>
1137
+ <%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
1138
+ <%= javascript_importmap_tags %>
1139
+ </head>
1140
+
1141
+ <body>
1142
+ <main>
1143
+ <%= yield %>
1144
+ </main>
1145
+ </body>
1146
+ </html>
1147
+ HTML
1148
+
1149
+ File.write("app/views/layouts/#{layout_name}.html.erb", layout_content)
1150
+ end
1151
+
1152
+ def add_layout_directive(layout_name, view_name)
1153
+ controller_file = 'app/controllers/home_controller.rb'
1154
+ unless File.exist?(controller_file)
1155
+ error_message("Home controller not found: #{controller_file}")
1156
+ end
1157
+
1158
+ controller_content = File.read(controller_file)
1159
+
1160
+ # Check if layout directive already exists
1161
+ if controller_content.match?(/layout\s+["']#{layout_name}["']/)
1162
+ puts "\n#{colorize("WARNING: This layout directive already exists!", :yellow)}"
1163
+ return
1164
+ end
1165
+
1166
+ # Add layout directive after class definition
1167
+ layout_line = " layout \"#{layout_name}\", only: :#{view_name}\n"
1168
+
1169
+ if controller_content.match?(/class\s+HomeController\s*<\s*ApplicationController\s*\n/)
1170
+ controller_content.sub!(/class\s+HomeController\s*<\s*ApplicationController\s*\n/) do |match|
1171
+ "#{match}#{layout_line}\n"
1172
+ end
1173
+ else
1174
+ error_message("HomeController class definition not found!")
1175
+ end
1176
+
1177
+ File.write(controller_file, controller_content)
1178
+ end
1179
+
1180
+ def remove_layout_directive(layout_name)
1181
+ controller_file = 'app/controllers/home_controller.rb'
1182
+ unless File.exist?(controller_file)
1183
+ error_message("Home controller not found: #{controller_file}")
1184
+ end
1185
+
1186
+ controller_content = File.read(controller_file)
1187
+
1188
+ # Find and remove layout directive
1189
+ controller_content.gsub!(/^\s*layout\s+["']#{layout_name}["'].*\n/, '')
1190
+
1191
+ File.write(controller_file, controller_content)
1192
+ end
1193
+
1194
+ def find_existing_layout_for_view(view_name)
1195
+ controller_file = 'app/controllers/home_controller.rb'
1196
+ return nil unless File.exist?(controller_file)
1197
+
1198
+ controller_content = File.read(controller_file)
1199
+
1200
+ # Search for layout "layout_name", only: :view_name pattern
1201
+ match = controller_content.match(/layout\s+["']([^"']+)["'].*only:\s*:#{view_name}\b/)
1202
+ match ? match[1] : nil
1203
+ end
1204
+
1205
+ def check_pin_usage(pin_name)
1206
+ used_files = []
1207
+
1208
+ # Search in JavaScript files
1209
+ if Dir.exist?('app/javascript')
1210
+ Dir.glob('app/javascript/**/*.js').each do |file|
1211
+ content = File.read(file)
1212
+ # Check for exact match with import or from
1213
+ # Example: from "alpinejs" or import "chart.js" or import Alpine from "alpinejs"
1214
+ if content.match?(/from\s+["']#{Regexp.escape(pin_name)}["']/) ||
1215
+ content.match?(/import\s+["']#{Regexp.escape(pin_name)}["']/) ||
1216
+ content.match?(/import\s+.+\s+from\s+["']#{Regexp.escape(pin_name)}["']/)
1217
+ used_files << file
1218
+ end
1219
+ end
1220
+ end
1221
+
1222
+ # Search in HTML/ERB files
1223
+ if Dir.exist?('app/views')
1224
+ Dir.glob('app/views/**/*.html.erb').each do |file|
1225
+ content = File.read(file)
1226
+ # Check for exact match inside script tag or importmap
1227
+ # Looking for exact match in quotes
1228
+ if content.match?(/["']#{Regexp.escape(pin_name)}["']/)
1229
+ used_files << file
1230
+ end
1231
+ end
1232
+ end
1233
+
1234
+ used_files.uniq
1235
+ end
1236
+
1237
+ # Message methods
1238
+ def show_title(message)
1239
+ puts colorize(message, :blue, bold: true)
1240
+ end
1241
+
1242
+ def show_message(message)
1243
+ # Just show message, no numbering
1244
+ print " #{message} "
1245
+ end
1246
+
1247
+ def success_message(message)
1248
+ puts colorize('OK', :green)
1249
+ end
1250
+
1251
+ def error_message(message)
1252
+ puts "\n#{colorize('ERROR:', :red)} #{message}\n"
1253
+ exit 1
1254
+ end
1255
+
1256
+ def completion_message
1257
+ puts colorize("Project successfully created!", :green, bold: true)
1258
+ puts "\n#{colorize('Next steps:', :blue)}"
1259
+ puts " 1. cd #{@project_name}"
1260
+ puts " 2. rails-frontend run"
1261
+ puts " 3. Open http://localhost:3000 in your browser"
1262
+ puts "\n#{colorize('For help:', :blue)}"
1263
+ puts " rails-frontend --help"
1264
+ puts ""
1265
+ end
1266
+
1267
+ def show_help
1268
+ show_title("Rails Frontend CLI v#{VERSION}")
1269
+
1270
+ puts colorize("Rails project management tool for frontend developers", :blue)
1271
+ puts ""
1272
+ puts colorize("USAGE:", :yellow, bold: true)
1273
+ puts " rails-frontend COMMAND [PARAMETERS]"
1274
+ puts ""
1275
+
1276
+ puts colorize("COMMANDS:", :yellow, bold: true)
1277
+ puts <<~COMMANDS
1278
+ Project Management:
1279
+ new, n PROJECT_NAME [--clean] Create new Rails frontend project
1280
+ build, b Build static site
1281
+ run, r Start server (bin/dev)
1282
+
1283
+ Page Management:
1284
+ add-page, ap PAGE_NAME Add new page (view + CSS + route)
1285
+ remove-page, rp PAGE_NAME Remove page
1286
+
1287
+ Stimulus Controller:
1288
+ add-stimulus, as CONTROLLER Add Stimulus controller
1289
+ remove-stimulus, rs CONTROLLER Remove Stimulus controller (checks usage)
1290
+
1291
+ Layout Management:
1292
+ add-layout, al LAYOUT_NAME Add layout (with view matching)
1293
+ remove-layout, rl LAYOUT_NAME Remove layout
1294
+
1295
+ JavaScript Libraries:
1296
+ add-pin, pin PACKAGE_NAME Add external JavaScript library
1297
+ remove-pin, unpin PACKAGE_NAME Remove external JavaScript library (checks usage)
1298
+
1299
+ Info:
1300
+ version, -v, --version Show version info
1301
+ help, -h, --help Show this help message
1302
+ COMMANDS
1303
+
1304
+ puts colorize("OPTIONS:", :yellow, bold: true)
1305
+ puts " --clean Clean unnecessary files for frontend"
1306
+ puts " (tests, mailers, jobs, channels, models etc.)"
1307
+ puts ""
1308
+
1309
+ puts colorize("EXAMPLES:", :yellow, bold: true)
1310
+ puts <<~EXAMPLES
1311
+ Create new project:
1312
+ rails-frontend new blog --clean
1313
+
1314
+ Add page:
1315
+ rails-frontend add-page about
1316
+
1317
+ Add layout:
1318
+ rails-frontend add-layout contact
1319
+
1320
+ Add JavaScript library:
1321
+ rails-frontend add-pin sweetalert2
1322
+
1323
+ Add Stimulus controller:
1324
+ rails-frontend add-stimulus dropdown
1325
+
1326
+ Start server:
1327
+ rails-frontend run
1328
+ EXAMPLES
1329
+
1330
+ puts colorize("MORE INFO:", :blue)
1331
+ puts " Detailed user manual: USER_MANUAL.md"
1332
+ puts " GitHub: https://github.com/ozbilgic/rails-frontend-cli"
1333
+ puts ""
1334
+ end
1335
+
1336
+ def colorize(text, color, bold: false)
1337
+ colors = {
1338
+ red: 31,
1339
+ green: 32,
1340
+ yellow: 33,
1341
+ blue: 34,
1342
+ magenta: 35,
1343
+ cyan: 36
1344
+ }
1345
+
1346
+ color_code = colors[color] || 37
1347
+ bold_code = bold ? '1;' : ''
1348
+
1349
+ "\e[#{bold_code}#{color_code}m#{text}\e[0m"
1350
+ end
1351
+ end
1352
+
1353
+ # When run as a script
1354
+ if __FILE__ == $0
1355
+ cli = RailsFrontendCLI.new
1356
+ cli.run(ARGV)
1357
+ end