rails-ai-context 4.3.2 → 4.4.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +127 -53
- data/CLAUDE.md +3 -1
- data/README.md +268 -197
- data/demo-trace.gif +0 -0
- data/demo-trace.tape +21 -0
- data/demo.gif +0 -0
- data/demo.tape +33 -0
- data/docs/GUIDE.md +9 -9
- data/lib/generators/rails_ai_context/install/install_generator.rb +2 -1
- data/lib/rails_ai_context/cli/tool_runner.rb +1 -1
- data/lib/rails_ai_context/configuration.rb +25 -1
- data/lib/rails_ai_context/doctor.rb +4 -2
- data/lib/rails_ai_context/fingerprinter.rb +6 -1
- data/lib/rails_ai_context/introspectors/accessibility_introspector.rb +52 -1
- data/lib/rails_ai_context/introspectors/action_mailbox_introspector.rb +10 -2
- data/lib/rails_ai_context/introspectors/action_text_introspector.rb +22 -2
- data/lib/rails_ai_context/introspectors/active_storage_introspector.rb +50 -4
- data/lib/rails_ai_context/introspectors/api_introspector.rb +41 -5
- data/lib/rails_ai_context/introspectors/asset_pipeline_introspector.rb +10 -4
- data/lib/rails_ai_context/introspectors/auth_introspector.rb +62 -7
- data/lib/rails_ai_context/introspectors/component_introspector.rb +6 -0
- data/lib/rails_ai_context/introspectors/config_introspector.rb +59 -9
- data/lib/rails_ai_context/introspectors/controller_introspector.rb +45 -13
- data/lib/rails_ai_context/introspectors/convention_detector.rb +25 -2
- data/lib/rails_ai_context/introspectors/database_stats_introspector.rb +58 -4
- data/lib/rails_ai_context/introspectors/design_token_introspector.rb +27 -5
- data/lib/rails_ai_context/introspectors/devops_introspector.rb +15 -8
- data/lib/rails_ai_context/introspectors/engine_introspector.rb +12 -3
- data/lib/rails_ai_context/introspectors/frontend_framework_introspector.rb +36 -1
- data/lib/rails_ai_context/introspectors/gem_introspector.rb +47 -1
- data/lib/rails_ai_context/introspectors/i18n_introspector.rb +49 -3
- data/lib/rails_ai_context/introspectors/job_introspector.rb +48 -5
- data/lib/rails_ai_context/introspectors/middleware_introspector.rb +24 -3
- data/lib/rails_ai_context/introspectors/migration_introspector.rb +4 -1
- data/lib/rails_ai_context/introspectors/model_introspector.rb +108 -11
- data/lib/rails_ai_context/introspectors/multi_database_introspector.rb +57 -12
- data/lib/rails_ai_context/introspectors/performance_introspector.rb +34 -9
- data/lib/rails_ai_context/introspectors/rake_task_introspector.rb +12 -2
- data/lib/rails_ai_context/introspectors/route_introspector.rb +25 -8
- data/lib/rails_ai_context/introspectors/schema_introspector.rb +45 -7
- data/lib/rails_ai_context/introspectors/seeds_introspector.rb +5 -2
- data/lib/rails_ai_context/introspectors/stimulus_introspector.rb +59 -6
- data/lib/rails_ai_context/introspectors/test_introspector.rb +50 -5
- data/lib/rails_ai_context/introspectors/turbo_introspector.rb +44 -13
- data/lib/rails_ai_context/introspectors/view_introspector.rb +46 -7
- data/lib/rails_ai_context/introspectors/view_template_introspector.rb +25 -7
- data/lib/rails_ai_context/resources.rb +1 -1
- data/lib/rails_ai_context/server.rb +6 -3
- data/lib/rails_ai_context/tasks/rails_ai_context.rake +8 -4
- data/lib/rails_ai_context/tools/analyze_feature.rb +66 -19
- data/lib/rails_ai_context/tools/base_tool.rb +1 -1
- data/lib/rails_ai_context/tools/diagnose.rb +4 -2
- data/lib/rails_ai_context/tools/get_callbacks.rb +4 -2
- data/lib/rails_ai_context/tools/get_concern.rb +12 -6
- data/lib/rails_ai_context/tools/get_controllers.rb +10 -5
- data/lib/rails_ai_context/tools/get_conventions.rb +4 -2
- data/lib/rails_ai_context/tools/get_design_system.rb +2 -1
- data/lib/rails_ai_context/tools/get_env.rb +8 -4
- data/lib/rails_ai_context/tools/get_helper_methods.rb +6 -3
- data/lib/rails_ai_context/tools/get_job_pattern.rb +2 -1
- data/lib/rails_ai_context/tools/get_model_details.rb +10 -5
- data/lib/rails_ai_context/tools/get_partial_interface.rb +14 -7
- data/lib/rails_ai_context/tools/get_schema.rb +2 -1
- data/lib/rails_ai_context/tools/get_service_pattern.rb +2 -1
- data/lib/rails_ai_context/tools/get_stimulus.rb +2 -1
- data/lib/rails_ai_context/tools/get_test_info.rb +4 -2
- data/lib/rails_ai_context/tools/get_turbo_map.rb +22 -11
- data/lib/rails_ai_context/tools/get_view.rb +6 -3
- data/lib/rails_ai_context/tools/migration_advisor.rb +2 -1
- data/lib/rails_ai_context/tools/onboard.rb +2 -1
- data/lib/rails_ai_context/tools/performance_check.rb +2 -1
- data/lib/rails_ai_context/tools/query.rb +5 -1
- data/lib/rails_ai_context/tools/read_logs.rb +3 -0
- data/lib/rails_ai_context/tools/runtime_info.rb +10 -5
- data/lib/rails_ai_context/tools/search_code.rb +8 -4
- data/lib/rails_ai_context/tools/search_docs.rb +2 -1
- data/lib/rails_ai_context/tools/session_context.rb +2 -1
- data/lib/rails_ai_context/tools/validate.rb +16 -8
- data/lib/rails_ai_context/version.rb +1 -1
- metadata +5 -1
data/demo.tape
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# VHS demo tape for rails-ai-context install
|
|
2
|
+
Output demo.gif
|
|
3
|
+
|
|
4
|
+
Set Shell zsh
|
|
5
|
+
Set FontSize 14
|
|
6
|
+
Set Width 950
|
|
7
|
+
Set Height 520
|
|
8
|
+
Set Padding 12
|
|
9
|
+
Set Theme "Catppuccin Mocha"
|
|
10
|
+
Set TypingSpeed 35ms
|
|
11
|
+
|
|
12
|
+
Type "cd /Users/crisjosephnahine/Documents/Projects/daily-content-chef"
|
|
13
|
+
Enter
|
|
14
|
+
Sleep 1s
|
|
15
|
+
|
|
16
|
+
Type "rails generate rails_ai_context:install"
|
|
17
|
+
Sleep 400ms
|
|
18
|
+
Enter
|
|
19
|
+
Sleep 3s
|
|
20
|
+
|
|
21
|
+
# Select Claude Code + Cursor
|
|
22
|
+
Type "1,2"
|
|
23
|
+
Sleep 300ms
|
|
24
|
+
Enter
|
|
25
|
+
Sleep 2s
|
|
26
|
+
|
|
27
|
+
# Select MCP mode (yes)
|
|
28
|
+
Type "1"
|
|
29
|
+
Sleep 300ms
|
|
30
|
+
Enter
|
|
31
|
+
Sleep 4s
|
|
32
|
+
|
|
33
|
+
Sleep 2s
|
data/docs/GUIDE.md
CHANGED
|
@@ -1128,7 +1128,7 @@ Both transports are **read-only** — they expose the same 39 tools and never mo
|
|
|
1128
1128
|
RailsAiContext.configure do |config|
|
|
1129
1129
|
# --- Introspectors ---
|
|
1130
1130
|
|
|
1131
|
-
# Presets: :full (
|
|
1131
|
+
# Presets: :full (33 introspectors, default) or :standard (19)
|
|
1132
1132
|
config.preset = :full
|
|
1133
1133
|
|
|
1134
1134
|
# Cherry-pick on top of a preset
|
|
@@ -1300,7 +1300,7 @@ All split rules include an app overview file, so no context is lost when root fi
|
|
|
1300
1300
|
|
|
1301
1301
|
## Introspectors — Full List
|
|
1302
1302
|
|
|
1303
|
-
### Standard preset (
|
|
1303
|
+
### Standard preset (19 introspectors)
|
|
1304
1304
|
|
|
1305
1305
|
Core Rails structure only. Use `config.preset = :standard` for a lighter footprint.
|
|
1306
1306
|
|
|
@@ -1320,19 +1320,21 @@ Core Rails structure only. Use `config.preset = :standard` for a lighter footpri
|
|
|
1320
1320
|
| `view_templates` | View file contents, partial references, Stimulus data attributes, UI pattern extraction, model field usage in partials. |
|
|
1321
1321
|
| `design_tokens` | Auto-detects CSS framework (Tailwind v3/v4, Bootstrap, Sass, plain CSS) and extracts design tokens from config files and built CSS. |
|
|
1322
1322
|
| `components` | ViewComponent/Phlex components: props, slots, previews, sidecar assets, usage examples. |
|
|
1323
|
+
| `turbo` | Turbo Frames (IDs and files), Turbo Stream templates, model broadcasts (`broadcasts_to`, `broadcasts`). |
|
|
1324
|
+
| `auth` | Devise models with modules, Rails 8 built-in auth, has_secure_password, Pundit policies, CanCanCan, CORS config, CSP config. |
|
|
1325
|
+
| `accessibility` | ARIA attributes, semantic HTML elements, screen reader text, alt text coverage, landmark roles, accessibility score. |
|
|
1326
|
+
| `performance` | N+1 query risks, missing counter_cache, missing FK indexes, Model.all anti-patterns, eager load candidates. |
|
|
1327
|
+
| `i18n` | Default locale, available locales, locale files with key counts, backend class, parse errors. |
|
|
1323
1328
|
|
|
1324
|
-
### Full preset (
|
|
1329
|
+
### Full preset (33 introspectors) — default
|
|
1325
1330
|
|
|
1326
1331
|
Includes all standard introspectors plus:
|
|
1327
1332
|
|
|
1328
1333
|
| Introspector | What it discovers |
|
|
1329
1334
|
|-------------|-------------------|
|
|
1330
1335
|
| `views` | Layouts, templates grouped by controller, partials (per-controller and shared), helpers with methods, template engines (erb, haml, slim), view components. |
|
|
1331
|
-
| `turbo` | Turbo Frames (IDs and files), Turbo Stream templates, model broadcasts (`broadcasts_to`, `broadcasts`). |
|
|
1332
|
-
| `i18n` | Default locale, available locales, locale files with key counts, backend class, parse errors. |
|
|
1333
1336
|
| `active_storage` | Attachments (has_one_attached, has_many_attached per model), storage services, direct upload config. |
|
|
1334
1337
|
| `action_text` | Rich text fields (has_rich_text per model), Action Text installation status. |
|
|
1335
|
-
| `auth` | Devise models with modules, Rails 8 built-in auth, has_secure_password, Pundit policies, CanCanCan, CORS config, CSP config. |
|
|
1336
1338
|
| `api` | API-only mode, API versioning (from directory structure), serializers (Jbuilder, AMS, etc.), GraphQL (types, mutations), rate limiting (Rack::Attack). |
|
|
1337
1339
|
| `rake_tasks` | Custom rake tasks in `lib/tasks/` with names, descriptions, namespaces, file paths. |
|
|
1338
1340
|
| `assets` | Asset pipeline (Propshaft/Sprockets), JS bundler (importmap/esbuild/webpack/vite), CSS framework, importmap pins, manifest files. |
|
|
@@ -1342,10 +1344,8 @@ Includes all standard introspectors plus:
|
|
|
1342
1344
|
| `middleware` | Custom Rack middleware in app/middleware/ with detected patterns (auth, rate limiting, tenant isolation, logging). Full middleware stack. |
|
|
1343
1345
|
| `engines` | Mounted Rails engines from routes.rb with paths and descriptions for 23+ known engines (Sidekiq::Web, Flipper::UI, PgHero, ActiveAdmin, etc.). |
|
|
1344
1346
|
| `multi_database` | Multiple databases, replicas, sharding config, model-specific `connects_to` declarations. database.yml parsing fallback. |
|
|
1345
|
-
| `accessibility` | ARIA attributes, semantic HTML elements, screen reader text, alt text coverage, landmark roles, accessibility score. |
|
|
1346
|
-
| `performance` | N+1 query risks, missing counter_cache, missing FK indexes, Model.all anti-patterns, eager load candidates. |
|
|
1347
1347
|
| `frontend_frameworks` | Frontend JS framework detection (React/Vue/Svelte/Angular), mounting strategy (Inertia/react-rails), TypeScript config, state management, package manager. |
|
|
1348
|
-
| `database_stats` | PostgreSQL approximate row counts via `pg_stat_user_tables`.
|
|
1348
|
+
| `database_stats` | PostgreSQL approximate row counts via `pg_stat_user_tables`. Gracefully skips on non-PostgreSQL adapters. |
|
|
1349
1349
|
|
|
1350
1350
|
### Using the standard preset
|
|
1351
1351
|
|
|
@@ -6,7 +6,7 @@ module RailsAiContext
|
|
|
6
6
|
standard: %i[schema models routes jobs gems conventions controllers tests migrations stimulus
|
|
7
7
|
view_templates design_tokens config components
|
|
8
8
|
turbo auth accessibility performance i18n],
|
|
9
|
-
full: %i[schema models routes jobs gems conventions stimulus controllers views view_templates design_tokens turbo
|
|
9
|
+
full: %i[schema models routes jobs gems conventions stimulus database_stats controllers views view_templates design_tokens turbo
|
|
10
10
|
i18n config active_storage action_text auth api tests rake_tasks assets
|
|
11
11
|
devops action_mailbox migrations seeds middleware engines multi_database
|
|
12
12
|
components accessibility performance frontend_frameworks]
|
|
@@ -198,5 +198,29 @@ module RailsAiContext
|
|
|
198
198
|
def output_dir_for(app)
|
|
199
199
|
@output_dir || app.root.to_s
|
|
200
200
|
end
|
|
201
|
+
|
|
202
|
+
def http_port=(value)
|
|
203
|
+
value = value.to_i
|
|
204
|
+
raise ArgumentError, "http_port must be between 1 and 65535 (got #{value})" unless value.between?(1, 65535)
|
|
205
|
+
@http_port = value
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def cache_ttl=(value)
|
|
209
|
+
value = value.to_i
|
|
210
|
+
raise ArgumentError, "cache_ttl must be positive (got #{value})" unless value > 0
|
|
211
|
+
@cache_ttl = value
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def max_tool_response_chars=(value)
|
|
215
|
+
value = value.to_i
|
|
216
|
+
raise ArgumentError, "max_tool_response_chars must be positive (got #{value})" unless value > 0
|
|
217
|
+
@max_tool_response_chars = value
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def query_row_limit=(value)
|
|
221
|
+
value = value.to_i
|
|
222
|
+
raise ArgumentError, "query_row_limit must be between 1 and 1000 (got #{value})" unless value.between?(1, 1000)
|
|
223
|
+
@query_row_limit = value
|
|
224
|
+
end
|
|
201
225
|
end
|
|
202
226
|
end
|
|
@@ -84,7 +84,8 @@ module RailsAiContext
|
|
|
84
84
|
message: "#{pending.size} pending migration(s) — schema data will be stale",
|
|
85
85
|
fix: "Run `rails db:migrate`")
|
|
86
86
|
end
|
|
87
|
-
rescue
|
|
87
|
+
rescue => e
|
|
88
|
+
$stderr.puts "[rails-ai-context] check_pending_migrations failed: #{e.message}" if ENV["DEBUG"]
|
|
88
89
|
# Can't check pending migrations in this environment
|
|
89
90
|
nil
|
|
90
91
|
end
|
|
@@ -234,7 +235,8 @@ module RailsAiContext
|
|
|
234
235
|
message: "#{errors.size} introspector(s) returned errors: #{errors.join(', ')}",
|
|
235
236
|
fix: "Check if the app has the required features (e.g., stimulus needs app/javascript/controllers/)")
|
|
236
237
|
end
|
|
237
|
-
rescue
|
|
238
|
+
rescue => e
|
|
239
|
+
$stderr.puts "[rails-ai-context] check_introspector_health failed: #{e.message}" if ENV["DEBUG"]
|
|
238
240
|
nil
|
|
239
241
|
end
|
|
240
242
|
|
|
@@ -50,6 +50,8 @@ module RailsAiContext
|
|
|
50
50
|
WATCHED_FILES.each do |file|
|
|
51
51
|
path = File.join(root, file)
|
|
52
52
|
digest.update(File.mtime(path).to_f.to_s) if File.exist?(path)
|
|
53
|
+
rescue Errno::ENOENT
|
|
54
|
+
# File deleted between exist? check and mtime read — skip
|
|
53
55
|
end
|
|
54
56
|
|
|
55
57
|
WATCHED_DIRS.each do |dir|
|
|
@@ -58,6 +60,8 @@ module RailsAiContext
|
|
|
58
60
|
|
|
59
61
|
Dir.glob(File.join(full_dir, "**/*.{rb,rake,js,ts,erb,haml,slim,yml}")).sort.each do |path|
|
|
60
62
|
digest.update(File.mtime(path).to_f.to_s)
|
|
63
|
+
rescue Errno::ENOENT
|
|
64
|
+
# File deleted between glob and mtime read — skip
|
|
61
65
|
end
|
|
62
66
|
end
|
|
63
67
|
|
|
@@ -71,7 +75,8 @@ module RailsAiContext
|
|
|
71
75
|
spec = Bundler.rubygems.find_name("rails-ai-context").first
|
|
72
76
|
return false unless spec
|
|
73
77
|
spec.source.is_a?(Bundler::Source::Path)
|
|
74
|
-
rescue
|
|
78
|
+
rescue => e
|
|
79
|
+
$stderr.puts "[rails-ai-context] local_gem_path? failed: #{e.message}" if ENV["DEBUG"]
|
|
75
80
|
false
|
|
76
81
|
end
|
|
77
82
|
|
|
@@ -21,6 +21,10 @@ module RailsAiContext
|
|
|
21
21
|
images: analyze_images(views),
|
|
22
22
|
labels: analyze_labels(views),
|
|
23
23
|
landmarks: extract_landmarks(views),
|
|
24
|
+
heading_hierarchy: analyze_heading_hierarchy(views),
|
|
25
|
+
skip_links: detect_skip_links(views),
|
|
26
|
+
live_regions: count_live_regions(views),
|
|
27
|
+
form_inputs: analyze_form_inputs(views),
|
|
24
28
|
summary: build_summary(views)
|
|
25
29
|
}
|
|
26
30
|
rescue => e
|
|
@@ -50,7 +54,8 @@ module RailsAiContext
|
|
|
50
54
|
Dir.glob(File.join(dir, "**/*.{erb,haml,slim,html}")).each do |path|
|
|
51
55
|
content = File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace)
|
|
52
56
|
views << { file: path.sub("#{root}/", ""), content: content }
|
|
53
|
-
rescue
|
|
57
|
+
rescue => e
|
|
58
|
+
$stderr.puts "[rails-ai-context] collect_view_content failed: #{e.message}" if ENV["DEBUG"]
|
|
54
59
|
next
|
|
55
60
|
end
|
|
56
61
|
end
|
|
@@ -149,6 +154,52 @@ module RailsAiContext
|
|
|
149
154
|
landmarks
|
|
150
155
|
end
|
|
151
156
|
|
|
157
|
+
def analyze_heading_hierarchy(views)
|
|
158
|
+
all_content = views.map { |v| v[:content] }.join("\n")
|
|
159
|
+
counts = {}
|
|
160
|
+
(1..6).each do |level|
|
|
161
|
+
count = all_content.scan(/<h#{level}[\s>]/i).size
|
|
162
|
+
counts["h#{level}"] = count if count > 0
|
|
163
|
+
end
|
|
164
|
+
counts
|
|
165
|
+
rescue => e
|
|
166
|
+
$stderr.puts "[rails-ai-context] analyze_heading_hierarchy failed: #{e.message}" if ENV["DEBUG"]
|
|
167
|
+
{}
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def detect_skip_links(views)
|
|
171
|
+
all_content = views.map { |v| v[:content] }.join("\n")
|
|
172
|
+
{
|
|
173
|
+
skip_to_content: all_content.scan(/skip.to.(?:content|main)/i).size > 0,
|
|
174
|
+
skip_navigation: all_content.scan(/skip.nav/i).size > 0
|
|
175
|
+
}
|
|
176
|
+
rescue => e
|
|
177
|
+
$stderr.puts "[rails-ai-context] detect_skip_links failed: #{e.message}" if ENV["DEBUG"]
|
|
178
|
+
{}
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def count_live_regions(views)
|
|
182
|
+
all_content = views.map { |v| v[:content] }.join("\n")
|
|
183
|
+
{
|
|
184
|
+
aria_live: all_content.scan(/aria-live=["'](\w+)["']/).flatten.tally,
|
|
185
|
+
aria_atomic: all_content.scan(/aria-atomic=/).size
|
|
186
|
+
}
|
|
187
|
+
rescue => e
|
|
188
|
+
$stderr.puts "[rails-ai-context] count_live_regions failed: #{e.message}" if ENV["DEBUG"]
|
|
189
|
+
{}
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def analyze_form_inputs(views)
|
|
193
|
+
all_content = views.map { |v| v[:content] }.join("\n")
|
|
194
|
+
{
|
|
195
|
+
required: all_content.scan(/\brequired\b/).size,
|
|
196
|
+
input_types: all_content.scan(/type=["'](\w+)["']/).flatten.tally
|
|
197
|
+
}
|
|
198
|
+
rescue => e
|
|
199
|
+
$stderr.puts "[rails-ai-context] analyze_form_inputs failed: #{e.message}" if ENV["DEBUG"]
|
|
200
|
+
{}
|
|
201
|
+
end
|
|
202
|
+
|
|
152
203
|
def build_summary(views)
|
|
153
204
|
all_content = views.map { |v| v[:content] }.join("\n")
|
|
154
205
|
|
|
@@ -40,8 +40,16 @@ module RailsAiContext
|
|
|
40
40
|
{ pattern: match[0], action: match[1] }
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
# Extract callbacks
|
|
44
|
+
callbacks = content.scan(/\b(before_processing|after_processing|around_processing)\s+:(\w+)/).map do |type, method|
|
|
45
|
+
{ type: type, method: method }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
entry = { name: name, file: relative, routing: routing }
|
|
49
|
+
entry[:callbacks] = callbacks if callbacks.any?
|
|
50
|
+
entry
|
|
51
|
+
rescue => e
|
|
52
|
+
$stderr.puts "[rails-ai-context] extract_mailboxes failed: #{e.message}" if ENV["DEBUG"]
|
|
45
53
|
nil
|
|
46
54
|
end.sort_by { |m| m[:name] }
|
|
47
55
|
end
|
|
@@ -13,7 +13,8 @@ module RailsAiContext
|
|
|
13
13
|
def call
|
|
14
14
|
{
|
|
15
15
|
installed: defined?(ActionText) ? true : false,
|
|
16
|
-
rich_text_fields: extract_rich_text_fields
|
|
16
|
+
rich_text_fields: extract_rich_text_fields,
|
|
17
|
+
trix_customizations: detect_trix_customizations
|
|
17
18
|
}
|
|
18
19
|
rescue => e
|
|
19
20
|
{ error: e.message }
|
|
@@ -25,6 +26,24 @@ module RailsAiContext
|
|
|
25
26
|
app.root.to_s
|
|
26
27
|
end
|
|
27
28
|
|
|
29
|
+
def detect_trix_customizations
|
|
30
|
+
customs = []
|
|
31
|
+
js_dirs = [ File.join(app.root, "app", "javascript"), File.join(app.root, "app", "assets", "javascripts") ]
|
|
32
|
+
js_dirs.each do |dir|
|
|
33
|
+
next unless Dir.exist?(dir)
|
|
34
|
+
Dir.glob(File.join(dir, "**", "*.{js,ts}")).each do |path|
|
|
35
|
+
content = File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace)
|
|
36
|
+
customs << "custom_toolbar" if content.match?(/Trix\.config\.toolbar/)
|
|
37
|
+
customs << "custom_attachment" if content.match?(/trix-attachment|Trix\.Attachment/)
|
|
38
|
+
customs << "custom_editor" if content.match?(/trix-initialize|trix-change/)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
customs.uniq
|
|
42
|
+
rescue => e
|
|
43
|
+
$stderr.puts "[rails-ai-context] detect_trix_customizations failed: #{e.message}" if ENV["DEBUG"]
|
|
44
|
+
[]
|
|
45
|
+
end
|
|
46
|
+
|
|
28
47
|
def extract_rich_text_fields
|
|
29
48
|
models_dir = File.join(root, "app/models")
|
|
30
49
|
return [] unless Dir.exist?(models_dir)
|
|
@@ -40,7 +59,8 @@ module RailsAiContext
|
|
|
40
59
|
end
|
|
41
60
|
|
|
42
61
|
fields.sort_by { |f| [ f[:model], f[:field] ] }
|
|
43
|
-
rescue
|
|
62
|
+
rescue => e
|
|
63
|
+
$stderr.puts "[rails-ai-context] extract_rich_text_fields failed: #{e.message}" if ENV["DEBUG"]
|
|
44
64
|
[]
|
|
45
65
|
end
|
|
46
66
|
end
|
|
@@ -16,7 +16,9 @@ module RailsAiContext
|
|
|
16
16
|
installed: defined?(ActiveStorage) ? true : false,
|
|
17
17
|
attachments: extract_attachments,
|
|
18
18
|
storage_services: extract_storage_services,
|
|
19
|
-
direct_upload: detect_direct_upload
|
|
19
|
+
direct_upload: detect_direct_upload,
|
|
20
|
+
validations: extract_attachment_validations,
|
|
21
|
+
variants: extract_variants
|
|
20
22
|
}
|
|
21
23
|
rescue => e
|
|
22
24
|
{ error: e.message }
|
|
@@ -47,7 +49,8 @@ module RailsAiContext
|
|
|
47
49
|
end
|
|
48
50
|
|
|
49
51
|
attachments.sort_by { |a| [ a[:model], a[:name] ] }
|
|
50
|
-
rescue
|
|
52
|
+
rescue => e
|
|
53
|
+
$stderr.puts "[rails-ai-context] extract_attachments failed: #{e.message}" if ENV["DEBUG"]
|
|
51
54
|
[]
|
|
52
55
|
end
|
|
53
56
|
|
|
@@ -58,7 +61,49 @@ module RailsAiContext
|
|
|
58
61
|
require "yaml"
|
|
59
62
|
config = YAML.load_file(config_path, permitted_classes: [ Symbol ], aliases: true) || {}
|
|
60
63
|
config.keys.sort
|
|
61
|
-
rescue
|
|
64
|
+
rescue => e
|
|
65
|
+
$stderr.puts "[rails-ai-context] extract_storage_services failed: #{e.message}" if ENV["DEBUG"]
|
|
66
|
+
[]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def extract_attachment_validations
|
|
70
|
+
validations = []
|
|
71
|
+
models_dir = File.join(app.root, "app", "models")
|
|
72
|
+
return validations unless Dir.exist?(models_dir)
|
|
73
|
+
|
|
74
|
+
Dir.glob(File.join(models_dir, "**", "*.rb")).each do |path|
|
|
75
|
+
content = File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace)
|
|
76
|
+
model = File.basename(path, ".rb").camelize
|
|
77
|
+
content.each_line do |line|
|
|
78
|
+
if (match = line.match(/validates?\s+:(\w+),.*content_type:/))
|
|
79
|
+
validations << { model: model, attachment: match[1], type: "content_type" }
|
|
80
|
+
end
|
|
81
|
+
if (match = line.match(/validates?\s+:(\w+),.*size:/))
|
|
82
|
+
validations << { model: model, attachment: match[1], type: "size" }
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
validations
|
|
87
|
+
rescue => e
|
|
88
|
+
$stderr.puts "[rails-ai-context] extract_attachment_validations failed: #{e.message}" if ENV["DEBUG"]
|
|
89
|
+
[]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def extract_variants
|
|
93
|
+
variants = []
|
|
94
|
+
models_dir = File.join(app.root, "app", "models")
|
|
95
|
+
return variants unless Dir.exist?(models_dir)
|
|
96
|
+
|
|
97
|
+
Dir.glob(File.join(models_dir, "**", "*.rb")).each do |path|
|
|
98
|
+
content = File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace)
|
|
99
|
+
model = File.basename(path, ".rb").camelize
|
|
100
|
+
content.scan(/\.variant\s*\(\s*:(\w+)/).each do |name|
|
|
101
|
+
variants << { model: model, name: name[0] }
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
variants
|
|
105
|
+
rescue => e
|
|
106
|
+
$stderr.puts "[rails-ai-context] extract_variants failed: #{e.message}" if ENV["DEBUG"]
|
|
62
107
|
[]
|
|
63
108
|
end
|
|
64
109
|
|
|
@@ -71,7 +116,8 @@ module RailsAiContext
|
|
|
71
116
|
Dir.glob(File.join(dir, "**/*")).any? do |f|
|
|
72
117
|
next false if File.directory?(f)
|
|
73
118
|
File.read(f).match?(/direct.upload|DirectUpload|direct_upload/)
|
|
74
|
-
rescue
|
|
119
|
+
rescue => e
|
|
120
|
+
$stderr.puts "[rails-ai-context] detect_direct_upload failed: #{e.message}" if ENV["DEBUG"]
|
|
75
121
|
false
|
|
76
122
|
end
|
|
77
123
|
end
|
|
@@ -20,7 +20,9 @@ module RailsAiContext
|
|
|
20
20
|
rate_limiting: detect_rate_limiting,
|
|
21
21
|
openapi_spec: detect_openapi_specs,
|
|
22
22
|
cors_config: detect_cors_config,
|
|
23
|
-
api_client_generation: detect_api_client_generation
|
|
23
|
+
api_client_generation: detect_api_client_generation,
|
|
24
|
+
graphql_details: extract_graphql_details,
|
|
25
|
+
pagination: detect_pagination
|
|
24
26
|
}
|
|
25
27
|
rescue => e
|
|
26
28
|
{ error: e.message }
|
|
@@ -85,7 +87,8 @@ module RailsAiContext
|
|
|
85
87
|
.map { |path| path.sub("#{root}/", "") }
|
|
86
88
|
.sort
|
|
87
89
|
.uniq
|
|
88
|
-
rescue
|
|
90
|
+
rescue => e
|
|
91
|
+
$stderr.puts "[rails-ai-context] detect_openapi_specs failed: #{e.message}" if ENV["DEBUG"]
|
|
89
92
|
[]
|
|
90
93
|
end
|
|
91
94
|
|
|
@@ -99,7 +102,8 @@ module RailsAiContext
|
|
|
99
102
|
end
|
|
100
103
|
|
|
101
104
|
{ file: "config/initializers/cors.rb", origins: origins }
|
|
102
|
-
rescue
|
|
105
|
+
rescue => e
|
|
106
|
+
$stderr.puts "[rails-ai-context] detect_cors_config failed: #{e.message}" if ENV["DEBUG"]
|
|
103
107
|
nil
|
|
104
108
|
end
|
|
105
109
|
|
|
@@ -111,10 +115,41 @@ module RailsAiContext
|
|
|
111
115
|
codegen_tools = %w[openapi-typescript @graphql-codegen/cli orval]
|
|
112
116
|
|
|
113
117
|
codegen_tools.select { |tool| content.include?(%("#{tool}")) }
|
|
114
|
-
rescue
|
|
118
|
+
rescue => e
|
|
119
|
+
$stderr.puts "[rails-ai-context] detect_api_client_generation failed: #{e.message}" if ENV["DEBUG"]
|
|
115
120
|
[]
|
|
116
121
|
end
|
|
117
122
|
|
|
123
|
+
def extract_graphql_details
|
|
124
|
+
graphql_dir = File.join(app.root, "app", "graphql")
|
|
125
|
+
return nil unless Dir.exist?(graphql_dir)
|
|
126
|
+
|
|
127
|
+
details = {}
|
|
128
|
+
details[:resolvers] = Dir.glob(File.join(graphql_dir, "**", "*resolver*")).map { |f| File.basename(f, ".rb").camelize }
|
|
129
|
+
details[:subscriptions] = Dir.glob(File.join(graphql_dir, "**", "subscriptions", "*.rb")).map { |f| File.basename(f, ".rb").camelize }
|
|
130
|
+
details[:dataloaders] = Dir.glob(File.join(graphql_dir, "**", "{loaders,dataloaders}", "*.rb")).map { |f| File.basename(f, ".rb").camelize }
|
|
131
|
+
details.reject { |_, v| v.empty? }
|
|
132
|
+
rescue => e
|
|
133
|
+
$stderr.puts "[rails-ai-context] extract_graphql_details failed: #{e.message}" if ENV["DEBUG"]
|
|
134
|
+
nil
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def detect_pagination
|
|
138
|
+
gemfile_lock = File.join(app.root, "Gemfile.lock")
|
|
139
|
+
return nil unless File.exist?(gemfile_lock)
|
|
140
|
+
content = File.read(gemfile_lock)
|
|
141
|
+
|
|
142
|
+
strategies = []
|
|
143
|
+
strategies << "pagy" if content.include?("pagy")
|
|
144
|
+
strategies << "kaminari" if content.include?("kaminari")
|
|
145
|
+
strategies << "will_paginate" if content.include?("will_paginate")
|
|
146
|
+
strategies << "cursor" if content.include?("graphql-pro") # cursor-based pagination
|
|
147
|
+
strategies.empty? ? nil : strategies
|
|
148
|
+
rescue => e
|
|
149
|
+
$stderr.puts "[rails-ai-context] detect_pagination failed: #{e.message}" if ENV["DEBUG"]
|
|
150
|
+
nil
|
|
151
|
+
end
|
|
152
|
+
|
|
118
153
|
def detect_rate_limiting
|
|
119
154
|
# Rack::Attack
|
|
120
155
|
init_path = File.join(root, "config/initializers/rack_attack.rb")
|
|
@@ -126,7 +161,8 @@ module RailsAiContext
|
|
|
126
161
|
Dir.glob(File.join(controllers_dir, "**/*.rb")).each do |path|
|
|
127
162
|
content = File.read(path)
|
|
128
163
|
return { rails_rate_limiting: true } if content.match?(/rate_limit\b/)
|
|
129
|
-
rescue
|
|
164
|
+
rescue => e
|
|
165
|
+
$stderr.puts "[rails-ai-context] detect_rate_limiting failed: #{e.message}" if ENV["DEBUG"]
|
|
130
166
|
next
|
|
131
167
|
end
|
|
132
168
|
end
|
|
@@ -42,7 +42,8 @@ module RailsAiContext
|
|
|
42
42
|
|
|
43
43
|
content = File.read(path)
|
|
44
44
|
content.scan(/pin\s+["']([^"']+)["']/).flatten.sort
|
|
45
|
-
rescue
|
|
45
|
+
rescue => e
|
|
46
|
+
$stderr.puts "[rails-ai-context] extract_importmap_pins failed: #{e.message}" if ENV["DEBUG"]
|
|
46
47
|
[]
|
|
47
48
|
end
|
|
48
49
|
|
|
@@ -50,14 +51,17 @@ module RailsAiContext
|
|
|
50
51
|
lock_content = read_gemfile_lock
|
|
51
52
|
return nil unless lock_content
|
|
52
53
|
|
|
53
|
-
return "tailwindcss" if lock_content.include?("tailwindcss-rails (")
|
|
54
|
+
return "tailwindcss" if lock_content.include?("tailwindcss-rails (") || package_json_has?("tailwindcss")
|
|
54
55
|
return "bootstrap" if lock_content.include?("bootstrap (") || package_json_has?("bootstrap")
|
|
55
56
|
return "bulma" if package_json_has?("bulma")
|
|
57
|
+
return "foundation" if package_json_has?("foundation-sites")
|
|
58
|
+
return "postcss" if package_json_has?("postcss") && !package_json_has?("tailwindcss")
|
|
56
59
|
nil
|
|
57
60
|
end
|
|
58
61
|
|
|
59
62
|
def detect_js_bundler
|
|
60
63
|
return "importmap" if File.exist?(File.join(root, "config/importmap.rb"))
|
|
64
|
+
return "bun" if File.exist?(File.join(root, "bun.lockb")) || File.exist?(File.join(root, "bunfig.toml"))
|
|
61
65
|
return "esbuild" if package_json_has?("esbuild")
|
|
62
66
|
return "webpack" if File.exist?(File.join(root, "config/webpack")) || package_json_has?("webpack")
|
|
63
67
|
return "vite" if Dir.glob(File.join(root, "vite.config.*")).any?
|
|
@@ -76,7 +80,8 @@ module RailsAiContext
|
|
|
76
80
|
def read_gemfile_lock
|
|
77
81
|
path = File.join(root, "Gemfile.lock")
|
|
78
82
|
File.exist?(path) ? File.read(path) : nil
|
|
79
|
-
rescue
|
|
83
|
+
rescue => e
|
|
84
|
+
$stderr.puts "[rails-ai-context] read_gemfile_lock failed: #{e.message}" if ENV["DEBUG"]
|
|
80
85
|
nil
|
|
81
86
|
end
|
|
82
87
|
|
|
@@ -84,7 +89,8 @@ module RailsAiContext
|
|
|
84
89
|
path = File.join(root, "package.json")
|
|
85
90
|
return false unless File.exist?(path)
|
|
86
91
|
File.read(path).include?("\"#{package}\"")
|
|
87
|
-
rescue
|
|
92
|
+
rescue => e
|
|
93
|
+
$stderr.puts "[rails-ai-context] package_json_has? failed: #{e.message}" if ENV["DEBUG"]
|
|
88
94
|
false
|
|
89
95
|
end
|
|
90
96
|
end
|