|
| 1 | +# frozen_string_literal: true |
| 2 | + |
| 3 | +require "rails/generators" |
| 4 | +require "erb" |
| 5 | +require_relative "source_excerpt_helper" |
| 6 | + |
| 7 | +module Strata |
| 8 | + module Generators |
| 9 | + # Generates path-scoped agent rule files for Strata SDK features. |
| 10 | + # Rules embed current SDK source code so re-running the generator |
| 11 | + # updates rules to match the installed SDK version. |
| 12 | + class RulesGenerator < Rails::Generators::NamedBase |
| 13 | + source_root File.expand_path("templates", __dir__) |
| 14 | + include SourceExcerptHelper |
| 15 | + |
| 16 | + SUPPORTED_FEATURES = %w[application_form].freeze |
| 17 | + |
| 18 | + # Manifest of all SDK source files referenced by templates. |
| 19 | + # Specs validate these paths still exist, catching drift when |
| 20 | + # SDK files are renamed or removed. |
| 21 | + SOURCE_REFERENCES = { |
| 22 | + "application_form" => { |
| 23 | + files: %w[ |
| 24 | + app/models/strata/application_form.rb |
| 25 | + app/lib/strata/attributes.rb |
| 26 | + app/lib/strata/attributes/address_attribute.rb |
| 27 | + app/lib/strata/attributes/name_attribute.rb |
| 28 | + app/lib/strata/attributes/memorable_date_attribute.rb |
| 29 | + app/models/concerns/strata/determinable.rb |
| 30 | + app/views/strata/application_forms/index.html.erb |
| 31 | + app/views/strata/application_forms/show.html.erb |
| 32 | + app/views/strata/shared/_form_buttons.html.erb |
| 33 | + app/views/strata/shared/_exit_link.html.erb |
| 34 | + app/views/strata/shared/_breadcrumbs.html.erb |
| 35 | + lib/generators/strata/application_form_views/templates/layout.html.erb.tt |
| 36 | + lib/generators/strata/application_form_views/templates/edit_page.html.erb.tt |
| 37 | + docs/intake-application-forms.md |
| 38 | + docs/multi-page-form-flows.md |
| 39 | + ] |
| 40 | + } |
| 41 | + }.freeze |
| 42 | + |
| 43 | + AGENT_DIRS = { |
| 44 | + "claude" => ".claude/rules", |
| 45 | + "cursor" => ".cursor/rules", |
| 46 | + "copilot" => ".copilot/rules" |
| 47 | + }.freeze |
| 48 | + |
| 49 | + DEFAULT_RULES_DIR = ".agents/rules" |
| 50 | + |
| 51 | + desc "Generates path-scoped agent rules for Strata SDK features" |
| 52 | + |
| 53 | + class_option :agent, type: :string, |
| 54 | + desc: "Target agent (claude, cursor, copilot). Default: writes to .agents/rules/" |
| 55 | + class_option :force, type: :boolean, default: true, |
| 56 | + desc: "Overwrite existing rule files" |
| 57 | + |
| 58 | + def validate_feature_name |
| 59 | + return if name == "all" |
| 60 | + |
| 61 | + unless SUPPORTED_FEATURES.include?(name.underscore) |
| 62 | + raise Thor::Error, "Unknown feature '#{name}'. Supported: #{SUPPORTED_FEATURES.join(', ')}, all" |
| 63 | + end |
| 64 | + end |
| 65 | + |
| 66 | + def generate_rules |
| 67 | + features = name == "all" ? SUPPORTED_FEATURES : [ name.underscore ] |
| 68 | + features.each do |feature| |
| 69 | + # Root rule file |
| 70 | + render_erb_template( |
| 71 | + "#{feature}.md.erb", |
| 72 | + File.join(output_dir, "strata-sdk", "strata-#{feature.dasherize}.md") |
| 73 | + ) |
| 74 | + |
| 75 | + # Sub-rule files from feature subdirectory |
| 76 | + sub_template_dir = File.join(self.class.source_root, feature) |
| 77 | + next unless File.directory?(sub_template_dir) |
| 78 | + |
| 79 | + Dir.glob("#{sub_template_dir}/*.md.erb").sort.each do |sub_template_path| |
| 80 | + sub_name = File.basename(sub_template_path, ".md.erb") |
| 81 | + render_erb_template( |
| 82 | + File.join(feature, "#{sub_name}.md.erb"), |
| 83 | + File.join(output_dir, "strata-sdk", "strata-#{feature.dasherize}", "#{sub_name}.md") |
| 84 | + ) |
| 85 | + end |
| 86 | + end |
| 87 | + end |
| 88 | + |
| 89 | + private |
| 90 | + |
| 91 | + # Walk up from destination_root to find the git root (.git directory). |
| 92 | + # Falls back to destination_root if no .git is found (e.g., in tests). |
| 93 | + def git_root |
| 94 | + dir = Pathname.new(destination_root).expand_path |
| 95 | + loop do |
| 96 | + return dir if (dir + ".git").exist? |
| 97 | + parent = dir.parent |
| 98 | + return Pathname.new(destination_root).expand_path if parent == dir |
| 99 | + dir = parent |
| 100 | + end |
| 101 | + end |
| 102 | + |
| 103 | + def output_dir |
| 104 | + agent_subdir = AGENT_DIRS.fetch(options[:agent]&.downcase, DEFAULT_RULES_DIR) |
| 105 | + git_root.join(agent_subdir).to_s |
| 106 | + end |
| 107 | + |
| 108 | + def render_erb_template(template_relative_path, destination) |
| 109 | + source_path = File.join(self.class.source_root, template_relative_path) |
| 110 | + erb_content = File.read(source_path, encoding: "UTF-8") |
| 111 | + rendered = ERB.new(erb_content, trim_mode: "-").result(binding) |
| 112 | + create_file destination, rendered |
| 113 | + end |
| 114 | + end |
| 115 | + end |
| 116 | +end |
0 commit comments