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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +304 -0
- data/app/assets/javascripts/rails_markup/toolbar.js +2225 -0
- data/app/controllers/rails_markup/annotations_controller.rb +244 -0
- data/app/controllers/rails_markup/application_controller.rb +7 -0
- data/app/controllers/rails_markup/dashboard_controller.rb +230 -0
- data/app/controllers/rails_markup/external/annotations_controller.rb +68 -0
- data/app/models/rails_markup/annotation.rb +176 -0
- data/app/views/layouts/rails_markup/application.html.erb +158 -0
- data/app/views/rails_markup/dashboard/_annotation_card.html.erb +25 -0
- data/app/views/rails_markup/dashboard/_annotation_list.html.erb +57 -0
- data/app/views/rails_markup/dashboard/_annotation_page.html.erb +13 -0
- data/app/views/rails_markup/dashboard/_filters.html.erb +75 -0
- data/app/views/rails_markup/dashboard/_stats.html.erb +22 -0
- data/app/views/rails_markup/dashboard/_styles.html.erb +115 -0
- data/app/views/rails_markup/dashboard/board.html.erb +135 -0
- data/app/views/rails_markup/dashboard/index.html.erb +38 -0
- data/app/views/rails_markup/dashboard/show.html.erb +129 -0
- data/app/views/rails_markup/shared/_toolbar.html.erb +28 -0
- data/bin/rails-markup +5 -0
- data/config/routes.rb +45 -0
- data/db/migrate/20260720000000_add_client_uuid_to_rails_markup_annotations.rb +13 -0
- data/db/migrate/20260721000000_backfill_rails_markup_client_uuids.rb +17 -0
- data/lib/generators/rails_markup/install/templates/auth_controller.rb.erb +17 -0
- data/lib/generators/rails_markup/install/templates/bin_markup.erb +5 -0
- data/lib/generators/rails_markup/install/templates/create_rails_markup_annotations.rb.erb +27 -0
- data/lib/generators/rails_markup/install/templates/initializer.rb.erb +54 -0
- data/lib/generators/rails_markup/install_generator.rb +167 -0
- data/lib/generators/rails_markup/uninstall_generator.rb +110 -0
- data/lib/rails_markup/cli/base.rb +8 -0
- data/lib/rails_markup/cli/initializer_writer.rb +54 -0
- data/lib/rails_markup/cli/setup_wizard.rb +229 -0
- data/lib/rails_markup/cli.rb +831 -0
- data/lib/rails_markup/client_uuid_maintenance.rb +174 -0
- data/lib/rails_markup/configuration.rb +160 -0
- data/lib/rails_markup/engine.rb +18 -0
- data/lib/rails_markup/http_server.rb +226 -0
- data/lib/rails_markup/http_store_proxy.rb +122 -0
- data/lib/rails_markup/mcp_config.rb +258 -0
- data/lib/rails_markup/mcp_server.rb +639 -0
- data/lib/rails_markup/server.rb +73 -0
- data/lib/rails_markup/store.rb +219 -0
- data/lib/rails_markup/version.rb +5 -0
- data/lib/rails_markup.rb +11 -0
- data/lib/tasks/rails_markup_client_uuids.rake +19 -0
- 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
|