rails-markup 1.2.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 (47) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +304 -0
  4. data/app/assets/javascripts/rails_markup/toolbar.js +2225 -0
  5. data/app/controllers/rails_markup/annotations_controller.rb +244 -0
  6. data/app/controllers/rails_markup/application_controller.rb +7 -0
  7. data/app/controllers/rails_markup/dashboard_controller.rb +230 -0
  8. data/app/controllers/rails_markup/external/annotations_controller.rb +68 -0
  9. data/app/models/rails_markup/annotation.rb +176 -0
  10. data/app/views/layouts/rails_markup/application.html.erb +158 -0
  11. data/app/views/rails_markup/dashboard/_annotation_card.html.erb +25 -0
  12. data/app/views/rails_markup/dashboard/_annotation_list.html.erb +57 -0
  13. data/app/views/rails_markup/dashboard/_annotation_page.html.erb +13 -0
  14. data/app/views/rails_markup/dashboard/_filters.html.erb +75 -0
  15. data/app/views/rails_markup/dashboard/_stats.html.erb +22 -0
  16. data/app/views/rails_markup/dashboard/_styles.html.erb +115 -0
  17. data/app/views/rails_markup/dashboard/board.html.erb +135 -0
  18. data/app/views/rails_markup/dashboard/index.html.erb +38 -0
  19. data/app/views/rails_markup/dashboard/show.html.erb +129 -0
  20. data/app/views/rails_markup/shared/_toolbar.html.erb +28 -0
  21. data/bin/rails-markup +5 -0
  22. data/config/routes.rb +45 -0
  23. data/db/migrate/20260720000000_add_client_uuid_to_rails_markup_annotations.rb +13 -0
  24. data/db/migrate/20260721000000_backfill_rails_markup_client_uuids.rb +17 -0
  25. data/lib/generators/rails_markup/install/templates/auth_controller.rb.erb +17 -0
  26. data/lib/generators/rails_markup/install/templates/bin_markup.erb +5 -0
  27. data/lib/generators/rails_markup/install/templates/create_rails_markup_annotations.rb.erb +27 -0
  28. data/lib/generators/rails_markup/install/templates/initializer.rb.erb +54 -0
  29. data/lib/generators/rails_markup/install_generator.rb +167 -0
  30. data/lib/generators/rails_markup/uninstall_generator.rb +110 -0
  31. data/lib/rails_markup/cli/base.rb +8 -0
  32. data/lib/rails_markup/cli/initializer_writer.rb +54 -0
  33. data/lib/rails_markup/cli/setup_wizard.rb +229 -0
  34. data/lib/rails_markup/cli.rb +831 -0
  35. data/lib/rails_markup/client_uuid_maintenance.rb +174 -0
  36. data/lib/rails_markup/configuration.rb +160 -0
  37. data/lib/rails_markup/engine.rb +18 -0
  38. data/lib/rails_markup/http_server.rb +226 -0
  39. data/lib/rails_markup/http_store_proxy.rb +122 -0
  40. data/lib/rails_markup/mcp_config.rb +258 -0
  41. data/lib/rails_markup/mcp_server.rb +639 -0
  42. data/lib/rails_markup/server.rb +73 -0
  43. data/lib/rails_markup/store.rb +219 -0
  44. data/lib/rails_markup/version.rb +5 -0
  45. data/lib/rails_markup.rb +11 -0
  46. data/lib/tasks/rails_markup_client_uuids.rake +19 -0
  47. metadata +198 -0
@@ -0,0 +1,258 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module RailsMarkup
6
+ # Reads and writes MCP configuration.
7
+ # Supports three scopes:
8
+ # "local" → .mcp.json (project-level, Claude Code)
9
+ # "global" → ~/.claude/settings.json (Claude Code global)
10
+ # "codex" → ~/.codex/config.toml (OpenAI Codex CLI global)
11
+ class McpConfig
12
+ FILE_NAME = ".mcp.json"
13
+ GLOBAL_PATH = File.join(Dir.home, ".claude", "settings.json")
14
+ CODEX_PATH = File.join(Dir.home, ".codex", "config.toml")
15
+ SERVER_KEY = "rails-markup"
16
+
17
+ SCOPES = %w[local global codex].freeze
18
+
19
+ ENV_KEYS = {
20
+ "prod_url" => "RAILS_MARKUP_PROD_URL",
21
+ "prod_token" => "RAILS_MARKUP_PROD_TOKEN",
22
+ "dev_url" => "RAILS_MARKUP_DEV_URL",
23
+ "mount_path" => "RAILS_MARKUP_MOUNT_PATH"
24
+ }.freeze
25
+
26
+ attr_reader :scope
27
+
28
+ def initialize(dir: Dir.pwd, scope: "local")
29
+ @scope = scope
30
+ @dir = dir
31
+ @path = resolve_path(dir, scope)
32
+ end
33
+
34
+ def exist?
35
+ File.exist?(@path)
36
+ end
37
+
38
+ def path
39
+ @path
40
+ end
41
+
42
+ def read
43
+ return {} unless exist?
44
+
45
+ toml? ? read_toml : JSON.parse(File.read(@path))
46
+ end
47
+
48
+ def env
49
+ read.dig(servers_key, SERVER_KEY, "env") || {}
50
+ end
51
+
52
+ alias_method :raw_env, :env
53
+
54
+ def update_env(new_vars)
55
+ if toml?
56
+ update_env_toml(new_vars)
57
+ else
58
+ update_env_json(new_vars)
59
+ end
60
+ end
61
+
62
+ def display_env
63
+ env.each_with_object({}) do |(k, v), hash|
64
+ hash[k] = k.include?("TOKEN") ? mask(v) : v
65
+ end
66
+ end
67
+
68
+ def scope_label
69
+ case @scope
70
+ when "global" then "~/.claude/settings.json"
71
+ when "codex" then "~/.codex/config.toml"
72
+ else ".mcp.json"
73
+ end
74
+ end
75
+
76
+ def global?
77
+ @scope != "local"
78
+ end
79
+
80
+ def toml?
81
+ @scope == "codex"
82
+ end
83
+
84
+ private
85
+
86
+ def servers_key
87
+ toml? ? "mcp_servers" : "mcpServers"
88
+ end
89
+
90
+ def resolve_path(dir, scope)
91
+ case scope
92
+ when "global" then GLOBAL_PATH
93
+ when "codex" then CODEX_PATH
94
+ else File.join(dir, FILE_NAME)
95
+ end
96
+ end
97
+
98
+ # ── JSON helpers ───────────────────────────────────────────
99
+
100
+ def update_env_json(new_vars)
101
+ config = exist? ? read : {}
102
+ server_entry = skeleton_json.dig("mcpServers", SERVER_KEY)
103
+
104
+ config["mcpServers"] ||= {}
105
+ config["mcpServers"][SERVER_KEY] ||= server_entry
106
+
107
+ # Always refresh command/args to match current bin/markup detection
108
+ config["mcpServers"][SERVER_KEY]["type"] = server_entry["type"]
109
+ config["mcpServers"][SERVER_KEY]["command"] = server_entry["command"]
110
+ config["mcpServers"][SERVER_KEY]["args"] = server_entry["args"]
111
+
112
+ config["mcpServers"][SERVER_KEY]["env"] ||= {}
113
+ config["mcpServers"][SERVER_KEY]["env"].merge!(new_vars)
114
+
115
+ File.write(@path, JSON.pretty_generate(config) + "\n")
116
+ end
117
+
118
+ def skeleton_json
119
+ cmd, args = detect_command
120
+ {
121
+ "mcpServers" => {
122
+ SERVER_KEY => {
123
+ "type" => "stdio",
124
+ "command" => cmd,
125
+ "args" => args,
126
+ "env" => {}
127
+ }
128
+ }
129
+ }
130
+ end
131
+
132
+ # ── TOML helpers (Codex CLI) ───────────────────────────────
133
+
134
+ def read_toml
135
+ content = File.read(@path)
136
+ result = {}
137
+ current_section = nil
138
+
139
+ content.each_line do |line|
140
+ stripped = line.strip
141
+ next if stripped.empty? || stripped.start_with?("#")
142
+
143
+ if stripped =~ /\A\[(.+)\]\z/
144
+ current_section = $1
145
+ elsif stripped =~ /\A(\w+)\s*=\s*(.+)\z/ && current_section
146
+ value = parse_toml_value($2)
147
+ set_nested(result, current_section, $1, value)
148
+ end
149
+ end
150
+
151
+ result
152
+ end
153
+
154
+ def update_env_toml(new_vars)
155
+ ensure_parent_dir
156
+
157
+ if exist?
158
+ content = File.read(@path)
159
+ content = remove_toml_section(content, "mcp_servers.#{SERVER_KEY}")
160
+ content = remove_toml_section(content, "mcp_servers.#{SERVER_KEY}.env")
161
+ content = content.rstrip
162
+ content += "\n\n" unless content.empty?
163
+ else
164
+ content = ""
165
+ end
166
+
167
+ cmd, args = detect_command
168
+ existing_env = env
169
+ merged_env = existing_env.merge(new_vars)
170
+
171
+ content += toml_server_section(cmd, args, merged_env)
172
+ File.write(@path, content)
173
+ end
174
+
175
+ def toml_server_section(cmd, args, env_vars)
176
+ lines = []
177
+ lines << "[mcp_servers.#{SERVER_KEY}]"
178
+ lines << "command = #{toml_value(cmd)}"
179
+ lines << "args = #{toml_value(args)}"
180
+ lines << ""
181
+ lines << "[mcp_servers.#{SERVER_KEY}.env]"
182
+ env_vars.each { |k, v| lines << "#{k} = #{toml_value(v)}" }
183
+ lines << ""
184
+ lines.join("\n")
185
+ end
186
+
187
+ def remove_toml_section(content, section_name)
188
+ lines = content.lines
189
+ result = []
190
+ skip = false
191
+
192
+ lines.each do |line|
193
+ if line.strip =~ /\A\[(.+)\]\z/
194
+ skip = $1 == section_name
195
+ end
196
+
197
+ result << line unless skip
198
+ end
199
+
200
+ result.join
201
+ end
202
+
203
+ def parse_toml_value(raw)
204
+ case raw
205
+ when /\A"(.*)"\z/ then $1
206
+ when /\Atrue\z/i then true
207
+ when /\Afalse\z/i then false
208
+ when /\A\d+\z/ then raw.to_i
209
+ when /\A\[(.+)\]\z/
210
+ $1.split(",").map { |s| parse_toml_value(s.strip) }
211
+ else raw
212
+ end
213
+ end
214
+
215
+ def toml_value(val)
216
+ case val
217
+ when String then %("#{val}")
218
+ when Array then "[#{val.map { |v| toml_value(v) }.join(", ")}]"
219
+ when true then "true"
220
+ when false then "false"
221
+ when Integer then val.to_s
222
+ else %("#{val}")
223
+ end
224
+ end
225
+
226
+ def set_nested(hash, section_path, key, value)
227
+ parts = section_path.split(".")
228
+ current = hash
229
+ parts.each { |part| current = (current[part] ||= {}) }
230
+ current[key] = value
231
+ end
232
+
233
+ def ensure_parent_dir
234
+ FileUtils.mkdir_p(File.dirname(@path))
235
+ end
236
+
237
+ # ── Shared ─────────────────────────────────────────────────
238
+
239
+ # Use bin/markup if it exists (install generator creates it),
240
+ # otherwise fall back to bundle exec rails-markup.
241
+ # Global/codex scope always uses absolute paths.
242
+ def detect_command
243
+ bin_wrapper = File.join(@dir, "bin", "markup")
244
+ if File.exist?(bin_wrapper)
245
+ path = global? ? File.expand_path(bin_wrapper) : "bin/markup"
246
+ [path, ["mcp"]]
247
+ else
248
+ ["bundle", ["exec", "rails-markup", "mcp"]]
249
+ end
250
+ end
251
+
252
+ def mask(value)
253
+ return value if value.nil? || value.length <= 8
254
+
255
+ "#{value[0..3]}****"
256
+ end
257
+ end
258
+ end