Class: Crimson::ProjectContext
- Inherits:
-
Object
- Object
- Crimson::ProjectContext
- Defined in:
- lib/crimson/project_context.rb
Overview
Auto-detects project language, framework, package manager, and testing tools. Also loads project context files (AGENTS.md, CLAUDE.md, etc.) from directory tree.
Constant Summary collapse
- CONTEXT_FILE_NAMES =
File names considered as project instruction files.
%w[ AGENTS.md AGENTS.MD CLAUDE.md CLAUDE.MD GEMINI.md GEMINI.MD ].freeze
Class Method Summary collapse
-
.detect(root_dir = Dir.pwd) ⇒ String
Detect project context (language, framework, package manager, git status).
- .detect_framework(root_dir) ⇒ Object private
- .detect_git(root_dir) ⇒ Object private
- .detect_language(root_dir) ⇒ Object private
- .detect_package_manager(root_dir) ⇒ Object private
- .detect_testing(root_dir) ⇒ Object private
- .file_has_dep?(root_dir, filename, dep_name) ⇒ Boolean private
-
.format_context_files(files) ⇒ String
Format context files into an XML-like string for the system prompt.
- .gem_in_gemfile?(root_dir, gem_name) ⇒ Boolean private
- .git_root?(dir) ⇒ Boolean private
-
.load_context_files(root_dir = Dir.pwd) ⇒ Array<Hash>
Load project context files (AGENTS.md, CLAUDE.md, GEMINI.md) walking up to git root.
Class Method Details
.detect(root_dir = Dir.pwd) ⇒ String
Detect project context (language, framework, package manager, git status).
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
# File 'lib/crimson/project_context.rb', line 19 def self.detect(root_dir = Dir.pwd) context = [] context << "Working directory: #{root_dir}" context << "OS: #{RUBY_PLATFORM}" lang = detect_language(root_dir) context << "Language: #{lang}" if lang framework = detect_framework(root_dir) context << "Framework: #{framework}" if framework pkg = detect_package_manager(root_dir) context << "Package manager: #{pkg}" if pkg testing = detect_testing(root_dir) context << "Testing: #{testing}" if testing git = detect_git(root_dir) context << "Git: #{git}" if git context.join("\n") end |
.detect_framework(root_dir) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
123 124 125 126 127 128 129 130 131 132 133 134 |
# File 'lib/crimson/project_context.rb', line 123 def self.detect_framework(root_dir) return "Rails" if File.exist?(File.join(root_dir, "bin", "rails")) return "Sinatra" if gem_in_gemfile?(root_dir, "sinatra") return "Hanami" if gem_in_gemfile?(root_dir, "hanami") return "Next.js" if file_has_dep?(root_dir, "package.json", "next") return "React" if file_has_dep?(root_dir, "package.json", "react") return "Vue" if file_has_dep?(root_dir, "package.json", "vue") return "Express" if file_has_dep?(root_dir, "package.json", "express") return "Django" if File.exist?(File.join(root_dir, "manage.py")) return "Flask" if file_has_dep?(root_dir, "requirements.txt", "flask") nil end |
.detect_git(root_dir) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
159 160 161 162 163 164 165 166 167 168 |
# File 'lib/crimson/project_context.rb', line 159 def self.detect_git(root_dir) return nil unless Dir.exist?(File.join(root_dir, ".git")) branch = `git rev-parse --abbrev-ref HEAD 2>/dev/null`.strip return nil if branch.empty? dirty = !`git status --porcelain 2>/dev/null`.strip.empty? status = dirty ? "#{branch} (dirty)" : "#{branch} (clean)" status end |
.detect_language(root_dir) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 |
# File 'lib/crimson/project_context.rb', line 101 def self.detect_language(root_dir) indicators = { "Ruby" => ["Gemfile", "*.rb", "*.gemspec"], "Python" => ["requirements.txt", "pyproject.toml", "*.py", "Pipfile"], "TypeScript" => ["tsconfig.json", "*.ts", "*.tsx"], "JavaScript" => ["package.json", "*.js", "*.jsx"], "Go" => ["go.mod", "*.go"], "Rust" => ["Cargo.toml", "*.rs"], "Java" => ["pom.xml", "build.gradle", "*.java"], "Elixir" => ["mix.exs", "*.ex"], } indicators.each do |lang, patterns| patterns.each do |pattern| return lang if Dir.glob(File.join(root_dir, pattern)).any? end end nil end |
.detect_package_manager(root_dir) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
137 138 139 140 141 142 143 144 145 146 |
# File 'lib/crimson/project_context.rb', line 137 def self.detect_package_manager(root_dir) return "bundler" if File.exist?(File.join(root_dir, "Gemfile")) return "npm" if File.exist?(File.join(root_dir, "package-lock.json")) return "yarn" if File.exist?(File.join(root_dir, "yarn.lock")) return "pnpm" if File.exist?(File.join(root_dir, "pnpm-lock.yaml")) return "pip" if File.exist?(File.join(root_dir, "requirements.txt")) return "cargo" if File.exist?(File.join(root_dir, "Cargo.toml")) return "go modules" if File.exist?(File.join(root_dir, "go.mod")) nil end |
.detect_testing(root_dir) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
149 150 151 152 153 154 155 156 |
# File 'lib/crimson/project_context.rb', line 149 def self.detect_testing(root_dir) return "RSpec" if File.exist?(File.join(root_dir, ".rspec")) || gem_in_gemfile?(root_dir, "rspec") return "Minitest" if Dir.glob(File.join(root_dir, "test/**/*_test.rb")).any? return "Jest" if file_has_dep?(root_dir, "package.json", "jest") return "pytest" if File.exist?(File.join(root_dir, "pytest.ini")) return "Go testing" if Dir.glob(File.join(root_dir, "**/*_test.go")).any? nil end |
.file_has_dep?(root_dir, filename, dep_name) ⇒ Boolean
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
178 179 180 181 182 |
# File 'lib/crimson/project_context.rb', line 178 def self.file_has_dep?(root_dir, filename, dep_name) path = File.join(root_dir, filename) return false unless File.exist?(path) File.read(path).include?(dep_name) end |
.format_context_files(files) ⇒ String
Format context files into an XML-like string for the system prompt.
81 82 83 84 85 86 87 88 89 90 91 92 93 |
# File 'lib/crimson/project_context.rb', line 81 def self.format_context_files(files) return "" if files.nil? || files.empty? parts = ["<project_context>", "", "Project-specific instructions and guidelines:", ""] files.each do |f| parts << "<project_instructions path=\"#{f[:path]}\">" parts << f[:content] parts << "</project_instructions>" parts << "" end parts << "</project_context>" parts.join("\n") end |
.gem_in_gemfile?(root_dir, gem_name) ⇒ Boolean
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
171 172 173 174 175 |
# File 'lib/crimson/project_context.rb', line 171 def self.gem_in_gemfile?(root_dir, gem_name) gemfile = File.join(root_dir, "Gemfile") return false unless File.exist?(gemfile) File.read(gemfile).include?(gem_name) end |
.git_root?(dir) ⇒ Boolean
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
96 97 98 |
# File 'lib/crimson/project_context.rb', line 96 def self.git_root?(dir) Dir.exist?(File.join(dir, ".git")) end |
.load_context_files(root_dir = Dir.pwd) ⇒ Array<Hash>
Load project context files (AGENTS.md, CLAUDE.md, GEMINI.md) walking up to git root.
45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
# File 'lib/crimson/project_context.rb', line 45 def self.load_context_files(root_dir = Dir.pwd) files = [] seen_paths = Set.new dir = File.(root_dir) loop do CONTEXT_FILE_NAMES.each do |name| path = File.join(dir, name) next unless File.exist?(path) real = File.realpath(path) rescue File.(path) next if seen_paths.include?(real) seen_paths.add(real) files << { path: path, content: File.read(path) } end break if git_root?(dir) parent = File.dirname(dir) break if parent == dir dir = parent end global = File.join(Crimson::CONFIG_DIR, "AGENTS.md") if File.exist?(global) real = File.realpath(global) rescue File.(global) unless seen_paths.include?(real) files << { path: global, content: File.read(global) } end end files end |