senren-ui 0.1.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.
Files changed (182) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +33 -0
  3. data/CONTRIBUTING.md +63 -0
  4. data/LICENSE +21 -0
  5. data/README.md +135 -0
  6. data/Rakefile +22 -0
  7. data/docs/visual_style.md +51 -0
  8. data/lib/generators/senren/component/component_generator.rb +62 -0
  9. data/lib/generators/senren/component/templates/component.html.erb.tt +3 -0
  10. data/lib/generators/senren/component/templates/component.rb.tt +13 -0
  11. data/lib/generators/senren/component/templates/component_test.rb.tt +16 -0
  12. data/lib/generators/senren/component/templates/controller.js.tt +23 -0
  13. data/lib/generators/senren/component/templates/system_test.rb.tt +7 -0
  14. data/lib/generators/senren/install/install_generator.rb +67 -0
  15. data/lib/generators/senren/install/templates/base_component.rb.tt +45 -0
  16. data/lib/generators/senren/install/templates/conventions.md.tt +66 -0
  17. data/lib/generators/senren/install/templates/installed_components.yml.tt +4 -0
  18. data/lib/generators/senren/install/templates/senren.css.tt +164 -0
  19. data/lib/senren/rails/component_copier.rb +111 -0
  20. data/lib/senren/rails/doctor.rb +86 -0
  21. data/lib/senren/rails/engine.rb +16 -0
  22. data/lib/senren/rails/host_paths.rb +36 -0
  23. data/lib/senren/rails/installer.rb +83 -0
  24. data/lib/senren/rails/llms_writer.rb +149 -0
  25. data/lib/senren/rails/registry.rb +161 -0
  26. data/lib/senren/rails/skill_writer.rb +166 -0
  27. data/lib/senren/rails/version.rb +7 -0
  28. data/lib/senren/rails.rb +39 -0
  29. data/lib/tasks/senren.rake +74 -0
  30. data/registry/components.yml +1053 -0
  31. data/registry/groups.yml +25 -0
  32. data/registry/recipes.yml +79 -0
  33. data/templates/components/accordion/accordion_component.html.erb +16 -0
  34. data/templates/components/accordion/accordion_component.rb +31 -0
  35. data/templates/components/activity_feed/activity_feed_component.html.erb +22 -0
  36. data/templates/components/activity_feed/activity_feed_component.rb +19 -0
  37. data/templates/components/alert/alert_component.html.erb +9 -0
  38. data/templates/components/alert/alert_component.rb +18 -0
  39. data/templates/components/alert_dialog/alert_dialog_component.html.erb +34 -0
  40. data/templates/components/alert_dialog/alert_dialog_component.rb +21 -0
  41. data/templates/components/api_key_field/api_key_field_component.html.erb +13 -0
  42. data/templates/components/api_key_field/api_key_field_component.rb +20 -0
  43. data/templates/components/app_shell/app_shell_component.html.erb +28 -0
  44. data/templates/components/app_shell/app_shell_component.rb +24 -0
  45. data/templates/components/aspect_ratio/aspect_ratio_component.html.erb +3 -0
  46. data/templates/components/aspect_ratio/aspect_ratio_component.rb +14 -0
  47. data/templates/components/avatar/avatar_component.html.erb +27 -0
  48. data/templates/components/avatar/avatar_component.rb +30 -0
  49. data/templates/components/badge/badge_component.html.erb +1 -0
  50. data/templates/components/badge/badge_component.rb +16 -0
  51. data/templates/components/billing_plan_card/billing_plan_card_component.html.erb +28 -0
  52. data/templates/components/billing_plan_card/billing_plan_card_component.rb +27 -0
  53. data/templates/components/breadcrumb/breadcrumb_component.html.erb +23 -0
  54. data/templates/components/breadcrumb/breadcrumb_component.rb +30 -0
  55. data/templates/components/bulk_action_bar/bulk_action_bar_component.html.erb +12 -0
  56. data/templates/components/bulk_action_bar/bulk_action_bar_component.rb +24 -0
  57. data/templates/components/button/button_component.html.erb +6 -0
  58. data/templates/components/button/button_component.rb +29 -0
  59. data/templates/components/calendar/calendar_component.html.erb +21 -0
  60. data/templates/components/calendar/calendar_component.rb +30 -0
  61. data/templates/components/card/card_component.html.erb +13 -0
  62. data/templates/components/card/card_component.rb +17 -0
  63. data/templates/components/carousel/carousel_component.html.erb +68 -0
  64. data/templates/components/carousel/carousel_component.rb +34 -0
  65. data/templates/components/checkbox/checkbox_component.html.erb +8 -0
  66. data/templates/components/checkbox/checkbox_component.rb +19 -0
  67. data/templates/components/checkbox_group/checkbox_group_component.html.erb +10 -0
  68. data/templates/components/checkbox_group/checkbox_group_component.rb +30 -0
  69. data/templates/components/clipboard/clipboard_component.html.erb +7 -0
  70. data/templates/components/clipboard/clipboard_component.rb +17 -0
  71. data/templates/components/codeblock/codeblock_component.html.erb +11 -0
  72. data/templates/components/codeblock/codeblock_component.rb +31 -0
  73. data/templates/components/collapsible/collapsible_component.html.erb +9 -0
  74. data/templates/components/collapsible/collapsible_component.rb +19 -0
  75. data/templates/components/combobox/combobox_component.html.erb +19 -0
  76. data/templates/components/combobox/combobox_component.rb +38 -0
  77. data/templates/components/command/command_component.html.erb +22 -0
  78. data/templates/components/command/command_component.rb +38 -0
  79. data/templates/components/context_menu/context_menu_component.html.erb +11 -0
  80. data/templates/components/context_menu/context_menu_component.rb +11 -0
  81. data/templates/components/data_table/data_table_component.html.erb +50 -0
  82. data/templates/components/data_table/data_table_component.rb +42 -0
  83. data/templates/components/date_picker/date_picker_component.html.erb +5 -0
  84. data/templates/components/date_picker/date_picker_component.rb +21 -0
  85. data/templates/components/dialog/dialog_component.html.erb +38 -0
  86. data/templates/components/dialog/dialog_component.rb +22 -0
  87. data/templates/components/dropdown_menu/dropdown_menu_component.html.erb +12 -0
  88. data/templates/components/dropdown_menu/dropdown_menu_component.rb +36 -0
  89. data/templates/components/empty_state/empty_state_component.html.erb +18 -0
  90. data/templates/components/empty_state/empty_state_component.rb +22 -0
  91. data/templates/components/filter_bar/filter_bar_component.html.erb +5 -0
  92. data/templates/components/filter_bar/filter_bar_component.rb +15 -0
  93. data/templates/components/form/form_component.html.erb +3 -0
  94. data/templates/components/form/form_component.rb +18 -0
  95. data/templates/components/hover_card/hover_card_component.html.erb +10 -0
  96. data/templates/components/hover_card/hover_card_component.rb +11 -0
  97. data/templates/components/input/input_component.html.erb +1 -0
  98. data/templates/components/input/input_component.rb +28 -0
  99. data/templates/components/invite_member_dialog/invite_member_dialog_component.html.erb +35 -0
  100. data/templates/components/invite_member_dialog/invite_member_dialog_component.rb +26 -0
  101. data/templates/components/label/label_component.html.erb +4 -0
  102. data/templates/components/label/label_component.rb +19 -0
  103. data/templates/components/link/link_component.html.erb +1 -0
  104. data/templates/components/link/link_component.rb +25 -0
  105. data/templates/components/masked_input/masked_input_component.html.erb +1 -0
  106. data/templates/components/masked_input/masked_input_component.rb +18 -0
  107. data/templates/components/native_select/native_select_component.html.erb +14 -0
  108. data/templates/components/native_select/native_select_component.rb +52 -0
  109. data/templates/components/page_header/page_header_component.html.erb +20 -0
  110. data/templates/components/page_header/page_header_component.rb +19 -0
  111. data/templates/components/pagination/pagination_component.html.erb +11 -0
  112. data/templates/components/pagination/pagination_component.rb +24 -0
  113. data/templates/components/popover/popover_component.html.erb +9 -0
  114. data/templates/components/popover/popover_component.rb +11 -0
  115. data/templates/components/progress/progress_component.html.erb +11 -0
  116. data/templates/components/progress/progress_component.rb +26 -0
  117. data/templates/components/radio_button/radio_button_component.html.erb +8 -0
  118. data/templates/components/radio_button/radio_button_component.rb +19 -0
  119. data/templates/components/rich_text_editor_lite/rich_text_editor_lite_component.html.erb +32 -0
  120. data/templates/components/rich_text_editor_lite/rich_text_editor_lite_component.rb +30 -0
  121. data/templates/components/search_input/search_input_component.html.erb +14 -0
  122. data/templates/components/search_input/search_input_component.rb +18 -0
  123. data/templates/components/select/select_component.html.erb +1 -0
  124. data/templates/components/select/select_component.rb +19 -0
  125. data/templates/components/separator/separator_component.html.erb +1 -0
  126. data/templates/components/separator/separator_component.rb +12 -0
  127. data/templates/components/settings_section/settings_section_component.html.erb +20 -0
  128. data/templates/components/settings_section/settings_section_component.rb +18 -0
  129. data/templates/components/sheet/sheet_component.html.erb +37 -0
  130. data/templates/components/sheet/sheet_component.rb +27 -0
  131. data/templates/components/shortcut_key/shortcut_key_component.html.erb +6 -0
  132. data/templates/components/shortcut_key/shortcut_key_component.rb +15 -0
  133. data/templates/components/sidebar/sidebar_component.html.erb +14 -0
  134. data/templates/components/sidebar/sidebar_component.rb +37 -0
  135. data/templates/components/skeleton/skeleton_component.html.erb +1 -0
  136. data/templates/components/skeleton/skeleton_component.rb +13 -0
  137. data/templates/components/stat_card/stat_card_component.html.erb +20 -0
  138. data/templates/components/stat_card/stat_card_component.rb +31 -0
  139. data/templates/components/switch/switch_component.html.erb +11 -0
  140. data/templates/components/switch/switch_component.rb +19 -0
  141. data/templates/components/table/table_component.html.erb +26 -0
  142. data/templates/components/table/table_component.rb +35 -0
  143. data/templates/components/tabs/tabs_component.html.erb +18 -0
  144. data/templates/components/tabs/tabs_component.rb +35 -0
  145. data/templates/components/team_member_list/team_member_list_component.html.erb +22 -0
  146. data/templates/components/team_member_list/team_member_list_component.rb +26 -0
  147. data/templates/components/textarea/textarea_component.html.erb +1 -0
  148. data/templates/components/textarea/textarea_component.rb +23 -0
  149. data/templates/components/theme_toggle/theme_toggle_component.html.erb +4 -0
  150. data/templates/components/theme_toggle/theme_toggle_component.rb +15 -0
  151. data/templates/components/tooltip/tooltip_component.html.erb +9 -0
  152. data/templates/components/tooltip/tooltip_component.rb +16 -0
  153. data/templates/components/top_nav/top_nav_component.html.erb +21 -0
  154. data/templates/components/top_nav/top_nav_component.rb +44 -0
  155. data/templates/components/typography/typography_component.html.erb +1 -0
  156. data/templates/components/typography/typography_component.rb +24 -0
  157. data/templates/controllers/accordion_controller.js +27 -0
  158. data/templates/controllers/alert_dialog_controller.js +38 -0
  159. data/templates/controllers/api_key_field_controller.js +36 -0
  160. data/templates/controllers/calendar_controller.js +16 -0
  161. data/templates/controllers/carousel_controller.js +50 -0
  162. data/templates/controllers/clipboard_controller.js +17 -0
  163. data/templates/controllers/collapsible_controller.js +13 -0
  164. data/templates/controllers/combobox_controller.js +64 -0
  165. data/templates/controllers/command_controller.js +80 -0
  166. data/templates/controllers/context_menu_controller.js +36 -0
  167. data/templates/controllers/data_table_controller.js +34 -0
  168. data/templates/controllers/date_picker_controller.js +17 -0
  169. data/templates/controllers/dialog_controller.js +50 -0
  170. data/templates/controllers/dropdown_menu_controller.js +92 -0
  171. data/templates/controllers/hover_card_controller.js +17 -0
  172. data/templates/controllers/invite_member_dialog_controller.js +28 -0
  173. data/templates/controllers/masked_input_controller.js +30 -0
  174. data/templates/controllers/popover_controller.js +42 -0
  175. data/templates/controllers/rich_text_editor_lite_controller.js +443 -0
  176. data/templates/controllers/select_controller.js +10 -0
  177. data/templates/controllers/sheet_controller.js +34 -0
  178. data/templates/controllers/sidebar_controller.js +10 -0
  179. data/templates/controllers/tabs_controller.js +41 -0
  180. data/templates/controllers/theme_toggle_controller.js +24 -0
  181. data/templates/controllers/tooltip_controller.js +10 -0
  182. metadata +257 -0
@@ -0,0 +1,161 @@
1
+ require 'yaml'
2
+
3
+ module Senren
4
+ module Rails
5
+ # Loads, validates, and queries the Senren component registry.
6
+ #
7
+ # reg = Senren::Rails::Registry.load!
8
+ # reg.find("button") # => Component struct
9
+ # reg.dependencies("dialog") # => [<button>]
10
+ # reg.group("forms") # => [<form>, <input>, ...]
11
+ class Registry
12
+ include Enumerable
13
+
14
+ REQUIRED_KEYS = %w[category client can_have_client files depends_on pairs_with variants accessibility ai].freeze
15
+ VALID_CATEGORIES = %w[actions forms overlays navigation layout data saas rich].freeze
16
+
17
+ Component = Struct.new(
18
+ :name, :category, :client, :can_have_client, :controller, :stub,
19
+ :files, :depends_on, :pairs_with, :variants, :accessibility,
20
+ :use_for, :avoid,
21
+ keyword_init: true
22
+ ) do
23
+ def stub? = stub == true
24
+ def client? = client == true
25
+
26
+ def to_h_full
27
+ to_h.merge(stub: stub?, client: client?)
28
+ end
29
+ end
30
+
31
+ attr_reader :components, :groups, :recipes
32
+
33
+ def self.load!(
34
+ components_path: Senren::Rails.registry_path,
35
+ groups_path: Senren::Rails.groups_path,
36
+ recipes_path: Senren::Rails.recipes_path
37
+ )
38
+ new(
39
+ YAML.safe_load_file(components_path, aliases: false),
40
+ YAML.safe_load_file(groups_path, aliases: false),
41
+ YAML.safe_load_file(recipes_path, aliases: false)
42
+ ).tap(&:validate!)
43
+ end
44
+
45
+ def initialize(components_yaml, groups_yaml, recipes_yaml)
46
+ @components = parse_components(components_yaml)
47
+ @groups = (groups_yaml || {}).fetch('groups', [])
48
+ @recipes = (recipes_yaml || {}).fetch('recipes', {})
49
+ end
50
+
51
+ def find(name)
52
+ @components[name.to_s]
53
+ end
54
+
55
+ def fetch(name)
56
+ find(name) or raise ArgumentError, "Unknown component: #{name.inspect}. " \
57
+ "Known: #{@components.keys.sort.join(', ')}"
58
+ end
59
+
60
+ def all
61
+ @components.values
62
+ end
63
+
64
+ def each(&block)
65
+ all.each(&block)
66
+ end
67
+
68
+ def find_each(&block)
69
+ each(&block)
70
+ end
71
+
72
+ def names
73
+ @components.keys
74
+ end
75
+
76
+ def group(category_id)
77
+ @components.values.select { |c| c.category == category_id.to_s }
78
+ end
79
+
80
+ def recipe(id)
81
+ @recipes.fetch(id.to_s) { raise ArgumentError, "Unknown recipe: #{id}" }
82
+ end
83
+
84
+ # Returns the transitive dependency closure for one or more components,
85
+ # in install order (deps first), de-duplicated.
86
+ def dependencies(*requested)
87
+ requested = requested.flatten.map(&:to_s)
88
+ result = []
89
+ visiting = []
90
+
91
+ visit = lambda do |name|
92
+ return if result.include?(name)
93
+ raise "Circular dependency: #{(visiting + [name]).join(' -> ')}" if visiting.include?(name)
94
+
95
+ visiting << name
96
+ comp = fetch(name)
97
+ comp.depends_on.each { |dep| visit.call(dep) }
98
+ visiting.pop
99
+ result << name
100
+ end
101
+
102
+ requested.each { |name| visit.call(name) }
103
+ result
104
+ end
105
+
106
+ def validate!
107
+ errors = []
108
+
109
+ validate_components(errors)
110
+ validate_recipes(errors)
111
+
112
+ raise "Registry validation failed:\n - #{errors.join("\n - ")}" if errors.any?
113
+
114
+ self
115
+ end
116
+
117
+ private
118
+
119
+ def validate_components(errors)
120
+ @components.each do |name, comp|
121
+ errors << "#{name}: invalid category #{comp.category.inspect}" unless VALID_CATEGORIES.include?(comp.category)
122
+ comp.depends_on.each do |dep|
123
+ errors << "#{name}: depends_on unknown component #{dep.inspect}" unless @components.key?(dep)
124
+ end
125
+ errors << "#{name}: client=true but can_have_client=false" if comp.client && !comp.can_have_client
126
+ errors << "#{name}: client=true requires a controller identifier" if comp.client && comp.controller.nil?
127
+ end
128
+ end
129
+
130
+ def validate_recipes(errors)
131
+ @recipes.each do |id, recipe|
132
+ recipe.fetch('components', []).each do |component|
133
+ errors << "recipe #{id}: unknown component #{component.inspect}" unless @components.key?(component)
134
+ end
135
+ end
136
+ end
137
+
138
+ def parse_components(yaml)
139
+ raw = (yaml || {}).fetch('components', {})
140
+ raw.each_with_object({}) do |(name, data), hash|
141
+ ai = data.fetch('ai', {})
142
+ hash[name] = Component.new(
143
+ name: name,
144
+ category: data.fetch('category'),
145
+ client: data.fetch('client', false),
146
+ can_have_client: data.fetch('can_have_client', false),
147
+ controller: data['controller'],
148
+ stub: data.fetch('stub', false),
149
+ files: Array(data['files']),
150
+ depends_on: Array(data['depends_on']),
151
+ pairs_with: Array(data['pairs_with']),
152
+ variants: Array(data['variants']),
153
+ accessibility: Array(data['accessibility']),
154
+ use_for: Array(ai['use_for']),
155
+ avoid: Array(ai['avoid'])
156
+ ).freeze
157
+ end
158
+ end
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+
5
+ module Senren
6
+ module Rails
7
+ # Writes the centralized .senren/skill.md file for AI agents.
8
+ #
9
+ # The file contains a generated region delimited by HTML comments:
10
+ #
11
+ # <!-- senren:skill:start -->
12
+ # ...generated by SkillWriter...
13
+ # <!-- senren:skill:end -->
14
+ #
15
+ # Anything outside that region is preserved across regeneration.
16
+ class SkillWriter
17
+ START_MARKER = '<!-- senren:skill:start -->'
18
+ END_MARKER = '<!-- senren:skill:end -->'
19
+
20
+ GROUP_ORDER = %w[actions forms overlays navigation layout data saas rich].freeze
21
+
22
+ attr_reader :registry, :paths
23
+
24
+ def initialize(registry: Registry.load!, paths: HostPaths.new)
25
+ @registry = registry
26
+ @paths = paths
27
+ end
28
+
29
+ def sync!
30
+ installed = installed_names
31
+ body = render(installed)
32
+
33
+ paths.senren_dir.mkpath
34
+ existing = paths.skill_file.exist? ? paths.skill_file.read : default_outer_template
35
+
36
+ new_content = inject(existing, body)
37
+ paths.skill_file.write(new_content)
38
+ paths.skill_file
39
+ end
40
+
41
+ private
42
+
43
+ def installed_names
44
+ path = paths.installed_components
45
+ return [] unless path.exist?
46
+
47
+ ledger = YAML.safe_load_file(path) || {}
48
+ Array(ledger['installed']).filter_map { |e| e['name'] }.uniq
49
+ end
50
+
51
+ def render(installed_names)
52
+ if installed_names.empty?
53
+ return '_No Senren components installed yet. Run `bin/rails senren:add <name>...` to install components._'
54
+ end
55
+
56
+ lines = []
57
+ installed = installed_names.filter_map { |n| registry.find(n) }
58
+
59
+ registry.groups.each do |group|
60
+ gid = group['id']
61
+ comps = installed.select { |c| c.category == gid }
62
+ next if comps.empty?
63
+
64
+ lines << "## #{group['title']}"
65
+ lines << ''
66
+ lines << group['description'].to_s
67
+ lines << ''
68
+ comps.sort_by(&:name).each { |c| lines << render_component(c) }
69
+ end
70
+
71
+ lines.join("\n")
72
+ end
73
+
74
+ def render_component(comp)
75
+ out = []
76
+ out << "### Component: #{humanize(comp.name)}"
77
+ out << ''
78
+ out << "- **Category**: #{comp.category}"
79
+ out << "- **Stub**: #{comp.stub? ? 'yes (placeholder)' : 'no'}"
80
+ out << "- **Client behavior**: #{client_summary(comp)}"
81
+ out << "- **Depends on**: #{format_list(comp.depends_on)}"
82
+ out << "- **Pairs with**: #{format_list(comp.pairs_with)}"
83
+ out << "- **Variants**: #{format_list(comp.variants)}"
84
+ out << ''
85
+ out << '**Use for**:'
86
+ comp.use_for.each { |u| out << "- #{u}" }
87
+ out << ''
88
+ out << '**Avoid**:'
89
+ comp.avoid.each { |a| out << "- #{a}" }
90
+ out << ''
91
+ out << '**Accessibility**:'
92
+ comp.accessibility.each { |a| out << "- #{a}" }
93
+ out << ''
94
+ out << '**Rails usage**:'
95
+ out << ''
96
+ out << '```erb'
97
+ out << render_usage_example(comp)
98
+ out << '```'
99
+ out << ''
100
+ "#{out.join("\n")}\n"
101
+ end
102
+
103
+ def render_usage_example(comp)
104
+ ruby_class = "Senren::#{ruby_class_for(comp.name)}Component"
105
+ if comp.variants.any?
106
+ variant = comp.variants.first
107
+ %(<%= render #{ruby_class}.new(variant: :#{variant}) do %>\n ...\n<% end %>)
108
+ else
109
+ %(<%= render #{ruby_class}.new do %>\n ...\n<% end %>)
110
+ end
111
+ end
112
+
113
+ def client_summary(comp)
114
+ return 'none' unless comp.client?
115
+
116
+ path = "app/javascript/controllers/senren/#{comp.name}_controller.js"
117
+ "Stimulus `#{comp.controller}` (`#{path}`)"
118
+ end
119
+
120
+ def format_list(items)
121
+ return '_none_' if items.empty?
122
+
123
+ items.map { |i| "`#{i}`" }.join(', ')
124
+ end
125
+
126
+ def humanize(name)
127
+ name.split('_').map { |w| w[0].upcase + w[1..] }.join
128
+ end
129
+
130
+ def ruby_class_for(name)
131
+ humanize(name)
132
+ end
133
+
134
+ def inject(existing, generated_body)
135
+ if existing.include?(START_MARKER) && existing.include?(END_MARKER)
136
+ before = existing.split(START_MARKER, 2).first
137
+ after = existing.split(END_MARKER, 2).last
138
+ "#{before}#{START_MARKER}\n\n#{generated_body}\n\n#{END_MARKER}#{after}"
139
+ else
140
+ "#{existing.rstrip}\n\n#{START_MARKER}\n\n#{generated_body}\n\n#{END_MARKER}\n"
141
+ end
142
+ end
143
+
144
+ def default_outer_template
145
+ <<~MD
146
+ # Senren Skill File
147
+
148
+ This file is the central guide for AI coding agents working in this app.
149
+
150
+ ## Hard Rules
151
+
152
+ - Use Senren components before writing custom HTML.
153
+ - Use ViewComponent for reusable UI; Turbo for server state; Stimulus for local behavior only.
154
+ - Do not introduce React, Vue, Alpine, or any external state framework.
155
+ - Do not hard-code colors; use semantic Tailwind tokens like `bg-background`, `text-foreground`, `bg-primary`.
156
+
157
+ ## Installed Components
158
+
159
+ The block between the markers below is generated by `bin/rails senren:skill:sync`.
160
+ Do not edit the generated region by hand. You may add notes outside the markers.
161
+
162
+ MD
163
+ end
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Senren
4
+ module Rails
5
+ VERSION = '0.1.0'
6
+ end
7
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'senren/rails/version'
4
+
5
+ module Senren
6
+ module Rails
7
+ GEM_ROOT = File.expand_path('../..', __dir__).freeze
8
+
9
+ autoload :Registry, 'senren/rails/registry'
10
+ autoload :ComponentCopier, 'senren/rails/component_copier'
11
+ autoload :SkillWriter, 'senren/rails/skill_writer'
12
+ autoload :LlmsWriter, 'senren/rails/llms_writer'
13
+ autoload :Installer, 'senren/rails/installer'
14
+ autoload :Doctor, 'senren/rails/doctor'
15
+ autoload :HostPaths, 'senren/rails/host_paths'
16
+
17
+ def self.gem_root
18
+ GEM_ROOT
19
+ end
20
+
21
+ def self.registry_path
22
+ File.join(GEM_ROOT, 'registry', 'components.yml')
23
+ end
24
+
25
+ def self.groups_path
26
+ File.join(GEM_ROOT, 'registry', 'groups.yml')
27
+ end
28
+
29
+ def self.recipes_path
30
+ File.join(GEM_ROOT, 'registry', 'recipes.yml')
31
+ end
32
+
33
+ def self.templates_root
34
+ File.join(GEM_ROOT, 'templates')
35
+ end
36
+ end
37
+ end
38
+
39
+ require 'senren/rails/engine' if defined?(Rails::Engine)
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'senren/rails'
4
+
5
+ namespace :senren do
6
+ desc 'Install one or more Senren components: rake senren:add[button,dialog] or bin/rails senren:add button dialog'
7
+ task :add, [:names] => :environment do |_t, args|
8
+ names = parse_names(args)
9
+ options = parse_options
10
+ abort 'Usage: bin/rails senren:add NAME [NAME...] [--client | --no-client]' if names.empty?
11
+
12
+ paths = Senren::Rails::HostPaths.new
13
+ registry = Senren::Rails::Registry.load!
14
+ copier = Senren::Rails::ComponentCopier.new(registry: registry, paths: paths)
15
+
16
+ installed = copier.install(names, client_override: options[:client_override], force: options[:force])
17
+
18
+ Senren::Rails::SkillWriter.new(registry: registry, paths: paths).sync!
19
+ Senren::Rails::LlmsWriter.new(registry: registry, paths: paths).generate!
20
+
21
+ puts "Installed: #{installed.join(', ')}"
22
+ end
23
+
24
+ namespace :skill do
25
+ desc 'Rebuild .senren/skill.md from the registry and installed_components ledger.'
26
+ task sync: :environment do
27
+ paths = Senren::Rails::HostPaths.new
28
+ registry = Senren::Rails::Registry.load!
29
+ file = Senren::Rails::SkillWriter.new(registry: registry, paths: paths).sync!
30
+ puts "Wrote #{file}"
31
+ end
32
+ end
33
+
34
+ namespace :llms do
35
+ desc 'Regenerate public/llms.txt and public/llms-full.txt.'
36
+ task generate: :environment do
37
+ paths = Senren::Rails::HostPaths.new
38
+ registry = Senren::Rails::Registry.load!
39
+ files = Senren::Rails::LlmsWriter.new(registry: registry, paths: paths).generate!
40
+ files.each { |f| puts "Wrote #{f}" }
41
+ end
42
+ end
43
+
44
+ desc "Run health checks against the host app's Senren installation."
45
+ task doctor: :environment do
46
+ ok = Senren::Rails::Doctor.new(paths: Senren::Rails::HostPaths.new).run!
47
+ exit(1) unless ok
48
+ end
49
+ end
50
+
51
+ # Helpers ---------------------------------------------------------------
52
+
53
+ def parse_names(args)
54
+ raw = []
55
+ raw.concat(args.extras)
56
+ raw << args[:names] if args[:names]
57
+ raw.concat(ARGV.drop_while { |a| !a.start_with?('senren:') }.drop(1))
58
+ raw
59
+ .flatten
60
+ .flat_map { |s| s.to_s.split(/[,\s]+/) }
61
+ .reject { |s| s.empty? || s.start_with?('-') }
62
+ .uniq
63
+ end
64
+
65
+ def parse_options
66
+ client_override =
67
+ if ARGV.include?('--client')
68
+ true
69
+ elsif ARGV.include?('--no-client')
70
+ false
71
+ end
72
+
73
+ { client_override: client_override, force: ARGV.include?('--force') }
74
+ end