Class: RailsAiContext::Generators::InstallGenerator

Inherits:
Rails::Generators::Base
  • Object
show all
Defined in:
lib/generators/rails_ai_context/install/install_generator.rb

Constant Summary collapse

AI_TOOLS =
{
  "1" => { key: :claude,   name: "Claude Code",     files: "CLAUDE.md + .claude/rules/",                        format: :claude },
  "2" => { key: :cursor,   name: "Cursor",          files: ".cursor/rules/",                                     format: :cursor },
  "3" => { key: :copilot,  name: "GitHub Copilot",  files: ".github/copilot-instructions.md + .github/instructions/", format: :copilot },
  "4" => { key: :opencode, name: "OpenCode",        files: "AGENTS.md",                                          format: :opencode },
  "5" => { key: :codex,   name: "Codex CLI",       files: "AGENTS.md + .codex/config.toml",                     format: :codex }
}.freeze
FORMAT_PATHS =

Files/dirs generated per AI tool format — used for cleanup on tool removal. MCP config files are NOT listed here — they use merge-safe removal via McpConfigGenerator.remove to preserve other servers’ entries.

{
  claude:   %w[CLAUDE.md .claude/rules],
  cursor:   %w[.cursor/rules],
  copilot:  %w[.github/copilot-instructions.md .github/instructions],
  opencode: %w[AGENTS.md app/models/AGENTS.md app/controllers/AGENTS.md],
  codex:    %w[AGENTS.md app/models/AGENTS.md app/controllers/AGENTS.md]
}.freeze
CONFIG_SECTIONS =

All config sections with their marker comment and content. Each section is identified by its marker (e.g., “── AI Tools ──”). On re-install, only sections NOT already present are appended.

{
  "AI Tools" => <<~SECTION,
  "Introspection" => <<~SECTION,
  "Models & Filtering" => <<~SECTION,
  "MCP Server" => <<~SECTION,
  "File Size Limits" => <<~SECTION,
  "Extensibility" => <<~SECTION,
  "Security" => <<~SECTION,
  "Search" => <<~SECTION,
  "Frontend" => <<~SECTION
      # ── Frontend Framework Detection ─────────────────────────────────
      # Auto-detected from package.json, config/vite.json, etc. Override only if needed.
      # config.frontend_paths = ["app/frontend", "../web-client"]
  SECTION
}.freeze

Instance Method Summary collapse

Instance Method Details

#add_to_gitignoreObject



468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
# File 'lib/generators/rails_ai_context/install/install_generator.rb', line 468

def add_to_gitignore
  gitignore = Rails.root.join(".gitignore")
  return unless File.exist?(gitignore)

  content = File.read(gitignore)
  append = []
  append << ".ai-context.json" unless content.include?(".ai-context.json")

  if append.any?
    File.open(gitignore, "a") do |f|
      f.puts ""
      f.puts "# rails-ai-context (JSON cache — markdown files should be committed)"
      append.each { |line| f.puts line }
    end
    say "Updated .gitignore", :green
  end
end

#cleanup_removed_toolsObject



60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/generators/rails_ai_context/install/install_generator.rb', line 60

def cleanup_removed_tools
  @previous_formats = read_previous_ai_tools
  return unless @previous_formats&.any?

  removed = @previous_formats - @selected_formats
  return if removed.empty?

  say ""
  say "These AI tools were removed from your selection:", :yellow
  removed.each_with_index do |fmt, idx|
    tool = AI_TOOLS.values.find { |t| t[:format] == fmt }
    say "  #{idx + 1}. #{tool[:name]} (#{tool[:files]})" if tool
  end
  say ""

  say "Remove their generated files?", :yellow
  say "  y — remove all listed above"
  say "  n — keep all (default)"
  say "  1,2 — remove only specific ones by number"
  say ""

  input = ask("Enter choice:").strip.downcase
  return if input.empty? || input == "n" || input == "no"

  to_remove = if input == "y" || input == "yes" || input == "a"
    removed
  else
    nums = input.split(/[\s,]+/).filter_map { |n| n.to_i - 1 }
    nums.filter_map { |i| removed[i] if i >= 0 && i < removed.size }
  end

  return if to_remove.empty?

  # Collect paths still needed by remaining tools to avoid deleting shared files
  kept_paths = @selected_formats.flat_map { |f| FORMAT_PATHS[f] || [] }.to_set

  to_remove.each do |fmt|
    tool = AI_TOOLS.values.find { |t| t[:format] == fmt }

    # Remove context files (skip if another selected tool still needs them)
    paths = FORMAT_PATHS[fmt] || []
    paths.each do |rel_path|
      next if kept_paths.include?(rel_path)

      full = Rails.root.join(rel_path)
      if File.directory?(full)
        FileUtils.rm_rf(full)
        say "  Removed #{rel_path}/", :red
      elsif File.exist?(full)
        FileUtils.rm_f(full)
        say "  Removed #{rel_path}", :red
      end
    end

    # Merge-safe MCP config cleanup — removes only the rails-ai-context entry
    cleaned = RailsAiContext::McpConfigGenerator.remove(tools: [ fmt ], output_dir: Rails.root.to_s)
    cleaned.each { |f| say "  Removed MCP entry from #{Pathname.new(f).relative_path_from(Rails.root)}", :red }

    say "#{tool[:name]} files removed", :green if tool
  end
end

#create_initializerObject



277
278
279
280
281
282
283
284
285
286
# File 'lib/generators/rails_ai_context/install/install_generator.rb', line 277

def create_initializer
  initializer_path = "config/initializers/rails_ai_context.rb"
  full_path = Rails.root.join(initializer_path)

  if File.exist?(full_path)
    update_existing_initializer(full_path)
  else
    create_new_initializer(initializer_path)
  end
end

#create_mcp_configObject



141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/generators/rails_ai_context/install/install_generator.rb', line 141

def create_mcp_config
  generator = RailsAiContext::McpConfigGenerator.new(
    tools: @selected_formats,
    output_dir: Rails.root.to_s,
    standalone: false,
    tool_mode: @tool_mode
  )
  result = generator.call
  result[:written].each do |f|
    rel = Pathname.new(f).relative_path_from(Rails.root)
    say "Created/Updated #{rel}", :green
  end
  result[:skipped].each do |f|
    rel = Pathname.new(f).relative_path_from(Rails.root)
    say "#{rel} unchanged — skipped", :yellow
  end
  if @tool_mode == :cli
    say "Skipped MCP config files (CLI-only mode)", :yellow
  end
end

#create_yaml_configObject

no_tasks



456
457
458
459
460
461
462
463
464
465
466
# File 'lib/generators/rails_ai_context/install/install_generator.rb', line 456

def create_yaml_config
  yaml_path = Rails.root.join(".rails-ai-context.yml")
  content = {
    "ai_tools" => @selected_formats.map(&:to_s),
    "tool_mode" => @tool_mode.to_s
  }

  require "yaml"
  File.write(yaml_path, YAML.dump(content))
  say "Created .rails-ai-context.yml (standalone config)", :green
end

#generate_context_filesObject



486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
# File 'lib/generators/rails_ai_context/install/install_generator.rb', line 486

def generate_context_files
  say ""
  say "Generating AI context files...", :yellow

  unless Rails.application
    say "  Skipped (Rails app not fully loaded). Run `rails ai:context` after install.", :yellow
    return
  end

  require "rails_ai_context"

  # One-time v5.0.0 legacy UI-pattern files cleanup prompt
  RailsAiContext::LegacyCleanup.prompt_legacy_files(
    @selected_formats, root: Rails.root
  )

  @selected_formats.each do |fmt|
    begin
      result = RailsAiContext.generate_context(format: fmt)
      (result[:written] || []).each { |f| say "#{f}", :green }
      (result[:skipped] || []).each { |f| say "  ⏭️  #{f} (unchanged)", :yellow }
    rescue => e
      say "#{fmt}: #{e.message}", :red
    end
  end
end

#select_ai_toolsObject



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/generators/rails_ai_context/install/install_generator.rb', line 31

def select_ai_tools
  say ""
  say "Which AI tools do you use? (select all that apply)", :yellow
  say ""
  AI_TOOLS.each do |num, info|
    say "  #{num}. #{info[:name].ljust(16)}#{info[:files]}"
  end
  say "  a. All of the above"
  say ""

  input = ask("Enter numbers separated by commas (e.g. 1,2) or 'a' for all:").strip.downcase

  @selected_formats = if input == "a" || input == "all"
    AI_TOOLS.values.map { |t| t[:format] }
  else
    nums = input.split(/[\s,]+/)
    nums.filter_map { |n| AI_TOOLS[n]&.dig(:format) }
  end

  if @selected_formats.empty?
    say "No tools selected — defaulting to all.", :yellow
    @selected_formats = AI_TOOLS.values.map { |t| t[:format] }
  end

  selected_names = AI_TOOLS.values.select { |t| @selected_formats.include?(t[:format]) }.map { |t| t[:name] }
  say ""
  say "Selected: #{selected_names.join(', ')}", :green
end

#select_tool_modeObject



122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/generators/rails_ai_context/install/install_generator.rb', line 122

def select_tool_mode
  say ""
  say "Do you also want MCP server support?", :yellow
  say ""
  say "  1. Yes — MCP primary + CLI fallback (generates per-tool MCP config files)"
  say "  2. No  — CLI only (no server needed)"
  say ""

  input = ask("Enter number (default: 1):").strip

  @tool_mode = case input
  when "2" then :cli
  else :mcp
  end

  mode_label = @tool_mode == :mcp ? "MCP + CLI fallback" : "CLI only"
  say "Selected: #{mode_label}", :green
end

#show_instructionsObject



513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
# File 'lib/generators/rails_ai_context/install/install_generator.rb', line 513

def show_instructions
  say ""
  say "=" * 50, :cyan
  say " rails-ai-context installed!", :cyan
  say "=" * 50, :cyan
  say ""
  say "Your setup:", :yellow
  AI_TOOLS.each_value do |info|
    next unless @selected_formats.include?(info[:format])
    say "#{info[:name].ljust(16)}#{info[:files]}"
  end
  say ""
  say "Commands:", :yellow
  say "  rails ai:context         # Regenerate context files"
  tool_count = RailsAiContext::Server.builtin_tools.size
  say "  rails 'ai:tool[schema]'    # Run any of the #{tool_count} tools from CLI"
  if @tool_mode == :mcp
    say "  rails ai:serve           # Start MCP server (#{tool_count} live tools)"
  end
  say "  rails ai:doctor          # Check AI readiness"
  say "  rails ai:inspect         # Print introspection summary"
  say ""
  if @tool_mode == :mcp
    say "MCP auto-discovery:", :yellow
    say "  Each AI tool gets its own config file — auto-detected on project open."
    say "  No manual config needed."
  else
    say "CLI tools:", :yellow
    say "  AI agents can run `rails 'ai:tool[schema]' table=users` directly."
    say "  No MCP server needed — tools work from the terminal."
  end
  say ""
  say "To add more AI tools later:", :yellow
  say "  rails ai:context:cursor   # Generate for Cursor"
  say "  rails ai:context:copilot  # Generate for Copilot"
  say "  rails generate rails_ai_context:install  # Re-run to pick tools"
  say ""
  say "Standalone (no Gemfile needed):", :yellow
  say "  gem install rails-ai-context"
  say "  rails-ai-context init          # interactive setup"
  say "  rails-ai-context serve         # start MCP server"
  say ""
  say "Commit context files and MCP config files so your team benefits!", :green
end