rubyllm-observ 0.5.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 (209) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +778 -0
  3. data/Rakefile +49 -0
  4. data/app/assets/javascripts/observ/application.js +12 -0
  5. data/app/assets/javascripts/observ/controllers/autoscroll_controller.js +33 -0
  6. data/app/assets/javascripts/observ/controllers/chat_form_controller.js +93 -0
  7. data/app/assets/javascripts/observ/controllers/copy_controller.js +43 -0
  8. data/app/assets/javascripts/observ/controllers/dashboard_controller.js +58 -0
  9. data/app/assets/javascripts/observ/controllers/drawer_controller.js +58 -0
  10. data/app/assets/javascripts/observ/controllers/expandable_controller.js +33 -0
  11. data/app/assets/javascripts/observ/controllers/filter_controller.js +36 -0
  12. data/app/assets/javascripts/observ/controllers/index.js +52 -0
  13. data/app/assets/javascripts/observ/controllers/json_viewer_controller.js +260 -0
  14. data/app/assets/javascripts/observ/controllers/message_form_controller.js +58 -0
  15. data/app/assets/javascripts/observ/controllers/prompt_variables_controller.js +64 -0
  16. data/app/assets/javascripts/observ/controllers/text_select_controller.js +14 -0
  17. data/app/assets/stylesheets/observ/_annotations.scss +127 -0
  18. data/app/assets/stylesheets/observ/_card.scss +52 -0
  19. data/app/assets/stylesheets/observ/_chat.scss +156 -0
  20. data/app/assets/stylesheets/observ/_components.scss +460 -0
  21. data/app/assets/stylesheets/observ/_dashboard.scss +40 -0
  22. data/app/assets/stylesheets/observ/_datasets.scss +697 -0
  23. data/app/assets/stylesheets/observ/_drawer.scss +273 -0
  24. data/app/assets/stylesheets/observ/_json_viewer.scss +120 -0
  25. data/app/assets/stylesheets/observ/_layout.scss +256 -0
  26. data/app/assets/stylesheets/observ/_metrics.scss +99 -0
  27. data/app/assets/stylesheets/observ/_observations.scss +160 -0
  28. data/app/assets/stylesheets/observ/_pagination.scss +143 -0
  29. data/app/assets/stylesheets/observ/_prompts.scss +365 -0
  30. data/app/assets/stylesheets/observ/_table.scss +53 -0
  31. data/app/assets/stylesheets/observ/_variables.scss +53 -0
  32. data/app/assets/stylesheets/observ/application.scss +15 -0
  33. data/app/controllers/observ/annotations_controller.rb +144 -0
  34. data/app/controllers/observ/application_controller.rb +8 -0
  35. data/app/controllers/observ/chats_controller.rb +58 -0
  36. data/app/controllers/observ/dashboard_controller.rb +159 -0
  37. data/app/controllers/observ/dataset_items_controller.rb +85 -0
  38. data/app/controllers/observ/dataset_run_items_controller.rb +84 -0
  39. data/app/controllers/observ/dataset_runs_controller.rb +110 -0
  40. data/app/controllers/observ/datasets_controller.rb +74 -0
  41. data/app/controllers/observ/messages_controller.rb +26 -0
  42. data/app/controllers/observ/observations_controller.rb +59 -0
  43. data/app/controllers/observ/prompt_versions_controller.rb +148 -0
  44. data/app/controllers/observ/prompts_controller.rb +205 -0
  45. data/app/controllers/observ/sessions_controller.rb +45 -0
  46. data/app/controllers/observ/traces_controller.rb +86 -0
  47. data/app/forms/observ/prompt_form.rb +96 -0
  48. data/app/helpers/observ/application_helper.rb +9 -0
  49. data/app/helpers/observ/chats_helper.rb +47 -0
  50. data/app/helpers/observ/dashboard_helper.rb +154 -0
  51. data/app/helpers/observ/datasets_helper.rb +62 -0
  52. data/app/helpers/observ/pagination_helper.rb +38 -0
  53. data/app/jobs/observ/application_job.rb +4 -0
  54. data/app/jobs/observ/dataset_runner_job.rb +49 -0
  55. data/app/mailers/observ/application_mailer.rb +6 -0
  56. data/app/models/concerns/observ/agent_phaseable.rb +124 -0
  57. data/app/models/concerns/observ/agent_selectable.rb +50 -0
  58. data/app/models/concerns/observ/chat_enhancements.rb +109 -0
  59. data/app/models/concerns/observ/message_enhancements.rb +31 -0
  60. data/app/models/concerns/observ/observability_instrumentation.rb +124 -0
  61. data/app/models/concerns/observ/prompt_management.rb +320 -0
  62. data/app/models/concerns/observ/trace_association.rb +9 -0
  63. data/app/models/observ/annotation.rb +23 -0
  64. data/app/models/observ/application_record.rb +5 -0
  65. data/app/models/observ/dataset.rb +51 -0
  66. data/app/models/observ/dataset_item.rb +41 -0
  67. data/app/models/observ/dataset_run.rb +104 -0
  68. data/app/models/observ/dataset_run_item.rb +111 -0
  69. data/app/models/observ/generation.rb +56 -0
  70. data/app/models/observ/null_prompt.rb +59 -0
  71. data/app/models/observ/observation.rb +38 -0
  72. data/app/models/observ/prompt.rb +315 -0
  73. data/app/models/observ/score.rb +51 -0
  74. data/app/models/observ/session.rb +131 -0
  75. data/app/models/observ/span.rb +13 -0
  76. data/app/models/observ/trace.rb +135 -0
  77. data/app/presenters/observ/agent_select_presenter.rb +59 -0
  78. data/app/services/observ/agent_executor_service.rb +174 -0
  79. data/app/services/observ/agent_provider.rb +60 -0
  80. data/app/services/observ/agent_selection_service.rb +53 -0
  81. data/app/services/observ/chat_instrumenter.rb +523 -0
  82. data/app/services/observ/dataset_runner_service.rb +153 -0
  83. data/app/services/observ/evaluator_runner_service.rb +58 -0
  84. data/app/services/observ/evaluators/base_evaluator.rb +51 -0
  85. data/app/services/observ/evaluators/contains_evaluator.rb +53 -0
  86. data/app/services/observ/evaluators/exact_match_evaluator.rb +23 -0
  87. data/app/services/observ/evaluators/json_structure_evaluator.rb +44 -0
  88. data/app/services/observ/prompt_manager/cache_statistics.rb +82 -0
  89. data/app/services/observ/prompt_manager/caching.rb +167 -0
  90. data/app/services/observ/prompt_manager/comparison.rb +49 -0
  91. data/app/services/observ/prompt_manager/version_management.rb +96 -0
  92. data/app/services/observ/prompt_manager.rb +40 -0
  93. data/app/services/observ/trace_text_formatter.rb +349 -0
  94. data/app/validators/observ/prompt_config_validator.rb +187 -0
  95. data/app/views/kaminari/_first_page.html.erb +11 -0
  96. data/app/views/kaminari/_gap.html.erb +8 -0
  97. data/app/views/kaminari/_last_page.html.erb +11 -0
  98. data/app/views/kaminari/_next_page.html.erb +11 -0
  99. data/app/views/kaminari/_page.html.erb +12 -0
  100. data/app/views/kaminari/_paginator.html.erb +25 -0
  101. data/app/views/kaminari/_prev_page.html.erb +11 -0
  102. data/app/views/kaminari/observ/_first_page.html.erb +11 -0
  103. data/app/views/kaminari/observ/_gap.html.erb +8 -0
  104. data/app/views/kaminari/observ/_last_page.html.erb +11 -0
  105. data/app/views/kaminari/observ/_next_page.html.erb +11 -0
  106. data/app/views/kaminari/observ/_page.html.erb +12 -0
  107. data/app/views/kaminari/observ/_paginator.html.erb +25 -0
  108. data/app/views/kaminari/observ/_prev_page.html.erb +11 -0
  109. data/app/views/layouts/observ/application.html.erb +88 -0
  110. data/app/views/observ/annotations/_annotation.html.erb +13 -0
  111. data/app/views/observ/annotations/_form.html.erb +28 -0
  112. data/app/views/observ/annotations/index.html.erb +28 -0
  113. data/app/views/observ/annotations/sessions_index.html.erb +48 -0
  114. data/app/views/observ/annotations/traces_index.html.erb +48 -0
  115. data/app/views/observ/chats/_form.html.erb +45 -0
  116. data/app/views/observ/chats/index.html.erb +67 -0
  117. data/app/views/observ/chats/new.html.erb +17 -0
  118. data/app/views/observ/chats/show.html.erb +34 -0
  119. data/app/views/observ/dashboard/index.html.erb +236 -0
  120. data/app/views/observ/dataset_items/_form.html.erb +49 -0
  121. data/app/views/observ/dataset_items/edit.html.erb +18 -0
  122. data/app/views/observ/dataset_items/index.html.erb +95 -0
  123. data/app/views/observ/dataset_items/new.html.erb +18 -0
  124. data/app/views/observ/dataset_run_items/_score_close_drawer.html.erb +4 -0
  125. data/app/views/observ/dataset_run_items/_score_drawer.html.erb +75 -0
  126. data/app/views/observ/dataset_run_items/_score_success.html.erb +29 -0
  127. data/app/views/observ/dataset_run_items/_scores_cell.html.erb +19 -0
  128. data/app/views/observ/dataset_run_items/details_drawer.turbo_stream.erb +80 -0
  129. data/app/views/observ/dataset_run_items/score_drawer.turbo_stream.erb +7 -0
  130. data/app/views/observ/dataset_runs/index.html.erb +108 -0
  131. data/app/views/observ/dataset_runs/new.html.erb +57 -0
  132. data/app/views/observ/dataset_runs/review.html.erb +155 -0
  133. data/app/views/observ/dataset_runs/show.html.erb +166 -0
  134. data/app/views/observ/datasets/_form.html.erb +62 -0
  135. data/app/views/observ/datasets/_items_tab.html.erb +66 -0
  136. data/app/views/observ/datasets/_runs_tab.html.erb +82 -0
  137. data/app/views/observ/datasets/edit.html.erb +32 -0
  138. data/app/views/observ/datasets/index.html.erb +105 -0
  139. data/app/views/observ/datasets/new.html.erb +18 -0
  140. data/app/views/observ/datasets/show.html.erb +67 -0
  141. data/app/views/observ/messages/_content.html.erb +1 -0
  142. data/app/views/observ/messages/_form.html.erb +33 -0
  143. data/app/views/observ/messages/_message.html.erb +14 -0
  144. data/app/views/observ/messages/_tool_calls.html.erb +10 -0
  145. data/app/views/observ/messages/create.turbo_stream.erb +9 -0
  146. data/app/views/observ/observations/index.html.erb +97 -0
  147. data/app/views/observ/observations/show_generation.html.erb +195 -0
  148. data/app/views/observ/observations/show_span.html.erb +93 -0
  149. data/app/views/observ/prompts/_diff_content.html.erb +16 -0
  150. data/app/views/observ/prompts/_form.html.erb +111 -0
  151. data/app/views/observ/prompts/_new_form.html.erb +102 -0
  152. data/app/views/observ/prompts/_prompt_actions.html.erb +4 -0
  153. data/app/views/observ/prompts/_prompt_content_highlighted.html.erb +4 -0
  154. data/app/views/observ/prompts/_version_actions.html.erb +40 -0
  155. data/app/views/observ/prompts/compare.html.erb +155 -0
  156. data/app/views/observ/prompts/edit.html.erb +17 -0
  157. data/app/views/observ/prompts/index.html.erb +108 -0
  158. data/app/views/observ/prompts/new.html.erb +17 -0
  159. data/app/views/observ/prompts/show.html.erb +138 -0
  160. data/app/views/observ/prompts/versions.html.erb +87 -0
  161. data/app/views/observ/sessions/annotations_drawer.turbo_stream.erb +25 -0
  162. data/app/views/observ/sessions/drawer_test.turbo_stream.erb +49 -0
  163. data/app/views/observ/sessions/index.html.erb +91 -0
  164. data/app/views/observ/sessions/show.html.erb +251 -0
  165. data/app/views/observ/traces/add_to_dataset_drawer.turbo_stream.erb +48 -0
  166. data/app/views/observ/traces/annotations_drawer.turbo_stream.erb +25 -0
  167. data/app/views/observ/traces/index.html.erb +87 -0
  168. data/app/views/observ/traces/show.html.erb +285 -0
  169. data/app/views/observ/traces/text_output_drawer.turbo_stream.erb +48 -0
  170. data/app/views/shared/_drawer.html.erb +26 -0
  171. data/config/routes.rb +80 -0
  172. data/db/migrate/001_create_observ_sessions.rb +21 -0
  173. data/db/migrate/002_create_observ_traces.rb +25 -0
  174. data/db/migrate/003_create_observ_observations.rb +42 -0
  175. data/db/migrate/004_add_message_id_to_observ_traces.rb +7 -0
  176. data/db/migrate/005_create_observ_prompts.rb +21 -0
  177. data/db/migrate/006_fix_prompt_config_strings.rb +23 -0
  178. data/db/migrate/007_create_observ_annotations.rb +12 -0
  179. data/db/migrate/009_add_prompt_fields_to_observ_chats.rb +11 -0
  180. data/db/migrate/010_create_observ_datasets.rb +15 -0
  181. data/db/migrate/011_create_observ_dataset_items.rb +17 -0
  182. data/db/migrate/012_create_observ_dataset_runs.rb +22 -0
  183. data/db/migrate/013_create_observ_dataset_run_items.rb +16 -0
  184. data/db/migrate/014_create_observ_scores.rb +26 -0
  185. data/lib/generators/observ/add_phase_tracking/add_phase_tracking_generator.rb +150 -0
  186. data/lib/generators/observ/add_phase_tracking/templates/migration.rb.tt +6 -0
  187. data/lib/generators/observ/install/USAGE +27 -0
  188. data/lib/generators/observ/install/install_generator.rb +270 -0
  189. data/lib/generators/observ/install_chat/install_chat_generator.rb +313 -0
  190. data/lib/generators/observ/install_chat/templates/agents/base_agent.rb.tt +147 -0
  191. data/lib/generators/observ/install_chat/templates/agents/simple_agent.rb.tt +55 -0
  192. data/lib/generators/observ/install_chat/templates/concerns/observ_chat_enhancements.rb.tt +34 -0
  193. data/lib/generators/observ/install_chat/templates/concerns/observ_message_enhancements.rb.tt +18 -0
  194. data/lib/generators/observ/install_chat/templates/initializers/observability.rb.tt +20 -0
  195. data/lib/generators/observ/install_chat/templates/jobs/chat_response_job.rb.tt +56 -0
  196. data/lib/generators/observ/install_chat/templates/migrations/add_agent_class_name.rb.tt +6 -0
  197. data/lib/generators/observ/install_chat/templates/migrations/add_observability_session_id.rb.tt +6 -0
  198. data/lib/generators/observ/install_chat/templates/tools/think_tool.rb.tt +29 -0
  199. data/lib/generators/observ/install_chat/templates/views/messages/_content.html.erb.tt +1 -0
  200. data/lib/observ/asset_installer.rb +130 -0
  201. data/lib/observ/asset_syncer.rb +104 -0
  202. data/lib/observ/configuration.rb +108 -0
  203. data/lib/observ/engine.rb +50 -0
  204. data/lib/observ/index_file_generator.rb +142 -0
  205. data/lib/observ/instrumenter/ruby_llm.rb +6 -0
  206. data/lib/observ/version.rb +3 -0
  207. data/lib/observ.rb +29 -0
  208. data/lib/tasks/observ_tasks.rake +75 -0
  209. metadata +453 -0
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateObservDatasetRunItems < ActiveRecord::Migration[7.0]
4
+ def change
5
+ create_table :observ_dataset_run_items do |t|
6
+ t.references :dataset_run, null: false, foreign_key: { to_table: :observ_dataset_runs }
7
+ t.references :dataset_item, null: false, foreign_key: { to_table: :observ_dataset_items }
8
+ t.references :trace, foreign_key: { to_table: :observ_traces }
9
+ t.references :observation, foreign_key: { to_table: :observ_observations }
10
+ t.text :error
11
+ t.timestamps
12
+
13
+ t.index [ :dataset_run_id, :dataset_item_id ], unique: true, name: "idx_run_items_on_run_and_item"
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateObservScores < ActiveRecord::Migration[7.0]
4
+ def change
5
+ create_table :observ_scores do |t|
6
+ t.references :dataset_run_item, null: false, foreign_key: { to_table: :observ_dataset_run_items }
7
+ t.references :trace, null: false, foreign_key: { to_table: :observ_traces }
8
+ t.references :observation, foreign_key: { to_table: :observ_observations }
9
+
10
+ t.string :name, null: false
11
+ t.decimal :value, precision: 10, scale: 4, null: false
12
+ t.integer :data_type, default: 0, null: false
13
+ t.integer :source, default: 0, null: false
14
+
15
+ t.text :comment
16
+ t.string :string_value
17
+ t.string :created_by
18
+
19
+ t.timestamps
20
+
21
+ t.index [ :dataset_run_item_id, :name, :source ], unique: true, name: "idx_scores_on_run_item_name_source"
22
+ t.index [ :trace_id, :name ]
23
+ t.index :name
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/active_record"
5
+
6
+ module Observ
7
+ module Generators
8
+ # Generator for adding phase tracking to Observ chats
9
+ #
10
+ # This generator adds the ability to track multi-phase agent workflows
11
+ # by adding a current_phase column and including the AgentPhaseable concern.
12
+ #
13
+ # Prerequisites:
14
+ # - Observ chat feature already installed (rails generate observ:install:chat)
15
+ # - Chat model exists at app/models/chat.rb
16
+ # - ObservChatEnhancements concern exists
17
+ #
18
+ # Usage:
19
+ # rails generate observ:add_phase_tracking
20
+ #
21
+ # What it does:
22
+ # 1. Adds current_phase column to chats table
23
+ # 2. Includes Observ::AgentPhaseable in ObservChatEnhancements
24
+ #
25
+ class AddPhaseTrackingGenerator < Rails::Generators::Base
26
+ include ActiveRecord::Generators::Migration
27
+
28
+ source_root File.expand_path("templates", __dir__)
29
+
30
+ def check_prerequisites
31
+ say "\n"
32
+ say "=" * 80, :cyan
33
+ say "Adding Phase Tracking to Observ Chats", :cyan
34
+ say "=" * 80, :cyan
35
+ say "\n"
36
+
37
+ check_chat_model_exists
38
+ check_concern_exists
39
+ end
40
+
41
+ def create_migration
42
+ say "Creating migration for current_phase column...", :cyan
43
+ say "-" * 80, :cyan
44
+
45
+ migration_template "migration.rb.tt",
46
+ "db/migrate/add_phase_tracking_to_chats.rb"
47
+
48
+ say " ✓ Created migration for current_phase column", :green
49
+ say "\n"
50
+ end
51
+
52
+ def update_chat_enhancements
53
+ say "Updating ObservChatEnhancements concern...", :cyan
54
+ say "-" * 80, :cyan
55
+
56
+ concern_path = Rails.root.join("app/models/concerns/observ_chat_enhancements.rb")
57
+ concern_content = File.read(concern_path)
58
+
59
+ if concern_content.include?("Observ::AgentPhaseable")
60
+ say " ⚠ AgentPhaseable already included in ObservChatEnhancements", :yellow
61
+ else
62
+ inject_into_file concern_path,
63
+ after: "include Observ::ObservabilityInstrumentation\n" do
64
+ " include Observ::AgentPhaseable\n"
65
+ end
66
+ say " ✓ Included AgentPhaseable in ObservChatEnhancements", :green
67
+ end
68
+
69
+ say "\n"
70
+ end
71
+
72
+ def show_post_install_instructions
73
+ say "\n"
74
+ say "=" * 80, :green
75
+ say "Phase Tracking Installation Complete!", :green
76
+ say "=" * 80, :green
77
+ say "\n"
78
+
79
+ say "Next steps:", :cyan
80
+ say "\n"
81
+
82
+ say "1. Run migrations:", :cyan
83
+ say " rails db:migrate", :white
84
+ say "\n"
85
+
86
+ say "2. (Optional) Define allowed phases in your Chat model:", :cyan
87
+ say " # app/models/chat.rb", :white
88
+ say " class Chat < ApplicationRecord", :white
89
+ say " # ...", :white
90
+ say " def allowed_phases", :white
91
+ say " %w[scoping research writing review]", :white
92
+ say " end", :white
93
+ say " end", :white
94
+ say "\n"
95
+
96
+ say "3. Use phase transitions in your agents:", :cyan
97
+ say " chat.transition_to_phase('research')", :white
98
+ say " chat.in_phase?('research') # => true", :white
99
+ say " chat.current_phase # => 'research'", :white
100
+ say "\n"
101
+
102
+ say "Documentation:", :cyan
103
+ say " • See app/models/concerns/observ/agent_phaseable.rb for full API", :white
104
+ say "\n"
105
+ end
106
+
107
+ private
108
+
109
+ def check_chat_model_exists
110
+ unless File.exist?(Rails.root.join("app/models/chat.rb"))
111
+ raise Thor::Error, <<~ERROR
112
+ Chat model not found!
113
+
114
+ This generator requires the Chat model to exist.
115
+
116
+ Please run:
117
+ rails generate observ:install:chat
118
+ rails db:migrate
119
+ #{' '}
120
+ Then run this generator again.
121
+ ERROR
122
+ end
123
+ say " ✓ Chat model found", :green
124
+ end
125
+
126
+ def check_concern_exists
127
+ concern_path = Rails.root.join("app/models/concerns/observ_chat_enhancements.rb")
128
+ unless File.exist?(concern_path)
129
+ raise Thor::Error, <<~ERROR
130
+ ObservChatEnhancements concern not found!
131
+
132
+ This generator requires observ:install:chat to be run first.
133
+
134
+ Please run:
135
+ rails generate observ:install:chat
136
+ rails db:migrate
137
+ #{' '}
138
+ Then run this generator again.
139
+ ERROR
140
+ end
141
+ say " ✓ ObservChatEnhancements concern found", :green
142
+ end
143
+
144
+ # Helper for migration timestamps
145
+ def migration_version
146
+ "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,6 @@
1
+ class AddPhaseTrackingToChats < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ add_column :chats, :current_phase, :string
4
+ add_index :chats, :current_phase
5
+ end
6
+ end
@@ -0,0 +1,27 @@
1
+ Description:
2
+ Install Observ assets (stylesheets and JavaScript controllers) in your Rails application.
3
+
4
+ This generator will:
5
+ - Copy Observ stylesheets to your app
6
+ - Copy Observ JavaScript Stimulus controllers to your app
7
+ - Generate index files for easy importing
8
+ - Check if controllers are properly registered
9
+
10
+ Examples:
11
+ rails generate observ:install
12
+
13
+ This will install assets to default locations:
14
+ - Styles: app/javascript/stylesheets/observ
15
+ - JavaScript: app/javascript/controllers/observ
16
+
17
+ rails generate observ:install --styles-dest=app/assets/stylesheets/observ
18
+
19
+ This will install stylesheets to a custom location
20
+
21
+ rails generate observ:install --js-dest=app/javascript/controllers/custom_observ
22
+
23
+ This will install JavaScript controllers to a custom location
24
+
25
+ rails generate observ:install --skip-index
26
+
27
+ This will skip generation of index files (useful if you want to manage imports manually)
@@ -0,0 +1,270 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "observ/asset_installer"
5
+
6
+ module Observ
7
+ module Generators
8
+ # Generator for installing Observ assets in a Rails application
9
+ #
10
+ # Usage:
11
+ # rails generate observ:install
12
+ # rails generate observ:install --styles-dest=custom/path
13
+ # rails generate observ:install --js-dest=custom/path
14
+ # rails generate observ:install --skip-index
15
+ # rails generate observ:install --skip-routes # Don't auto-mount engine
16
+ # rails generate observ:install --force # Skip confirmation prompt
17
+ class InstallGenerator < Rails::Generators::Base
18
+ source_root File.expand_path("templates", __dir__)
19
+
20
+ class_option :styles_dest,
21
+ type: :string,
22
+ desc: "Destination path for stylesheets (default: app/javascript/stylesheets/observ)",
23
+ default: nil
24
+
25
+ class_option :js_dest,
26
+ type: :string,
27
+ desc: "Destination path for JavaScript controllers (default: app/javascript/controllers/observ)",
28
+ default: nil
29
+
30
+ class_option :skip_index,
31
+ type: :boolean,
32
+ desc: "Skip generation of index files",
33
+ default: false
34
+
35
+ class_option :force,
36
+ type: :boolean,
37
+ desc: "Skip confirmation prompt",
38
+ default: false
39
+
40
+ class_option :skip_routes,
41
+ type: :boolean,
42
+ desc: "Skip automatic route mounting",
43
+ default: false
44
+
45
+ def confirm_installation
46
+ return if options[:force]
47
+
48
+ styles_dest = options[:styles_dest] || Observ::AssetInstaller::DEFAULT_STYLES_DEST
49
+ js_dest = options[:js_dest] || Observ::AssetInstaller::DEFAULT_JS_DEST
50
+
51
+ say "\n"
52
+ say "=" * 80, :cyan
53
+ say "Observ Installation", :cyan
54
+ say "=" * 80, :cyan
55
+ say "\n"
56
+
57
+ # Collect files that will actually be copied
58
+ stylesheets_to_copy = collect_files_to_copy("stylesheets", "*.scss", styles_dest)
59
+ js_files_to_copy = collect_files_to_copy("javascripts", "*.js", js_dest)
60
+ index_will_be_generated = !options[:skip_index] && will_generate_index?(js_dest)
61
+ route_will_be_added = !options[:skip_routes] && !route_already_exists?
62
+
63
+ # Check if there are any changes to make
64
+ total_changes = stylesheets_to_copy.count + js_files_to_copy.count +
65
+ (index_will_be_generated ? 1 : 0) + (route_will_be_added ? 1 : 0)
66
+
67
+ if total_changes == 0
68
+ say "No changes needed - all files are up to date!", :green
69
+ say "\n"
70
+ return
71
+ end
72
+
73
+ say "The following changes will be made:", :yellow
74
+ say "\n"
75
+
76
+ # Show stylesheet files that will be copied
77
+ if stylesheets_to_copy.any?
78
+ say "Stylesheets (#{stylesheets_to_copy.count} files to #{styles_dest}):", :yellow
79
+ stylesheets_to_copy.each do |file|
80
+ say " • #{File.basename(file)}", :white
81
+ end
82
+ say "\n"
83
+ end
84
+
85
+ # Show JavaScript files that will be copied
86
+ if js_files_to_copy.any?
87
+ say "JavaScript Controllers (#{js_files_to_copy.count} files to #{js_dest}):", :yellow
88
+ js_files_to_copy.each do |file|
89
+ say " • #{File.basename(file)}", :white
90
+ end
91
+ say "\n"
92
+ end
93
+
94
+ # Show generated files
95
+ if index_will_be_generated
96
+ say "Generated Files:", :yellow
97
+ say " • #{js_dest}/index.js (controller index)", :white
98
+ say "\n"
99
+ end
100
+
101
+ # Show routes
102
+ if route_will_be_added
103
+ say "Routes (will be added to config/routes.rb):", :yellow
104
+ say ' mount Observ::Engine, at: "/observ"', :white
105
+ say "\n"
106
+ elsif !options[:skip_routes]
107
+ say "Routes:", :green
108
+ say " Engine already mounted in config/routes.rb", :green
109
+ say "\n"
110
+ end
111
+
112
+ unless yes?("Do you want to proceed with the installation? (y/n)", :yellow)
113
+ say "\nInstallation cancelled.", :red
114
+ exit 0
115
+ end
116
+ say "\n"
117
+ end
118
+
119
+ def mount_engine
120
+ return if options[:skip_routes]
121
+
122
+ say "Checking routes...", :cyan
123
+ say "-" * 80, :cyan
124
+
125
+ if route_already_exists?
126
+ say " Engine already mounted in config/routes.rb", :yellow
127
+ else
128
+ route 'mount Observ::Engine, at: "/observ"'
129
+ say " ✓ Added route: mount Observ::Engine, at: \"/observ\"", :green
130
+ end
131
+ say "\n"
132
+ end
133
+
134
+ def install_assets
135
+ installer = Observ::AssetInstaller.new(
136
+ gem_root: Observ::Engine.root,
137
+ app_root: Rails.root,
138
+ logger: GeneratorLogger.new(self)
139
+ )
140
+
141
+ @result = installer.install(
142
+ styles_dest: options[:styles_dest],
143
+ js_dest: options[:js_dest],
144
+ generate_index: !options[:skip_index]
145
+ )
146
+ end
147
+
148
+ def show_post_install_message
149
+ say "\n"
150
+ say "=" * 80, :green
151
+ say "Observ installed successfully!", :green
152
+ say "=" * 80, :green
153
+ say "\n"
154
+
155
+ if @result[:registration] && @result[:registration][:suggestions]
156
+ say "⚠ Action required:", :yellow
157
+ @result[:registration][:suggestions].each do |suggestion|
158
+ say " #{suggestion}", :yellow
159
+ end
160
+ say "\n"
161
+ end
162
+
163
+ say "Next steps:", :cyan
164
+ say " 1. Import stylesheets in your application", :cyan
165
+ say " Add to app/javascript/application.js:", :cyan
166
+ say " import 'observ'", :cyan
167
+ say "\n"
168
+ say " 2. Restart your development server", :cyan
169
+ say " bin/dev or rails server", :cyan
170
+ say "\n"
171
+ say " 3. Visit /observ in your browser", :cyan
172
+ unless options[:skip_routes]
173
+ say " (Engine mounted at /observ)", :cyan
174
+ end
175
+ say "\n"
176
+ end
177
+
178
+ private
179
+
180
+ def route_already_exists?
181
+ routes_file = Rails.root.join("config/routes.rb")
182
+ return false unless routes_file.exist?
183
+
184
+ routes_content = File.read(routes_file)
185
+ routes_content.match?(/mount\s+Observ::Engine/)
186
+ end
187
+
188
+ # Collect only files that will actually be copied (new or modified)
189
+ # @param asset_type [String] "stylesheets" or "javascripts"
190
+ # @param pattern [String] File glob pattern (e.g., "*.scss", "*.js")
191
+ # @param dest_path [String] Destination directory path
192
+ # @return [Array<String>] List of source file paths that will be copied
193
+ def collect_files_to_copy(asset_type, pattern, dest_path)
194
+ source_path = get_source_path(asset_type)
195
+ return [] unless source_path.directory?
196
+
197
+ dest_path = Rails.root.join(dest_path)
198
+
199
+ files_to_copy = []
200
+ Dir.glob(source_path.join(pattern)).sort.each do |source_file|
201
+ filename = File.basename(source_file)
202
+ dest_file = dest_path.join(filename)
203
+
204
+ if should_copy_file?(source_file, dest_file)
205
+ files_to_copy << source_file
206
+ end
207
+ end
208
+
209
+ files_to_copy
210
+ end
211
+
212
+ # Get the source path for the given asset type
213
+ # @param asset_type [String] "stylesheets" or "javascripts"
214
+ # @return [Pathname] Source directory path
215
+ def get_source_path(asset_type)
216
+ if asset_type == "stylesheets"
217
+ Observ::Engine.root.join("app", "assets", "stylesheets", "observ")
218
+ else
219
+ Observ::Engine.root.join("app", "assets", "javascripts", "observ", "controllers")
220
+ end
221
+ end
222
+
223
+ # Determine if a file should be copied (matches AssetSyncer logic)
224
+ # @param source_file [String] Path to source file
225
+ # @param dest_file [Pathname] Path to destination file
226
+ # @return [Boolean] true if file should be copied
227
+ def should_copy_file?(source_file, dest_file)
228
+ !dest_file.exist? || !FileUtils.identical?(source_file, dest_file.to_s)
229
+ end
230
+
231
+ # Check if index.js will be generated (new or different content)
232
+ # @param js_dest [String] Destination path for JavaScript controllers
233
+ # @return [Boolean] true if index.js will be generated
234
+ def will_generate_index?(js_dest)
235
+ index_file = Rails.root.join(js_dest, "index.js")
236
+ # Index file will be generated if it doesn't exist
237
+ # (we don't check content as the generator always creates it)
238
+ !index_file.exist?
239
+ end
240
+
241
+ # Logger adapter for Rails generator
242
+ class GeneratorLogger
243
+ def initialize(generator)
244
+ @generator = generator
245
+ end
246
+
247
+ def puts(message)
248
+ # Remove color codes and special characters for cleaner output
249
+ clean_message = message.gsub(/[✓✗⚠-]/, "").strip
250
+
251
+ if message.include?("✓") || message.include?("Copied")
252
+ @generator.say(" #{clean_message}", :green)
253
+ elsif message.include?("⚠")
254
+ @generator.say(" #{clean_message}", :yellow)
255
+ elsif message.include?("=") || message.start_with?("Syncing", "Generating", "Checking")
256
+ @generator.say(clean_message, :cyan)
257
+ elsif message.include?("Skipped")
258
+ @generator.say(" #{clean_message}", :white)
259
+ else
260
+ @generator.say(clean_message)
261
+ end
262
+ end
263
+
264
+ def info(message)
265
+ puts(message)
266
+ end
267
+ end
268
+ end
269
+ end
270
+ end