senren-ui 0.1.4 → 0.1.6

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +49 -2
  3. data/CONTRIBUTING.md +41 -8
  4. data/README.md +73 -11
  5. data/docs/components.md +222 -0
  6. data/docs/performance_testing.md +34 -0
  7. data/lib/commands/senren/add/add_command.rb +35 -0
  8. data/lib/generators/senren/install/install_generator.rb +4 -7
  9. data/lib/generators/senren/install/templates/base_component.rb.tt +39 -6
  10. data/lib/generators/senren/install/templates/conventions.md.tt +22 -8
  11. data/lib/senren/rails/agent_rules_writer.rb +175 -0
  12. data/lib/senren/rails/component_copier.rb +75 -6
  13. data/lib/senren/rails/component_installer.rb +47 -0
  14. data/lib/senren/rails/doctor.rb +26 -13
  15. data/lib/senren/rails/host_paths.rb +12 -3
  16. data/lib/senren/rails/installer.rb +4 -2
  17. data/lib/senren/rails/llms_writer.rb +5 -132
  18. data/lib/senren/rails/registry.rb +63 -31
  19. data/lib/senren/rails/skill_writer.rb +1 -1
  20. data/lib/senren/rails/version.rb +1 -1
  21. data/lib/senren/rails.rb +2 -0
  22. data/lib/tasks/senren.rake +26 -21
  23. data/templates/components/billing_plan_card/billing_plan_card_component.html.erb +1 -1
  24. data/templates/components/breadcrumb/breadcrumb_component.rb +2 -2
  25. data/templates/components/button/button_component.html.erb +1 -1
  26. data/templates/components/carousel/carousel_component.rb +1 -1
  27. data/templates/components/command/command_component.rb +1 -1
  28. data/templates/components/dropdown_menu/dropdown_menu_component.rb +10 -7
  29. data/templates/components/form/form_component.html.erb +8 -1
  30. data/templates/components/form/form_component.rb +3 -1
  31. data/templates/components/input/input_component.html.erb +1 -1
  32. data/templates/components/input/input_component.rb +19 -0
  33. data/templates/components/label/label_component.html.erb +1 -2
  34. data/templates/components/label/label_component.rb +12 -2
  35. data/templates/components/link/link_component.html.erb +1 -1
  36. data/templates/components/native_select/native_select_component.html.erb +19 -5
  37. data/templates/components/native_select/native_select_component.rb +17 -5
  38. data/templates/components/pagination/pagination_component.rb +2 -1
  39. data/templates/components/sidebar/sidebar_component.rb +2 -2
  40. data/templates/components/switch/switch_component.html.erb +2 -2
  41. data/templates/components/top_nav/top_nav_component.rb +2 -2
  42. data/templates/controllers/rich_text_editor_lite_controller.js +12 -2
  43. metadata +23 -4
@@ -1,3 +1,6 @@
1
+ require 'uri'
2
+ require 'view_component'
3
+
1
4
  module Senren
2
5
  # Base class for every Senren ViewComponent.
3
6
  #
@@ -6,13 +9,16 @@ module Senren
6
9
  # - variant and size resolution against class-level constants
7
10
  # - small class merger (no external `tailwind_merge` dependency in v0.1)
8
11
  # - a default root-attribute helper that emits `data-senren-component="<name>"`
9
- class BaseComponent < ViewComponent::Base
10
- VARIANTS = { default: "" }.freeze
11
- SIZES = { md: "" }.freeze
12
+ class BaseComponent < ::ViewComponent::Base
13
+ VARIANTS = { default: '' }.freeze
14
+ SIZES = { md: '' }.freeze
15
+ SAFE_URL_PROTOCOLS = %w[http https mailto tel].freeze
16
+ SAFE_MEDIA_URL_PROTOCOLS = %w[http https].freeze
12
17
 
13
18
  attr_reader :class_name, :variant, :size, :html_attrs
14
19
 
15
20
  def initialize(variant: :default, size: :md, class_name: nil, **html_attrs)
21
+ super()
16
22
  @variant = resolve!(variant, self.class::VARIANTS, :variant)
17
23
  @size = resolve!(size, self.class::SIZES, :size)
18
24
  @class_name = class_name
@@ -22,24 +28,51 @@ module Senren
22
28
  # Compose final root attributes; subclasses pass their base classes.
23
29
  def root_attrs(*classes, **extra)
24
30
  data = (extra.delete(:data) || {}).merge(senren_component: senren_component_name)
25
- tag_class = merge_classes(classes, self.class::VARIANTS[@variant], self.class::SIZES[@size], @class_name, extra.delete(:class))
31
+ tag_class = merge_classes(
32
+ classes,
33
+ self.class::VARIANTS[@variant],
34
+ self.class::SIZES[@size],
35
+ @class_name,
36
+ extra.delete(:class)
37
+ )
26
38
  { class: tag_class, data: data, **html_attrs, **extra }
27
39
  end
28
40
 
29
41
  def senren_component_name
30
- self.class.name.to_s.sub(/^Senren::/, "").sub(/Component$/, "").gsub(/([a-z])([A-Z])/, '\1_\2').downcase
42
+ self.class.name.to_s.sub(/^Senren::/, '').sub(/Component$/, '').gsub(/([a-z])([A-Z])/, '\1_\2').downcase
31
43
  end
32
44
 
33
45
  private
34
46
 
47
+ def safe_url(value, fallback: '#', protocols: SAFE_URL_PROTOCOLS)
48
+ url = value.to_s.strip
49
+ return fallback if url.empty?
50
+ return url if url.start_with?('#')
51
+ return url if url.start_with?('/') && !url.start_with?('//')
52
+
53
+ uri = URI.parse(url)
54
+ return url if uri.scheme && Array(protocols).map(&:to_s).include?(uri.scheme.downcase)
55
+ return fallback if uri.host
56
+ return url unless uri.scheme
57
+
58
+ fallback
59
+ rescue URI::InvalidURIError
60
+ fallback
61
+ end
62
+
63
+ def safe_media_url(value, fallback: nil)
64
+ safe_url(value, fallback: fallback, protocols: SAFE_MEDIA_URL_PROTOCOLS)
65
+ end
66
+
35
67
  def resolve!(value, table, label)
36
68
  key = value.to_sym
37
69
  return key if table.key?(key)
70
+
38
71
  raise ArgumentError, "Unknown #{label}: #{value.inspect}. Allowed: #{table.keys.join(', ')}"
39
72
  end
40
73
 
41
74
  def merge_classes(*sources)
42
- sources.flatten.map { |s| s.to_s.strip }.reject(&:empty?).join(" ")
75
+ sources.flatten.map { |s| s.to_s.strip }.reject(&:empty?).join(' ')
43
76
  end
44
77
  end
45
78
  end
@@ -20,11 +20,11 @@ and obey it strictly.
20
20
  block to `.new`, **not** to `render`, producing an empty component.
21
21
  Use either of these forms instead:
22
22
  ```erb
23
- <%= render(Senren::ButtonComponent.new(variant: :primary)) { "Save" } %>
23
+ <%%= render(Senren::ButtonComponent.new(variant: :primary)) { "Save" } %>
24
24
 
25
- <%= render Senren::ButtonComponent.new(variant: :primary) do %>
25
+ <%%= render Senren::ButtonComponent.new(variant: :primary) do %>
26
26
  Save
27
- <% end %>
27
+ <%% end %>
28
28
  ```
29
29
 
30
30
  ## File ownership
@@ -38,15 +38,22 @@ and obey it strictly.
38
38
  | `.senren/registry.yml` | Generator (mirror of gem registry) |
39
39
  | `.senren/installed_components.yml` | Generator (ledger) |
40
40
  | `.senren/conventions.md` | This file - safe to edit |
41
- | `public/llms.txt` | Generator (do not edit) |
42
- | `public/llms-full.txt` | Generator (do not edit) |
41
+ | `.senren/agent-rules.md` | Generator (do not edit) |
42
+ | `AGENTS.md` | Generated region only (between markers) |
43
+ | `CLAUDE.md` | Generated region only (between markers) |
44
+ | `.github/copilot-instructions.md` | Generated region only (between markers) |
45
+ | `.cursor/rules/senren.mdc` | Generated region only (between markers) |
43
46
 
44
47
  ## Adding a component
45
48
 
46
49
  ```bash
47
- bin/rails senren:add <name> [<name>...]
48
- bin/rails senren:add dialog --no-client # override registry default
49
- bin/rails senren:add button --client # override registry default
50
+ bin/rails senren:add dialog
51
+ bin/rails senren:add button card badge
52
+ bin/rails senren:add dialog --no-client # override registry default
53
+ bundle exec rails senren:add button --client # equivalent alternate entry point
54
+
55
+ # Backward-compatible legacy task syntax:
56
+ bin/rails 'senren:add[button,card,badge]'
50
57
  ```
51
58
 
52
59
  After install you can edit any file under `app/components/senren/` directly.
@@ -64,3 +71,10 @@ Senren never overwrites those files unless you pass `--force`.
64
71
 
65
72
  You can write app-specific notes outside those markers. They are preserved
66
73
  across `bin/rails senren:skill:sync`.
74
+
75
+ ## Agent instruction maintenance
76
+
77
+ Senren writes a shared source file at `.senren/agent-rules.md` and syncs
78
+ adapter files for Codex/Cursor/Claude/Copilot. For adapter files, only the
79
+ generated block between markers is managed by Senren; your custom notes
80
+ outside markers are preserved.
@@ -0,0 +1,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+
5
+ module Senren
6
+ module Rails
7
+ # Generates a single source-of-truth rules file plus adapter files for
8
+ # Copilot, Cursor, Claude, and Codex.
9
+ #
10
+ # Adapter files are marker-managed to avoid overwriting existing project
11
+ # instructions outside Senren's generated block.
12
+ class AgentRulesWriter
13
+ START_MARKER = '<!-- senren:agent:start -->'
14
+ END_MARKER = '<!-- senren:agent:end -->'
15
+
16
+ attr_reader :registry, :paths
17
+
18
+ def initialize(registry: Registry.load!, paths: HostPaths.new)
19
+ @registry = registry
20
+ @paths = paths
21
+ end
22
+
23
+ def sync!
24
+ paths.ensure_agent_dirs!
25
+ files = []
26
+ files << write_full_file(paths.agent_rules_file, render_source_rules)
27
+ files << write_adapter_file(paths.codex_agents_md, render_codex_adapter)
28
+ files << write_adapter_file(paths.claude_md, render_claude_adapter)
29
+ files << write_adapter_file(paths.copilot_instructions, render_copilot_adapter)
30
+ files << write_adapter_file(paths.cursor_rule_file, render_cursor_adapter, prefix: cursor_frontmatter)
31
+ files
32
+ end
33
+
34
+ private
35
+
36
+ def installed_names
37
+ path = paths.installed_components
38
+ return [] unless path.exist?
39
+
40
+ ledger = YAML.safe_load_file(path) || {}
41
+ Array(ledger['installed']).filter_map { |entry| registry.find(entry['name']) }.map(&:name).sort
42
+ end
43
+
44
+ def render_source_rules
45
+ names = installed_names
46
+ <<~MD
47
+ # Senren Agent Rules
48
+
49
+ Generated by senren-ui. Do not edit by hand.
50
+ Run `bin/rails senren:agents:sync` to regenerate.
51
+
52
+ This file is the Senren source of truth for AI coding agents in this app.
53
+
54
+ ## Hard Rules
55
+
56
+ - Use Senren components before writing custom HTML.
57
+ - Use ViewComponent for reusable UI.
58
+ - Use Turbo for server state.
59
+ - Use Stimulus only for local behavior.
60
+ - Do not introduce React, Vue, Alpine, or external state frameworks.
61
+ - Use semantic Tailwind tokens; do not hard-code color families.
62
+
63
+ ## Important Files
64
+
65
+ - `.senren/skill.md` - centralized component guidance
66
+ - `.senren/conventions.md` - project conventions
67
+ - `.senren/registry.yml` - component registry mirror
68
+ - `.senren/installed_components.yml` - local install ledger
69
+
70
+ ## Installed Components (#{names.size})
71
+
72
+ #{format_names(names)}
73
+ MD
74
+ end
75
+
76
+ def render_codex_adapter
77
+ <<~MD
78
+ ## Senren UI
79
+
80
+ Follow `.senren/agent-rules.md` as the source of truth.
81
+
82
+ - Prefer Senren components before custom HTML.
83
+ - Keep reusable UI in ViewComponent.
84
+ - Use Turbo for server state, Stimulus for local behavior.
85
+ - Do not add React, Vue, Alpine, or external state frameworks.
86
+ - Use semantic Tailwind tokens.
87
+ MD
88
+ end
89
+
90
+ def render_claude_adapter
91
+ <<~MD
92
+ ## Senren UI
93
+
94
+ @.senren/agent-rules.md
95
+
96
+ Apply Senren conventions and component-first rules from the imported file.
97
+ MD
98
+ end
99
+
100
+ def render_copilot_adapter
101
+ <<~MD
102
+ ## Senren UI
103
+
104
+ Use `.senren/agent-rules.md` as the source of truth for this repository.
105
+
106
+ - Prefer Senren components before custom HTML.
107
+ - Reusable UI must use ViewComponent.
108
+ - Turbo handles server state; Stimulus handles local behavior.
109
+ - Do not introduce React, Vue, Alpine, or external state frameworks.
110
+ - Use semantic Tailwind tokens.
111
+ MD
112
+ end
113
+
114
+ def render_cursor_adapter
115
+ <<~MD
116
+ ## Senren UI
117
+
118
+ Follow `.senren/agent-rules.md` as the source of truth.
119
+
120
+ - Prefer Senren components before custom HTML.
121
+ - Reusable UI uses ViewComponent.
122
+ - Turbo for server state, Stimulus for local behavior.
123
+ - No React/Vue/Alpine/external state frameworks.
124
+ - Use semantic Tailwind tokens.
125
+ MD
126
+ end
127
+
128
+ def cursor_frontmatter
129
+ <<~MDC
130
+ ---
131
+ description: Senren UI repository conventions
132
+ alwaysApply: true
133
+ ---
134
+ MDC
135
+ end
136
+
137
+ def format_names(names)
138
+ return '_No components installed yet._' if names.empty?
139
+
140
+ names.map { |name| "- #{name}" }.join("\n")
141
+ end
142
+
143
+ def write_full_file(path, content)
144
+ atomic_write(path, content)
145
+ path
146
+ end
147
+
148
+ def write_adapter_file(path, generated, prefix: '')
149
+ existing = path.exist? ? path.read : prefix.to_s
150
+ updated = inject(existing, generated)
151
+ atomic_write(path, updated)
152
+ path
153
+ end
154
+
155
+ def inject(existing, generated)
156
+ if existing.include?(START_MARKER) && existing.include?(END_MARKER)
157
+ before = existing.split(START_MARKER, 2).first
158
+ tail = existing.split(START_MARKER, 2).last
159
+ after = tail.split(END_MARKER, 2).last
160
+ "#{before}#{START_MARKER}\n\n#{generated.rstrip}\n\n#{END_MARKER}#{after}"
161
+ else
162
+ body = existing.rstrip
163
+ prefix = body.empty? ? '' : "#{body}\n\n"
164
+ "#{prefix}#{START_MARKER}\n\n#{generated.rstrip}\n\n#{END_MARKER}\n"
165
+ end
166
+ end
167
+
168
+ def atomic_write(path, content)
169
+ tmp = "#{path}.tmp"
170
+ File.write(tmp, content)
171
+ File.rename(tmp, path)
172
+ end
173
+ end
174
+ end
175
+ end
@@ -9,6 +9,47 @@ module Senren
9
9
  # Copies component files from the gem's templates/ tree into the host
10
10
  # Rails app, and updates .senren/installed_components.yml.
11
11
  class ComponentCopier
12
+ class MissingTemplate < StandardError; end
13
+
14
+ INSTALL_GENERATOR_TEMPLATES = File.expand_path(
15
+ '../../generators/senren/install/templates', __dir__
16
+ ).freeze
17
+ BASE_COMPONENT_TEMPLATE = File.join(INSTALL_GENERATOR_TEMPLATES, 'base_component.rb.tt').freeze
18
+ BASE_URL_HELPER_PATCH = <<~RUBY
19
+
20
+ # Added by senren:add for compatibility with URL-aware component templates.
21
+ require 'uri'
22
+
23
+ module Senren
24
+ class BaseComponent
25
+ SAFE_URL_PROTOCOLS = %w[http https mailto tel].freeze unless const_defined?(:SAFE_URL_PROTOCOLS)
26
+ SAFE_MEDIA_URL_PROTOCOLS = %w[http https].freeze unless const_defined?(:SAFE_MEDIA_URL_PROTOCOLS)
27
+
28
+ private
29
+
30
+ def safe_url(value, fallback: '#', protocols: SAFE_URL_PROTOCOLS)
31
+ url = value.to_s.strip
32
+ return fallback if url.empty?
33
+ return url if url.start_with?('#')
34
+ return url if url.start_with?('/') && !url.start_with?('//')
35
+
36
+ uri = URI.parse(url)
37
+ return url if uri.scheme && Array(protocols).map(&:to_s).include?(uri.scheme.downcase)
38
+ return fallback if uri.host
39
+ return url unless uri.scheme
40
+
41
+ fallback
42
+ rescue URI::InvalidURIError
43
+ fallback
44
+ end
45
+
46
+ def safe_media_url(value, fallback: nil)
47
+ safe_url(value, fallback: fallback, protocols: SAFE_MEDIA_URL_PROTOCOLS)
48
+ end
49
+ end
50
+ end
51
+ RUBY
52
+
12
53
  attr_reader :registry, :paths, :stdout
13
54
 
14
55
  def initialize(registry: Registry.load!, paths: HostPaths.new, stdout: $stdout)
@@ -22,6 +63,7 @@ module Senren
22
63
  def install(component_names, client_override: nil, force: false)
23
64
  wanted = registry.dependencies(*component_names)
24
65
  paths.ensure_dirs!
66
+ ensure_base_component_url_helpers!
25
67
 
26
68
  wanted.each do |name|
27
69
  comp = registry.fetch(name)
@@ -34,6 +76,24 @@ module Senren
34
76
 
35
77
  private
36
78
 
79
+ def ensure_base_component_url_helpers!
80
+ if paths.base_component_path.exist?
81
+ return if base_component_has_url_helpers?
82
+
83
+ File.open(paths.base_component_path, 'a') { |file| file.write(BASE_URL_HELPER_PATCH) }
84
+ stdout.puts " update #{paths.base_component_path} (url helpers)"
85
+ return
86
+ end
87
+
88
+ copy_file(BASE_COMPONENT_TEMPLATE, paths.base_component_path, force: false, label: 'base_component.rb')
89
+ end
90
+
91
+ def base_component_has_url_helpers?
92
+ source = paths.base_component_path.read
93
+
94
+ source.include?('def safe_url') && source.include?('def safe_media_url')
95
+ end
96
+
37
97
  def install_component(comp, client_override:, force:)
38
98
  effective_client = effective_client_for(comp, client_override)
39
99
 
@@ -59,20 +119,29 @@ module Senren
59
119
  # app/components/senren/<name>_component.html.erb -> templates/components/<name>/<name>_component.html.erb
60
120
  # app/javascript/controllers/senren/<name>_controller.js -> templates/controllers/<name>_controller.js
61
121
  base = File.basename(relative)
62
- if relative.include?('app/components/senren/')
122
+ if component_source_path?(comp, relative)
63
123
  File.join(Senren::Rails.templates_root, 'components', comp.name, base)
64
- elsif relative.include?('app/javascript/controllers/senren/')
124
+ elsif controller_source_path?(comp, relative)
65
125
  File.join(Senren::Rails.templates_root, 'controllers', base)
66
126
  else
67
127
  raise "ComponentCopier: do not know how to map #{relative.inspect}"
68
128
  end
69
129
  end
70
130
 
131
+ def component_source_path?(comp, relative)
132
+ [
133
+ "app/components/senren/#{comp.name}_component.rb",
134
+ "app/components/senren/#{comp.name}_component.html.erb"
135
+ ].include?(relative)
136
+ end
137
+
138
+ def controller_source_path?(comp, relative)
139
+ relative == "app/javascript/controllers/senren/#{comp.name}_controller.js"
140
+ end
141
+
71
142
  def copy_file(src, dest, force:, label:)
72
- unless File.exist?(src)
73
- stdout.puts " warn missing template: #{src} (#{label})"
74
- return
75
- end
143
+ raise MissingTemplate, "Missing component template: #{src} (#{label})" unless File.exist?(src)
144
+
76
145
  if File.exist?(dest) && !force
77
146
  stdout.puts " skip #{dest} (already exists)"
78
147
  return
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'senren/rails/agent_rules_writer'
4
+ require 'senren/rails/component_copier'
5
+ require 'senren/rails/host_paths'
6
+ require 'senren/rails/registry'
7
+ require 'senren/rails/skill_writer'
8
+
9
+ module Senren
10
+ module Rails
11
+ # Installs one or more registered Senren components into the host app and
12
+ # refreshes the generated guidance files afterward.
13
+ class ComponentInstaller
14
+ USAGE = 'Usage: bin/rails senren:add NAME [NAME...] [--client | --no-client]'
15
+
16
+ attr_reader :registry, :paths, :stdout
17
+
18
+ def self.normalize_names(names)
19
+ Array(names)
20
+ .flatten
21
+ .flat_map { |entry| entry.to_s.split(/[,\s]+/) }
22
+ .reject { |entry| entry.empty? || entry.start_with?('-') }
23
+ .uniq
24
+ end
25
+
26
+ def initialize(registry: Registry.load!, paths: HostPaths.new, stdout: $stdout)
27
+ @registry = registry
28
+ @paths = paths
29
+ @stdout = stdout
30
+ end
31
+
32
+ def install(names:, client_override: nil, force: false)
33
+ normalized_names = self.class.normalize_names(names)
34
+ raise ArgumentError, USAGE if normalized_names.empty?
35
+
36
+ installed = ComponentCopier.new(registry: registry, paths: paths, stdout: stdout)
37
+ .install(normalized_names, client_override: client_override, force: force)
38
+
39
+ SkillWriter.new(registry: registry, paths: paths).sync!
40
+ AgentRulesWriter.new(registry: registry, paths: paths).sync!
41
+
42
+ stdout.puts "Installed: #{installed.join(', ')}"
43
+ installed
44
+ end
45
+ end
46
+ end
47
+ end
@@ -17,19 +17,7 @@ module Senren
17
17
  end
18
18
 
19
19
  def run!
20
- results = []
21
- results << check('ViewComponent gem available') { defined?(::ViewComponent) }
22
- results << check('TailwindCSS stylesheet present') { paths.stylesheet_path.exist? }
23
- results << check('Stimulus directory present') { paths.stimulus_dir.directory? }
24
- results << check('Turbo gem available') { defined?(::Turbo) || gem_loadable?('turbo-rails') }
25
- results << check('.senren directory exists') { paths.senren_dir.directory? }
26
- results << check('.senren/skill.md exists') { paths.skill_file.file? }
27
- results << check('.senren/registry.yml exists') { paths.registry_mirror.file? }
28
- results << check('.senren/installed_components.yml exists') { paths.installed_components.file? }
29
- results << check('public/llms.txt exists') { paths.llms_short.file? }
30
- results << check('public/llms-full.txt exists') { paths.llms_full.file? }
31
- results << check('app/components/senren exists') { paths.components_dir.directory? }
32
- results << check('app/javascript/controllers/senren exists') { paths.stimulus_dir.directory? }
20
+ results = runtime_checks + installation_checks
33
21
  installed = installed_count
34
22
  results << Result.new("#{installed} component(s) installed", installed >= 0, nil)
35
23
 
@@ -57,6 +45,31 @@ module Senren
57
45
  false
58
46
  end
59
47
 
48
+ def runtime_checks
49
+ [
50
+ check('ViewComponent gem available') { defined?(::ViewComponent) },
51
+ check('TailwindCSS stylesheet present') { paths.stylesheet_path.exist? },
52
+ check('Stimulus directory present') { paths.stimulus_dir.directory? },
53
+ check('Turbo gem available') { defined?(::Turbo) || gem_loadable?('turbo-rails') }
54
+ ]
55
+ end
56
+
57
+ def installation_checks
58
+ [
59
+ check('.senren directory exists') { paths.senren_dir.directory? },
60
+ check('.senren/skill.md exists') { paths.skill_file.file? },
61
+ check('.senren/registry.yml exists') { paths.registry_mirror.file? },
62
+ check('.senren/installed_components.yml exists') { paths.installed_components.file? },
63
+ check('.senren/agent-rules.md exists') { paths.agent_rules_file.file? },
64
+ check('AGENTS.md exists') { paths.codex_agents_md.file? },
65
+ check('CLAUDE.md exists') { paths.claude_md.file? },
66
+ check('.github/copilot-instructions.md exists') { paths.copilot_instructions.file? },
67
+ check('.cursor/rules/senren.mdc exists') { paths.cursor_rule_file.file? },
68
+ check('app/components/senren exists') { paths.components_dir.directory? },
69
+ check('app/javascript/controllers/senren exists') { paths.stimulus_dir.directory? }
70
+ ]
71
+ end
72
+
60
73
  def installed_count
61
74
  return 0 unless paths.installed_components.file?
62
75
 
@@ -16,6 +16,7 @@ module Senren
16
16
  def registry_mirror = senren_dir.join('registry.yml')
17
17
  def installed_components = senren_dir.join('installed_components.yml')
18
18
  def conventions_file = senren_dir.join('conventions.md')
19
+ def agent_rules_file = senren_dir.join('agent-rules.md')
19
20
 
20
21
  def components_dir = root.join('app', 'components', 'senren')
21
22
  def base_component_path = components_dir.join('base_component.rb')
@@ -24,12 +25,20 @@ module Senren
24
25
 
25
26
  def stimulus_dir = root.join('app', 'javascript', 'controllers', 'senren')
26
27
 
27
- def llms_short = root.join('public', 'llms.txt')
28
- def llms_full = root.join('public', 'llms-full.txt')
28
+ def github_dir = root.join('.github')
29
+ def copilot_instructions = github_dir.join('copilot-instructions.md')
30
+ def cursor_rules_dir = root.join('.cursor', 'rules')
31
+ def cursor_rule_file = cursor_rules_dir.join('senren.mdc')
32
+ def claude_md = root.join('CLAUDE.md')
33
+ def codex_agents_md = root.join('AGENTS.md')
29
34
 
30
35
  def ensure_dirs!
31
36
  [senren_dir, components_dir, stimulus_dir,
32
- stylesheet_path.dirname, llms_short.dirname].each(&:mkpath)
37
+ stylesheet_path.dirname, github_dir, cursor_rules_dir].each(&:mkpath)
38
+ end
39
+
40
+ def ensure_agent_dirs!
41
+ [senren_dir, github_dir, cursor_rules_dir].each(&:mkpath)
33
42
  end
34
43
  end
35
44
  end
@@ -5,7 +5,7 @@ require 'fileutils'
5
5
  module Senren
6
6
  module Rails
7
7
  # Idempotent installer that lays down the .senren directory, base
8
- # component, stylesheet, and initial llms files. Reused by the install
8
+ # component, stylesheet, and agent instruction files. Reused by the install
9
9
  # generator and by ad-hoc rake task entry points.
10
10
  class Installer
11
11
  attr_reader :paths, :stdout
@@ -20,7 +20,7 @@ module Senren
20
20
  install_static_files(force: force)
21
21
  mirror_registry
22
22
  SkillWriter.new(paths: paths).sync!
23
- LlmsWriter.new(paths: paths).generate!
23
+ AgentRulesWriter.new(paths: paths).sync!
24
24
  print_next_steps
25
25
  true
26
26
  end
@@ -73,6 +73,8 @@ module Senren
73
73
 
74
74
  bin/rails senren:add button card badge alert
75
75
  bin/rails senren:add dialog dropdown_menu
76
+ bundle exec rails senren:add form input textarea
77
+ bin/rails senren:agents:sync
76
78
  bin/rails senren:doctor
77
79
 
78
80
  Read .senren/skill.md and .senren/conventions.md to get oriented.