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,167 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/active_record"
|
|
5
|
+
|
|
6
|
+
module RailsMarkup
|
|
7
|
+
module Generators
|
|
8
|
+
class InstallGenerator < Rails::Generators::Base
|
|
9
|
+
include ActiveRecord::Generators::Migration
|
|
10
|
+
|
|
11
|
+
source_root File.expand_path("install/templates", __dir__)
|
|
12
|
+
|
|
13
|
+
desc "Install Rails Markup: create migration, initializer, auth controller, toolbar, and mount engine routes."
|
|
14
|
+
|
|
15
|
+
class_option :mount_path, type: :string, default: "/admin/annotations",
|
|
16
|
+
desc: "Path to mount the Rails Markup engine"
|
|
17
|
+
class_option :base_controller, type: :string, default: "ApplicationController",
|
|
18
|
+
desc: "Base controller class for authentication"
|
|
19
|
+
class_option :layout, type: :string, default: "application",
|
|
20
|
+
desc: "Layout to inject the toolbar into"
|
|
21
|
+
class_option :table_name, type: :string, default: "rails_markup_annotations",
|
|
22
|
+
desc: "Database table name for annotations (must match config.table_name)"
|
|
23
|
+
|
|
24
|
+
def copy_migration
|
|
25
|
+
migration_template "create_rails_markup_annotations.rb.erb",
|
|
26
|
+
"db/migrate/create_rails_markup_annotations.rb"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def create_initializer
|
|
30
|
+
template "initializer.rb.erb", "config/initializers/rails_markup.rb"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Keep the model's config.table_name in lockstep with the table the
|
|
34
|
+
# migration actually creates when a custom name is requested.
|
|
35
|
+
def configure_table_name
|
|
36
|
+
return if options[:table_name] == "rails_markup_annotations"
|
|
37
|
+
|
|
38
|
+
gsub_file "config/initializers/rails_markup.rb",
|
|
39
|
+
/^\s*#?\s*config\.table_name\s*=.*$/,
|
|
40
|
+
%( config.table_name = "#{options[:table_name]}")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def create_auth_controller
|
|
44
|
+
template "auth_controller.rb.erb", "app/controllers/rails_markup_auth_controller.rb"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def mount_engine
|
|
48
|
+
mount_path = options[:mount_path]
|
|
49
|
+
route_line = %{mount RailsMarkup::Engine, at: "#{mount_path}" if defined?(RailsMarkup::Engine)}
|
|
50
|
+
route route_line
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def inject_toolbar_into_layout
|
|
54
|
+
layout_path = "app/views/layouts/#{options[:layout]}.html.erb"
|
|
55
|
+
|
|
56
|
+
unless File.exist?(File.join(destination_root, layout_path))
|
|
57
|
+
say_status :skip, "#{layout_path} not found — add the toolbar manually", :yellow
|
|
58
|
+
return
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
toolbar_block = <<~ERB.indent(4)
|
|
62
|
+
<%# Rails Markup annotation toolbar %>
|
|
63
|
+
<% if lookup_context.exists?("rails_markup/shared/toolbar", [], true) %>
|
|
64
|
+
<%= render "rails_markup/shared/toolbar" %>
|
|
65
|
+
<% end %>
|
|
66
|
+
ERB
|
|
67
|
+
|
|
68
|
+
if File.read(File.join(destination_root, layout_path)).include?("rails_markup/shared/toolbar")
|
|
69
|
+
say_status :skip, "toolbar already present in #{layout_path}", :yellow
|
|
70
|
+
return
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
inject_into_file layout_path, toolbar_block, before: %r{</body>}
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def inject_procfile
|
|
77
|
+
procfile = File.join(destination_root, "Procfile.dev")
|
|
78
|
+
return unless File.exist?(procfile)
|
|
79
|
+
|
|
80
|
+
content = File.read(procfile)
|
|
81
|
+
|
|
82
|
+
if content =~ /^markup:/
|
|
83
|
+
unless content =~ /^markup:\s*bin\/markup\s+server/
|
|
84
|
+
gsub_file "Procfile.dev", /^markup:.*$/, "markup: bin/markup server"
|
|
85
|
+
say_status :update, "Procfile.dev (markup → bin/markup server)", :green
|
|
86
|
+
else
|
|
87
|
+
say_status :skip, "Procfile.dev already uses bin/markup server", :yellow
|
|
88
|
+
end
|
|
89
|
+
else
|
|
90
|
+
append_to_file "Procfile.dev", "markup: bin/markup server\n"
|
|
91
|
+
say_status :append, "Procfile.dev (added markup server)", :green
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def create_bin_wrapper
|
|
96
|
+
template "bin_markup.erb", "bin/markup"
|
|
97
|
+
chmod "bin/markup", 0o755
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def configure_mcp
|
|
101
|
+
require_relative "../../rails_markup/mcp_config"
|
|
102
|
+
config = RailsMarkup::McpConfig.new(dir: destination_root)
|
|
103
|
+
dev_url = detect_dev_url
|
|
104
|
+
env_updates = {
|
|
105
|
+
"RAILS_MARKUP_DEV_URL" => dev_url,
|
|
106
|
+
"RAILS_MARKUP_MOUNT_PATH" => options[:mount_path]
|
|
107
|
+
}
|
|
108
|
+
config.update_env(env_updates)
|
|
109
|
+
say_status :create, ".mcp.json (dev URL: #{dev_url})", :green
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def show_post_install
|
|
113
|
+
say ""
|
|
114
|
+
say "Rails Markup installed successfully!", :green
|
|
115
|
+
say ""
|
|
116
|
+
say "Created:"
|
|
117
|
+
say " - db/migrate/*_create_rails_markup_annotations.rb"
|
|
118
|
+
say " - config/initializers/rails_markup.rb"
|
|
119
|
+
say " - app/controllers/rails_markup_auth_controller.rb"
|
|
120
|
+
say " - bin/markup"
|
|
121
|
+
say " - .mcp.json (MCP config for AI agents)"
|
|
122
|
+
say ""
|
|
123
|
+
say "Next steps:"
|
|
124
|
+
say " 1. Run migrations: rails db:migrate"
|
|
125
|
+
say " 2. Visit dashboard: #{options[:mount_path]}"
|
|
126
|
+
say " 3. Configure auth in config/initializers/rails_markup.rb"
|
|
127
|
+
say ""
|
|
128
|
+
dev_url = detect_dev_url
|
|
129
|
+
unless dev_url == "http://localhost:3000"
|
|
130
|
+
say "Detected dev server: #{dev_url} (from Procfile.dev)"
|
|
131
|
+
end
|
|
132
|
+
say ""
|
|
133
|
+
say "Not using localhost:3000? Update your dev URL:", :yellow
|
|
134
|
+
say " bin/markup configure --dev-url=http://YOUR_HOST:PORT"
|
|
135
|
+
say ""
|
|
136
|
+
say "CLI commands:"
|
|
137
|
+
say " bin/markup fetch # See pending annotations"
|
|
138
|
+
say " bin/markup fetch --env=production # From production"
|
|
139
|
+
say " bin/markup configure --prod-url=URL --prod-token=TOKEN"
|
|
140
|
+
say " bin/markup status # Show current config"
|
|
141
|
+
say ""
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
private
|
|
145
|
+
|
|
146
|
+
def migration_version
|
|
147
|
+
"[#{ActiveRecord::Migration.current_version}]"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def detect_dev_url
|
|
151
|
+
# Try to read bind address and port from Procfile.dev
|
|
152
|
+
procfile = File.join(destination_root, "Procfile.dev")
|
|
153
|
+
if File.exist?(procfile)
|
|
154
|
+
content = File.read(procfile)
|
|
155
|
+
if content =~ /-b\s+([\d.]+)\s+-p\s+(\d+)/
|
|
156
|
+
return "http://#{$1}:#{$2}"
|
|
157
|
+
end
|
|
158
|
+
if content =~ /-p\s+(\d+)/
|
|
159
|
+
return "http://localhost:#{$1}"
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
"http://localhost:3000"
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
|
|
5
|
+
module RailsMarkup
|
|
6
|
+
module Generators
|
|
7
|
+
class UninstallGenerator < Rails::Generators::Base
|
|
8
|
+
desc "Uninstall Rails Markup: remove initializer, auth controller, toolbar, routes, and bin wrapper."
|
|
9
|
+
|
|
10
|
+
class_option :remove_migration, type: :boolean, default: false,
|
|
11
|
+
desc: "Also remove the migration file (dangerous — only if table was never created)"
|
|
12
|
+
|
|
13
|
+
def remove_route_mount
|
|
14
|
+
route_file = File.join(destination_root, "config/routes.rb")
|
|
15
|
+
return unless File.exist?(route_file)
|
|
16
|
+
|
|
17
|
+
gsub_file "config/routes.rb", /^\s*mount RailsMarkup::Engine.*\n/, ""
|
|
18
|
+
say_status :remove, "RailsMarkup engine mount from routes", :green
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def remove_initializer
|
|
22
|
+
path = "config/initializers/rails_markup.rb"
|
|
23
|
+
remove_file path if File.exist?(File.join(destination_root, path))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def remove_auth_controller
|
|
27
|
+
path = "app/controllers/rails_markup_auth_controller.rb"
|
|
28
|
+
full_path = File.join(destination_root, path)
|
|
29
|
+
return unless File.exist?(full_path)
|
|
30
|
+
|
|
31
|
+
content = File.read(full_path)
|
|
32
|
+
if content.include?("RailsMarkupAuthController")
|
|
33
|
+
remove_file path
|
|
34
|
+
else
|
|
35
|
+
say_status :skip, "#{path} doesn't match generated pattern — leaving in place", :yellow
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def remove_toolbar_from_layouts
|
|
40
|
+
Dir.glob(File.join(destination_root, "app/views/layouts/*.html.erb")).each do |layout|
|
|
41
|
+
relative = layout.sub("#{destination_root}/", "")
|
|
42
|
+
content = File.read(layout)
|
|
43
|
+
next unless content.include?("rails_markup/shared/toolbar")
|
|
44
|
+
|
|
45
|
+
gsub_file relative, /^\s*<%# Rails Markup annotation toolbar %>\n/, ""
|
|
46
|
+
gsub_file relative, /^\s*<% if lookup_context\.exists\?\("rails_markup\/shared\/toolbar".*%>\n\s*<%= render "rails_markup\/shared\/toolbar" %>\n\s*<% end %>\n/, ""
|
|
47
|
+
say_status :remove, "toolbar from #{relative}", :green
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def remove_mcp_config
|
|
52
|
+
mcp_path = File.join(destination_root, ".mcp.json")
|
|
53
|
+
return unless File.exist?(mcp_path)
|
|
54
|
+
|
|
55
|
+
config = JSON.parse(File.read(mcp_path))
|
|
56
|
+
server = config.dig("mcpServers", "rails-markup")
|
|
57
|
+
return unless server
|
|
58
|
+
|
|
59
|
+
config["mcpServers"].delete("rails-markup")
|
|
60
|
+
|
|
61
|
+
if config["mcpServers"].empty?
|
|
62
|
+
remove_file ".mcp.json"
|
|
63
|
+
else
|
|
64
|
+
File.write(mcp_path, JSON.pretty_generate(config) + "\n")
|
|
65
|
+
say_status :remove, "rails-markup entry from .mcp.json", :green
|
|
66
|
+
end
|
|
67
|
+
rescue JSON::ParserError
|
|
68
|
+
say_status :skip, ".mcp.json has invalid JSON — leaving in place", :yellow
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def remove_bin_wrapper
|
|
72
|
+
path = "bin/markup"
|
|
73
|
+
full_path = File.join(destination_root, path)
|
|
74
|
+
return unless File.exist?(full_path)
|
|
75
|
+
|
|
76
|
+
content = File.read(full_path)
|
|
77
|
+
if content.include?("rails_markup") || content.include?("RailsMarkup")
|
|
78
|
+
remove_file path
|
|
79
|
+
else
|
|
80
|
+
say_status :skip, "#{path} doesn't reference rails_markup — leaving in place", :yellow
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def remove_migration
|
|
85
|
+
return unless options[:remove_migration]
|
|
86
|
+
|
|
87
|
+
migration = Dir.glob(File.join(destination_root, "db/migrate/*_create_rails_markup_annotations.rb")).first
|
|
88
|
+
if migration
|
|
89
|
+
remove_file migration.sub("#{destination_root}/", "")
|
|
90
|
+
else
|
|
91
|
+
say_status :skip, "no rails_markup migration found", :yellow
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def show_post_uninstall
|
|
96
|
+
say ""
|
|
97
|
+
say "Rails Markup uninstalled.", :green
|
|
98
|
+
say ""
|
|
99
|
+
say "Remaining manual steps:"
|
|
100
|
+
say " 1. Remove 'rails_markup' from your Gemfile and run bundle"
|
|
101
|
+
unless options[:remove_migration]
|
|
102
|
+
say " 2. Drop the table: rails db:migrate:down VERSION=<migration_version>"
|
|
103
|
+
say " Or: rails generate rails_markup:uninstall --remove-migration"
|
|
104
|
+
end
|
|
105
|
+
say " 3. Search for stale references: grep -r 'rails_markup\\|RailsMarkup' app/ config/"
|
|
106
|
+
say ""
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module RailsMarkup
|
|
6
|
+
class Cli
|
|
7
|
+
# Reads/writes config/initializers/rails_markup.rb with upsert semantics.
|
|
8
|
+
# Handles toolbar_accent, toolbar_position, toolbar_size, enable_screenshots,
|
|
9
|
+
# toolbar_enabled, fab_visible.
|
|
10
|
+
class InitializerWriter
|
|
11
|
+
MANAGED_KEYS = %w[toolbar_accent toolbar_position toolbar_size enable_screenshots toolbar_enabled fab_visible].freeze
|
|
12
|
+
|
|
13
|
+
def initialize(dir: Dir.pwd)
|
|
14
|
+
@path = File.join(dir, "config", "initializers", "rails_markup.rb")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def write(**options)
|
|
18
|
+
content = File.exist?(@path) ? File.read(@path) : template
|
|
19
|
+
options.each do |key, value|
|
|
20
|
+
content = upsert_config(content, key.to_s, value)
|
|
21
|
+
end
|
|
22
|
+
File.write(@path, content)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def upsert_config(content, key, value)
|
|
28
|
+
formatted = format_value(value)
|
|
29
|
+
pattern = /^(\s*)#?\s*config\.#{Regexp.escape(key)}\s*=.*$/
|
|
30
|
+
|
|
31
|
+
if content.match?(pattern)
|
|
32
|
+
content.gsub(pattern, "\\1config.#{key} = #{formatted}")
|
|
33
|
+
else
|
|
34
|
+
content.sub(/^(end\s*)$/, " config.#{key} = #{formatted}\n\\1")
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def format_value(value)
|
|
39
|
+
case value
|
|
40
|
+
when true, false then value.to_s
|
|
41
|
+
when Integer, Float then value.to_s
|
|
42
|
+
else %("#{value}")
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def template
|
|
47
|
+
<<~RUBY
|
|
48
|
+
RailsMarkup.configure do |config|
|
|
49
|
+
end
|
|
50
|
+
RUBY
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bubbletea"
|
|
4
|
+
require "lipgloss"
|
|
5
|
+
require_relative "base"
|
|
6
|
+
require_relative "../mcp_config"
|
|
7
|
+
require_relative "initializer_writer"
|
|
8
|
+
|
|
9
|
+
module RailsMarkup
|
|
10
|
+
class Cli
|
|
11
|
+
# Interactive TUI setup wizard using Bubbletea's Elm architecture.
|
|
12
|
+
# State machine: accent → position → size → fab → screenshots → url → scope → confirm
|
|
13
|
+
class SetupWizard
|
|
14
|
+
include Bubbletea::Model
|
|
15
|
+
|
|
16
|
+
STEPS = %i[accent position size fab screenshots url scope confirm].freeze
|
|
17
|
+
|
|
18
|
+
STEP_CONFIG = {
|
|
19
|
+
accent: {
|
|
20
|
+
title: "Toolbar Accent",
|
|
21
|
+
type: :select,
|
|
22
|
+
options: %w[indigo amber blue emerald rose]
|
|
23
|
+
},
|
|
24
|
+
position: {
|
|
25
|
+
title: "Toolbar Position",
|
|
26
|
+
type: :select,
|
|
27
|
+
options: %w[bl br tl tr],
|
|
28
|
+
labels: { "bl" => "Bottom-left", "br" => "Bottom-right", "tl" => "Top-left", "tr" => "Top-right" }
|
|
29
|
+
},
|
|
30
|
+
size: {
|
|
31
|
+
title: "Toolbar Size",
|
|
32
|
+
type: :select,
|
|
33
|
+
options: %w[slim compact default],
|
|
34
|
+
labels: { "slim" => "Slim (32px)", "compact" => "Compact (40px)", "default" => "Default (48px)" }
|
|
35
|
+
},
|
|
36
|
+
fab: {
|
|
37
|
+
title: "Show Floating Button",
|
|
38
|
+
type: :select,
|
|
39
|
+
options: %w[yes no],
|
|
40
|
+
labels: { "yes" => "Yes — show the FAB", "no" => "No — hide the FAB (pins still show)" }
|
|
41
|
+
},
|
|
42
|
+
screenshots: {
|
|
43
|
+
title: "Enable Screenshots",
|
|
44
|
+
type: :select,
|
|
45
|
+
options: %w[yes no],
|
|
46
|
+
labels: { "yes" => "Yes", "no" => "No" }
|
|
47
|
+
},
|
|
48
|
+
url: {
|
|
49
|
+
title: "Production URL",
|
|
50
|
+
type: :text,
|
|
51
|
+
hint: "Optional — press Enter to skip"
|
|
52
|
+
},
|
|
53
|
+
scope: {
|
|
54
|
+
title: "MCP Server Scope",
|
|
55
|
+
type: :select,
|
|
56
|
+
options: %w[local global codex],
|
|
57
|
+
labels: {
|
|
58
|
+
"local" => "This project only (.mcp.json)",
|
|
59
|
+
"global" => "All projects — Claude Code (~/.claude/settings.json)",
|
|
60
|
+
"codex" => "All projects — Codex CLI (~/.codex/config.toml)"
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
confirm: {
|
|
64
|
+
title: "Confirm Setup",
|
|
65
|
+
type: :confirm
|
|
66
|
+
}
|
|
67
|
+
}.freeze
|
|
68
|
+
|
|
69
|
+
HEADER_STYLE = Lipgloss::Style.new.bold(true).foreground("#FFFFFF").background("#5C4AE4").padding(0, 1)
|
|
70
|
+
HINT_STYLE = Lipgloss::Style.new.foreground("#6B7280")
|
|
71
|
+
CURSOR_STYLE = Lipgloss::Style.new.bold(true).foreground("#818cf8")
|
|
72
|
+
OPTION_STYLE = Lipgloss::Style.new.foreground("#E2E2E2")
|
|
73
|
+
|
|
74
|
+
attr_reader :completed, :choices
|
|
75
|
+
|
|
76
|
+
def initialize(dir: Dir.pwd)
|
|
77
|
+
@dir = dir
|
|
78
|
+
@step_index = 0
|
|
79
|
+
@cursor = 0
|
|
80
|
+
@choices = {}
|
|
81
|
+
@text_input = ""
|
|
82
|
+
@completed = false
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def init
|
|
86
|
+
[self, nil]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def update(message)
|
|
90
|
+
case message
|
|
91
|
+
when Bubbletea::KeyMessage
|
|
92
|
+
handle_key(message)
|
|
93
|
+
else
|
|
94
|
+
[self, nil]
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def view
|
|
99
|
+
step = current_step
|
|
100
|
+
config = STEP_CONFIG[step]
|
|
101
|
+
|
|
102
|
+
lines = []
|
|
103
|
+
lines << HEADER_STYLE.render(" Rails Markup Setup — Step #{@step_index + 1}/#{STEPS.size} ")
|
|
104
|
+
lines << ""
|
|
105
|
+
lines << " #{config[:title]}"
|
|
106
|
+
lines << ""
|
|
107
|
+
|
|
108
|
+
case config[:type]
|
|
109
|
+
when :select
|
|
110
|
+
config[:options].each_with_index do |opt, i|
|
|
111
|
+
label = config.dig(:labels, opt) || opt
|
|
112
|
+
if i == @cursor
|
|
113
|
+
lines << " #{CURSOR_STYLE.render("▸")} #{label}"
|
|
114
|
+
else
|
|
115
|
+
lines << " #{OPTION_STYLE.render(label)}"
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
when :text
|
|
119
|
+
lines << " #{HINT_STYLE.render(config[:hint])}"
|
|
120
|
+
lines << ""
|
|
121
|
+
lines << " > #{@text_input}█"
|
|
122
|
+
when :confirm
|
|
123
|
+
lines << render_summary
|
|
124
|
+
lines << ""
|
|
125
|
+
lines << " #{HINT_STYLE.render("Press Enter to write config, Esc to cancel")}"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
lines << ""
|
|
129
|
+
lines << " #{HINT_STYLE.render("↑/↓ navigate · Enter select · Esc cancel")}" unless step == :confirm
|
|
130
|
+
|
|
131
|
+
lines.join("\n")
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
private
|
|
135
|
+
|
|
136
|
+
def current_step
|
|
137
|
+
STEPS[@step_index]
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def handle_key(msg)
|
|
141
|
+
return [self, Bubbletea.quit] if msg.esc?
|
|
142
|
+
|
|
143
|
+
step = current_step
|
|
144
|
+
config = STEP_CONFIG[step]
|
|
145
|
+
|
|
146
|
+
case config[:type]
|
|
147
|
+
when :select then handle_select(msg, config)
|
|
148
|
+
when :text then handle_text(msg)
|
|
149
|
+
when :confirm then handle_confirm(msg)
|
|
150
|
+
else [self, nil]
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def handle_select(msg, config)
|
|
155
|
+
if msg.up?
|
|
156
|
+
@cursor = (@cursor - 1) % config[:options].size
|
|
157
|
+
elsif msg.down?
|
|
158
|
+
@cursor = (@cursor + 1) % config[:options].size
|
|
159
|
+
elsif msg.enter?
|
|
160
|
+
value = config[:options][@cursor]
|
|
161
|
+
store_choice(current_step, value)
|
|
162
|
+
advance_step
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
[self, nil]
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def handle_text(msg)
|
|
169
|
+
if msg.enter?
|
|
170
|
+
store_choice(:url, @text_input.empty? ? nil : @text_input)
|
|
171
|
+
advance_step
|
|
172
|
+
elsif msg.backspace?
|
|
173
|
+
@text_input = @text_input[0..-2] || ""
|
|
174
|
+
elsif msg.runes?
|
|
175
|
+
@text_input += msg.char if msg.char
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
[self, nil]
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def handle_confirm(msg)
|
|
182
|
+
if msg.enter?
|
|
183
|
+
write_config
|
|
184
|
+
@completed = true
|
|
185
|
+
return [self, Bubbletea.quit]
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
[self, nil]
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def store_choice(step, value)
|
|
192
|
+
case step
|
|
193
|
+
when :accent then @choices[:toolbar_accent] = value
|
|
194
|
+
when :position then @choices[:toolbar_position] = value
|
|
195
|
+
when :size then @choices[:toolbar_size] = value
|
|
196
|
+
when :fab then @choices[:fab_visible] = (value == "yes")
|
|
197
|
+
when :screenshots then @choices[:enable_screenshots] = (value == "yes")
|
|
198
|
+
when :url then @choices[:url] = value
|
|
199
|
+
when :scope then @choices[:scope] = value
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def advance_step
|
|
204
|
+
@step_index += 1
|
|
205
|
+
@cursor = 0
|
|
206
|
+
@text_input = ""
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def write_config
|
|
210
|
+
writer = InitializerWriter.new(dir: @dir)
|
|
211
|
+
init_opts = @choices.except(:url, :scope)
|
|
212
|
+
writer.write(**init_opts)
|
|
213
|
+
|
|
214
|
+
scope = @choices[:scope] || "local"
|
|
215
|
+
config = McpConfig.new(dir: @dir, scope: scope)
|
|
216
|
+
env_vars = {}
|
|
217
|
+
env_vars["RAILS_MARKUP_PROD_URL"] = @choices[:url] if @choices[:url]
|
|
218
|
+
config.update_env(env_vars)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def render_summary
|
|
222
|
+
lines = @choices.map do |key, value|
|
|
223
|
+
" #{key}: #{value.nil? ? '(skipped)' : value}"
|
|
224
|
+
end
|
|
225
|
+
lines.join("\n")
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|