rails-ai-context 0.11.0 → 0.12.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/CLAUDE.md +4 -4
- data/CONTRIBUTING.md +2 -2
- data/README.md +9 -8
- data/demo_script.sh +1 -1
- data/docs/GUIDE.md +6 -6
- data/lib/rails_ai_context/configuration.rb +2 -2
- data/lib/rails_ai_context/introspector.rb +1 -0
- data/lib/rails_ai_context/introspectors/design_token_introspector.rb +187 -0
- data/lib/rails_ai_context/introspectors/view_template_introspector.rb +245 -40
- data/lib/rails_ai_context/serializers/claude_rules_serializer.rb +75 -35
- data/lib/rails_ai_context/serializers/claude_serializer.rb +24 -8
- data/lib/rails_ai_context/serializers/copilot_instructions_serializer.rb +6 -11
- data/lib/rails_ai_context/serializers/copilot_serializer.rb +3 -5
- data/lib/rails_ai_context/serializers/cursor_rules_serializer.rb +7 -11
- data/lib/rails_ai_context/serializers/opencode_serializer.rb +12 -9
- data/lib/rails_ai_context/serializers/rules_serializer.rb +3 -5
- data/lib/rails_ai_context/serializers/windsurf_rules_serializer.rb +6 -8
- data/lib/rails_ai_context/serializers/windsurf_serializer.rb +3 -3
- data/lib/rails_ai_context/server.rb +2 -1
- data/lib/rails_ai_context/tools/get_controllers.rb +22 -10
- data/lib/rails_ai_context/tools/get_edit_context.rb +123 -0
- data/lib/rails_ai_context/tools/get_model_details.rb +51 -0
- data/lib/rails_ai_context/tools/get_stimulus.rb +3 -3
- data/lib/rails_ai_context/tools/get_test_info.rb +11 -0
- data/lib/rails_ai_context/tools/get_view.rb +8 -4
- data/lib/rails_ai_context/tools/search_code.rb +1 -0
- data/lib/rails_ai_context/version.rb +1 -1
- data/server.json +3 -3
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c2116ee4ebdfbdfeeef7e91c6a4ebd9d618a84ed182cdc1d012b90c362b2a6cf
|
|
4
|
+
data.tar.gz: e1a084f0f348a1c549c123aa305e75c51eb45632c1c46dec396c1e6f36b3dc4e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2d919dc0670b50c31496866b354f215413828af302cd4d63089577d64a3253f1e5150a326ef29de16d32a29a30b3ada440a16ca1500284109e92aceb4904014b
|
|
7
|
+
data.tar.gz: f12682e40ce7733d1dd068fd47055ba74b40e474735dea7633ae38ae6b403d8027cfae89eb5ab3eab4e3d44f9acc10253148b467267305a22bd2e482f39b177d
|
data/CLAUDE.md
CHANGED
|
@@ -8,8 +8,8 @@ structure to AI assistants via the Model Context Protocol (MCP).
|
|
|
8
8
|
- `lib/rails_ai_context.rb` — Main entry point, public API (Zeitwerk autoloaded)
|
|
9
9
|
- `lib/rails_ai_context/configuration.rb` — User-facing config with presets (:standard, :full)
|
|
10
10
|
- `lib/rails_ai_context/introspector.rb` — Orchestrates sub-introspectors
|
|
11
|
-
- `lib/rails_ai_context/introspectors/` —
|
|
12
|
-
- `lib/rails_ai_context/tools/` —
|
|
11
|
+
- `lib/rails_ai_context/introspectors/` — 29 introspectors (schema, models, routes, jobs, gems, conventions, stimulus, database_stats, controllers, views, view_templates, design_tokens, turbo, i18n, config, active_storage, action_text, auth, api, tests, rake_tasks, assets, devops, action_mailbox, migrations, seeds, middleware, engines, multi_database)
|
|
12
|
+
- `lib/rails_ai_context/tools/` — 12 MCP tools using the official mcp SDK
|
|
13
13
|
- `lib/rails_ai_context/serializers/` — Output formatters (claude, claude_rules, opencode, cursor_rules, windsurf, windsurf_rules, copilot, copilot_instructions, rules, markdown, JSON)
|
|
14
14
|
- `lib/rails_ai_context/resources.rb` — MCP resources (static data AI clients read directly)
|
|
15
15
|
- `lib/rails_ai_context/server.rb` — MCP server configuration (stdio + HTTP transports)
|
|
@@ -32,7 +32,7 @@ structure to AI assistants via the Model Context Protocol (MCP).
|
|
|
32
32
|
6. **Diff-aware** — context regeneration skips unchanged files
|
|
33
33
|
7. **Per-assistant serializers** — each AI tool gets tailored output format
|
|
34
34
|
8. **Zeitwerk autoloading** — files loaded on-demand, not all upfront
|
|
35
|
-
9. **Introspector presets** — `:standard` (
|
|
35
|
+
9. **Introspector presets** — `:standard` (12 core) default, `:full` (28) for power users
|
|
36
36
|
10. **MCP auto-discovery** — `.mcp.json` generated by install generator
|
|
37
37
|
11. **Compact by default** — context files ≤150 lines, MCP tools use `detail` parameter (summary/standard/full)
|
|
38
38
|
12. **Per-tool split rules** — `.claude/rules/`, `.cursor/rules/`, `.windsurf/rules/`, `.github/instructions/`
|
|
@@ -42,7 +42,7 @@ structure to AI assistants via the Model Context Protocol (MCP).
|
|
|
42
42
|
## Testing
|
|
43
43
|
|
|
44
44
|
```bash
|
|
45
|
-
bundle exec rspec # Run specs (
|
|
45
|
+
bundle exec rspec # Run specs (481 examples)
|
|
46
46
|
bundle exec rubocop # Lint
|
|
47
47
|
```
|
|
48
48
|
|
data/CONTRIBUTING.md
CHANGED
|
@@ -18,8 +18,8 @@ The test suite uses [Combustion](https://github.com/pat/combustion) to boot a mi
|
|
|
18
18
|
|
|
19
19
|
```
|
|
20
20
|
lib/rails_ai_context/
|
|
21
|
-
├── introspectors/ #
|
|
22
|
-
├── tools/ #
|
|
21
|
+
├── introspectors/ # 29 introspectors (schema, models, routes, etc.)
|
|
22
|
+
├── tools/ # 12 MCP tools with detail levels and pagination
|
|
23
23
|
├── serializers/ # Per-assistant formatters (claude, opencode, cursor, windsurf, copilot, JSON)
|
|
24
24
|
├── server.rb # MCP server setup (stdio + HTTP)
|
|
25
25
|
├── live_reload.rb # MCP live reload (file watcher + cache invalidation)
|
data/README.md
CHANGED
|
@@ -27,7 +27,7 @@ Same task — *"Add status and date range filters to the Cooks index page"* —
|
|
|
27
27
|
|
|
28
28
|
| Setup | Tokens | Saved | What it knows |
|
|
29
29
|
|-------|--------|-------|---------------|
|
|
30
|
-
| **rails-ai-context (full)** | **28,834** | **37%** |
|
|
30
|
+
| **rails-ai-context (full)** | **28,834** | **37%** | 12 MCP tools + generated docs + rules |
|
|
31
31
|
| rails-ai-context CLAUDE.md only | 33,106 | 27% | Generated docs + rules, no MCP tools |
|
|
32
32
|
| Normal Claude `/init` | 40,700 | 11% | Generic CLAUDE.md only |
|
|
33
33
|
| No rails-ai-context at all | 45,477 | baseline | Nothing — discovers everything from scratch |
|
|
@@ -95,9 +95,9 @@ The install generator creates `.mcp.json` for auto-discovery — Claude Code and
|
|
|
95
95
|
|
|
96
96
|
---
|
|
97
97
|
|
|
98
|
-
##
|
|
98
|
+
## 12 Live MCP Tools
|
|
99
99
|
|
|
100
|
-
The gem exposes **
|
|
100
|
+
The gem exposes **12 read-only tools** via MCP that AI clients call on-demand:
|
|
101
101
|
|
|
102
102
|
| Tool | What it returns |
|
|
103
103
|
|------|----------------|
|
|
@@ -112,6 +112,7 @@ The gem exposes **11 read-only tools** via MCP that AI clients call on-demand:
|
|
|
112
112
|
| `rails_search_code` | Ripgrep-powered regex search across the codebase |
|
|
113
113
|
| `rails_get_view` | View templates, partials, Stimulus references |
|
|
114
114
|
| `rails_get_stimulus` | Stimulus controllers — targets, values, actions, outlets |
|
|
115
|
+
| `rails_get_edit_context` | Surgical edit helper — returns code around a match with line numbers |
|
|
115
116
|
|
|
116
117
|
### Smart Detail Levels
|
|
117
118
|
|
|
@@ -201,7 +202,7 @@ Root files (CLAUDE.md, AGENTS.md, etc.) use **section markers** — your custom
|
|
|
201
202
|
| **DevOps** | Puma, Procfile, Docker, deployment tools, asset pipeline |
|
|
202
203
|
| **Architecture** | Service objects, STI, polymorphism, state machines, multi-tenancy, engines |
|
|
203
204
|
|
|
204
|
-
|
|
205
|
+
29 introspectors total. The `:standard` preset runs 12 core ones by default; use `:full` for 28 (`database_stats` is opt-in, PostgreSQL only).
|
|
205
206
|
|
|
206
207
|
---
|
|
207
208
|
|
|
@@ -255,7 +256,7 @@ end
|
|
|
255
256
|
```ruby
|
|
256
257
|
# config/initializers/rails_ai_context.rb
|
|
257
258
|
RailsAiContext.configure do |config|
|
|
258
|
-
# Presets: :standard (
|
|
259
|
+
# Presets: :standard (12 introspectors, default) or :full (all 28)
|
|
259
260
|
config.preset = :standard
|
|
260
261
|
|
|
261
262
|
# Cherry-pick on top of a preset
|
|
@@ -289,7 +290,7 @@ end
|
|
|
289
290
|
| Option | Default | Description |
|
|
290
291
|
|--------|---------|-------------|
|
|
291
292
|
| `preset` | `:standard` | Introspector preset (`:standard` or `:full`) |
|
|
292
|
-
| `introspectors` |
|
|
293
|
+
| `introspectors` | 12 core | Array of introspector symbols |
|
|
293
294
|
| `context_mode` | `:compact` | `:compact` (≤150 lines) or `:full` (dump everything) |
|
|
294
295
|
| `claude_max_lines` | `150` | Max lines for CLAUDE.md in compact mode |
|
|
295
296
|
| `max_tool_response_chars` | `120_000` | Safety cap for MCP tool responses |
|
|
@@ -350,7 +351,7 @@ Works with every Rails architecture — auto-detects what's relevant:
|
|
|
350
351
|
|
|
351
352
|
| Setup | Coverage | Notes |
|
|
352
353
|
|-------|----------|-------|
|
|
353
|
-
| Rails full-stack (ERB + Hotwire) |
|
|
354
|
+
| Rails full-stack (ERB + Hotwire) | 29/29 | All introspectors relevant |
|
|
354
355
|
| Rails + Inertia.js (React/Vue) | ~22/27 | Views/Turbo partially useful, backend fully covered |
|
|
355
356
|
| Rails API + React/Next.js SPA | ~20/27 | Schema, models, routes, API, auth, jobs — all covered |
|
|
356
357
|
| Rails API + mobile app | ~20/27 | Same as SPA — backend introspection is identical |
|
|
@@ -389,7 +390,7 @@ The gem parses `db/schema.rb` as text when no database is connected. Works in CI
|
|
|
389
390
|
```bash
|
|
390
391
|
git clone https://github.com/crisnahine/rails-ai-context.git
|
|
391
392
|
cd rails-ai-context && bundle install
|
|
392
|
-
bundle exec rspec #
|
|
393
|
+
bundle exec rspec # 481 examples
|
|
393
394
|
bundle exec rubocop # Lint
|
|
394
395
|
```
|
|
395
396
|
|
data/demo_script.sh
CHANGED
data/docs/GUIDE.md
CHANGED
|
@@ -246,7 +246,7 @@ rails ai:context:claude # Use this instead (no quoting needed)
|
|
|
246
246
|
|
|
247
247
|
## MCP Tools — Full Reference
|
|
248
248
|
|
|
249
|
-
All
|
|
249
|
+
All 12 tools are **read-only** and **idempotent** — they never modify your application or database.
|
|
250
250
|
|
|
251
251
|
### rails_get_schema
|
|
252
252
|
|
|
@@ -568,7 +568,7 @@ RailsAiContext.configure do |config|
|
|
|
568
568
|
end
|
|
569
569
|
```
|
|
570
570
|
|
|
571
|
-
Both transports are **read-only** — they expose the same
|
|
571
|
+
Both transports are **read-only** — they expose the same 12 tools and never modify your app.
|
|
572
572
|
|
|
573
573
|
---
|
|
574
574
|
|
|
@@ -579,7 +579,7 @@ Both transports are **read-only** — they expose the same 11 tools and never mo
|
|
|
579
579
|
RailsAiContext.configure do |config|
|
|
580
580
|
# --- Introspectors ---
|
|
581
581
|
|
|
582
|
-
# Presets: :standard (
|
|
582
|
+
# Presets: :standard (12 core, default) or :full (all 28)
|
|
583
583
|
config.preset = :standard
|
|
584
584
|
|
|
585
585
|
# Cherry-pick on top of a preset
|
|
@@ -636,7 +636,7 @@ end
|
|
|
636
636
|
| Option | Type | Default | Description |
|
|
637
637
|
|--------|------|---------|-------------|
|
|
638
638
|
| `preset` | Symbol | `:standard` | Introspector preset (`:standard` or `:full`) |
|
|
639
|
-
| `introspectors` | Array |
|
|
639
|
+
| `introspectors` | Array | 12 core symbols | Which introspectors to run |
|
|
640
640
|
| `context_mode` | Symbol | `:compact` | `:compact` or `:full` |
|
|
641
641
|
| `claude_max_lines` | Integer | `150` | Max lines for CLAUDE.md in compact mode |
|
|
642
642
|
| `max_tool_response_chars` | Integer | `120_000` | Safety cap for MCP tool responses |
|
|
@@ -689,7 +689,7 @@ These run by default. Fast and cover core Rails structure.
|
|
|
689
689
|
| `tests` | Test framework (rspec/minitest), factories/fixtures with locations and counts, system tests, CI config files, coverage tool, test helpers, VCR cassettes. |
|
|
690
690
|
| `migrations` | Total count, schema version, pending migrations, recent migration history with detected actions (create_table, add_column, etc.), migration statistics. |
|
|
691
691
|
|
|
692
|
-
### Full preset (
|
|
692
|
+
### Full preset (29 introspectors)
|
|
693
693
|
|
|
694
694
|
Includes all standard introspectors plus:
|
|
695
695
|
|
|
@@ -812,7 +812,7 @@ OpenCode uses **per-directory lazy-loading**: when the agent reads a file, it wa
|
|
|
812
812
|
|
|
813
813
|
| Setup | Coverage | Notes |
|
|
814
814
|
|-------|----------|-------|
|
|
815
|
-
| Rails full-stack (ERB + Hotwire) |
|
|
815
|
+
| Rails full-stack (ERB + Hotwire) | 29/29 | All introspectors relevant |
|
|
816
816
|
| Rails + Inertia.js (React/Vue) | ~22/27 | Views/Turbo partially useful, backend fully covered |
|
|
817
817
|
| Rails API + React/Next.js SPA | ~20/27 | Schema, models, routes, API, auth, jobs — all covered |
|
|
818
818
|
| Rails API + mobile app | ~20/27 | Same as SPA — backend introspection is identical |
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
module RailsAiContext
|
|
4
4
|
class Configuration
|
|
5
5
|
PRESETS = {
|
|
6
|
-
standard: %i[schema models routes jobs gems conventions controllers tests migrations stimulus view_templates],
|
|
7
|
-
full: %i[schema models routes jobs gems conventions stimulus controllers views view_templates turbo
|
|
6
|
+
standard: %i[schema models routes jobs gems conventions controllers tests migrations stimulus view_templates design_tokens],
|
|
7
|
+
full: %i[schema models routes jobs gems conventions stimulus controllers views view_templates design_tokens turbo
|
|
8
8
|
i18n config active_storage action_text auth api tests rake_tasks assets
|
|
9
9
|
devops action_mailbox migrations seeds middleware engines multi_database]
|
|
10
10
|
}.freeze
|
|
@@ -58,6 +58,7 @@ module RailsAiContext
|
|
|
58
58
|
when :controllers then Introspectors::ControllerIntrospector.new(app)
|
|
59
59
|
when :views then Introspectors::ViewIntrospector.new(app)
|
|
60
60
|
when :view_templates then Introspectors::ViewTemplateIntrospector.new(app)
|
|
61
|
+
when :design_tokens then Introspectors::DesignTokenIntrospector.new(app)
|
|
61
62
|
when :turbo then Introspectors::TurboIntrospector.new(app)
|
|
62
63
|
when :i18n then Introspectors::I18nIntrospector.new(app)
|
|
63
64
|
when :config then Introspectors::ConfigIntrospector.new(app)
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAiContext
|
|
4
|
+
module Introspectors
|
|
5
|
+
# Extracts design tokens from CSS/SCSS files across ALL Rails CSS setups:
|
|
6
|
+
# - Tailwind v4 @theme blocks
|
|
7
|
+
# - Tailwind v4 built CSS :root variables
|
|
8
|
+
# - Tailwind v3 tailwind.config.js (simple key-values only)
|
|
9
|
+
# - Bootstrap/Sass $variable definitions
|
|
10
|
+
# - Plain CSS :root custom properties
|
|
11
|
+
# - Webpacker-era stylesheets
|
|
12
|
+
# - ViewComponent sidecar CSS
|
|
13
|
+
#
|
|
14
|
+
# Returns a framework-agnostic hash of design tokens.
|
|
15
|
+
# No external dependencies — pure regex parsing.
|
|
16
|
+
class DesignTokenIntrospector
|
|
17
|
+
attr_reader :app
|
|
18
|
+
|
|
19
|
+
def initialize(app)
|
|
20
|
+
@app = app
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def call
|
|
24
|
+
root = app.root.to_s
|
|
25
|
+
tokens = {}
|
|
26
|
+
|
|
27
|
+
# Priority order: check each source, merge found tokens
|
|
28
|
+
extract_built_css_vars(root, tokens)
|
|
29
|
+
extract_tailwind_v4_theme(root, tokens)
|
|
30
|
+
extract_tailwind_v3_config(root, tokens)
|
|
31
|
+
extract_scss_variables(root, tokens)
|
|
32
|
+
extract_css_custom_properties(root, tokens)
|
|
33
|
+
extract_webpacker_styles(root, tokens)
|
|
34
|
+
extract_component_css(root, tokens)
|
|
35
|
+
|
|
36
|
+
return { skipped: true, reason: "No design tokens found" } if tokens.empty?
|
|
37
|
+
|
|
38
|
+
{
|
|
39
|
+
framework: detect_framework(root),
|
|
40
|
+
tokens: tokens
|
|
41
|
+
}
|
|
42
|
+
rescue => e
|
|
43
|
+
{ error: e.message }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def detect_framework(root)
|
|
49
|
+
gemfile = File.join(root, "Gemfile")
|
|
50
|
+
return "unknown" unless File.exist?(gemfile)
|
|
51
|
+
content = File.read(gemfile, encoding: "UTF-8", invalid: :replace, undef: :replace)
|
|
52
|
+
|
|
53
|
+
if content.include?("tailwindcss-rails")
|
|
54
|
+
"tailwind"
|
|
55
|
+
elsif content.include?("bootstrap")
|
|
56
|
+
"bootstrap"
|
|
57
|
+
elsif content.include?("dartsass-rails") || content.include?("sassc-rails") || content.include?("sass-rails")
|
|
58
|
+
"sass"
|
|
59
|
+
elsif content.include?("cssbundling-rails")
|
|
60
|
+
"cssbundling"
|
|
61
|
+
else
|
|
62
|
+
"plain_css"
|
|
63
|
+
end
|
|
64
|
+
rescue
|
|
65
|
+
"unknown"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# 1. Built CSS output (Tailwind v4, cssbundling-rails, dartsass-rails)
|
|
69
|
+
def extract_built_css_vars(root, tokens)
|
|
70
|
+
Dir.glob(File.join(root, "app", "assets", "builds", "*.css")).each do |path|
|
|
71
|
+
content = File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace) rescue next
|
|
72
|
+
extract_root_vars(content, tokens)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# 2. Tailwind v4 @theme blocks in source CSS
|
|
77
|
+
def extract_tailwind_v4_theme(root, tokens)
|
|
78
|
+
%w[app/assets/tailwind app/assets/stylesheets].each do |dir|
|
|
79
|
+
Dir.glob(File.join(root, dir, "**", "*.css")).each do |path|
|
|
80
|
+
content = File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace) rescue next
|
|
81
|
+
content.scan(/@theme\s*(?:inline)?\s*\{([^}]+)\}/m).each do |match|
|
|
82
|
+
match[0].scan(/--([a-zA-Z0-9-]+):\s*([^;]+);/).each do |name, value|
|
|
83
|
+
tokens["--#{name}"] = value.strip
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# 3. Tailwind v3 config (regex on JS — handles nested color palettes)
|
|
91
|
+
def extract_tailwind_v3_config(root, tokens) # rubocop:disable Metrics/MethodLength
|
|
92
|
+
path = File.join(root, "config", "tailwind.config.js")
|
|
93
|
+
path = File.join(root, "tailwind.config.js") unless File.exist?(path)
|
|
94
|
+
return unless File.exist?(path)
|
|
95
|
+
|
|
96
|
+
content = File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace) rescue return
|
|
97
|
+
|
|
98
|
+
# Extract ALL hex/rgb/hsl color values with their context
|
|
99
|
+
# Pattern: 'key': '#hex' or "key": "rgb(...)" anywhere in file
|
|
100
|
+
content.scan(/['"]([\w-]+)['"]\s*:\s*['"]([#][\da-fA-F]{3,8})['"]/).each do |name, value|
|
|
101
|
+
tokens["tw3-#{name}"] = value
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Extract color shades: number keys with hex values (inside palette objects)
|
|
105
|
+
content.scan(/['"]?(\d{2,3})['"]?\s*:\s*['"]([#][\da-fA-F]{3,8})['"]/).each do |shade, value|
|
|
106
|
+
tokens["tw3-shade-#{shade}"] = value
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Extract named color strings: surface: '#ffffff'
|
|
110
|
+
content.scan(/(\w+)\s*:\s*['"]([#][\da-fA-F]{3,8})['"]/).each do |name, value|
|
|
111
|
+
next if name.match?(/\A\d/)
|
|
112
|
+
tokens["tw3-#{name}"] = value
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Extract fontFamily arrays
|
|
116
|
+
content.scan(/(\w+)\s*:\s*\[['"]([^'"]+)['"]/).each do |name, font|
|
|
117
|
+
tokens["tw3-font-#{name}"] = font if name.match?(/font|sans|serif|mono|display|heading/)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# 4. Bootstrap/Sass variable definitions
|
|
122
|
+
def extract_scss_variables(root, tokens)
|
|
123
|
+
%w[app/assets/stylesheets app/assets/stylesheets/config].each do |dir|
|
|
124
|
+
Dir.glob(File.join(root, dir, "**", "*.{scss,sass}")).each do |path|
|
|
125
|
+
content = File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace) rescue next
|
|
126
|
+
content.scan(/^\$([a-zA-Z][\w-]*)\s*:\s*([^;!]+)/).each do |name, value|
|
|
127
|
+
value = value.strip
|
|
128
|
+
# Skip computed values (references to other variables, functions)
|
|
129
|
+
next if value.match?(/\$\w|lighten|darken|mix|adjust|scale|rgba\(\$/)
|
|
130
|
+
tokens["$#{name}"] = value
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# 5. CSS custom properties in stylesheet files
|
|
137
|
+
def extract_css_custom_properties(root, tokens)
|
|
138
|
+
%w[app/assets/stylesheets].each do |dir|
|
|
139
|
+
Dir.glob(File.join(root, dir, "**", "*.css")).each do |path|
|
|
140
|
+
content = File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace) rescue next
|
|
141
|
+
extract_root_vars(content, tokens)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# 6. Webpacker-era stylesheets (Rails 6)
|
|
147
|
+
def extract_webpacker_styles(root, tokens)
|
|
148
|
+
%w[app/javascript/stylesheets app/javascript/css].each do |dir|
|
|
149
|
+
full_dir = File.join(root, dir)
|
|
150
|
+
next unless Dir.exist?(full_dir)
|
|
151
|
+
|
|
152
|
+
Dir.glob(File.join(full_dir, "**", "*.{scss,sass,css}")).each do |path|
|
|
153
|
+
content = File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace) rescue next
|
|
154
|
+
# Sass variables
|
|
155
|
+
content.scan(/^\$([a-zA-Z][\w-]*)\s*:\s*([^;!]+)/).each do |name, value|
|
|
156
|
+
value = value.strip
|
|
157
|
+
next if value.match?(/\$\w|lighten|darken|mix/)
|
|
158
|
+
tokens["$#{name}"] = value
|
|
159
|
+
end
|
|
160
|
+
# CSS custom properties
|
|
161
|
+
extract_root_vars(content, tokens)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# 7. ViewComponent sidecar CSS
|
|
167
|
+
def extract_component_css(root, tokens)
|
|
168
|
+
dir = File.join(root, "app", "components")
|
|
169
|
+
return unless Dir.exist?(dir)
|
|
170
|
+
|
|
171
|
+
Dir.glob(File.join(dir, "**", "*.css")).each do |path|
|
|
172
|
+
content = File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace) rescue next
|
|
173
|
+
extract_root_vars(content, tokens)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Helper: extract :root { --var: value } from CSS content
|
|
178
|
+
def extract_root_vars(content, tokens)
|
|
179
|
+
content.scan(/:root\s*(?:,\s*:host)?\s*\{([^}]+)\}/m).each do |match|
|
|
180
|
+
match[0].scan(/--([a-zA-Z0-9-]+):\s*([^;]+);/).each do |name, value|
|
|
181
|
+
tokens["--#{name}"] = value.strip
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|