rails-blocks-cli 0.1.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/README.md +45 -0
- data/exe/rails-blocks +5 -0
- data/lib/rails_blocks/cli.rb +492 -0
- metadata +56 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: a208908dfd35e07556c86dc68f85642d7f0b4428927f370898fa91611155881f
|
|
4
|
+
data.tar.gz: ea4678983d9e63e30a99450b665c3ec4be126e4b72ca1e616f0953e497345561
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 1ea4fe353fbe84f63aa493a9040b4ac51c736f47715e4be550912b2c307c4a188cf727b2f7c7522969954f94d2bc01a42b1909cca99c2807cdbf13dcdc68415e
|
|
7
|
+
data.tar.gz: a9f560d47c9f4dae1ea9dfa3b14f43fa49287913a9bbb787ab361ae84411a8b06d3da7762898de4c4d05c63a9aa091f046426188c5f1f550567c3f38408792d2
|
data/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Rails Blocks CLI
|
|
2
|
+
|
|
3
|
+
Static-first installer for Rails Blocks components.
|
|
4
|
+
|
|
5
|
+
Free component metadata and packages are read from the public `Rails-Blocks` GitHub artifact registry. The Rails Blocks app is only contacted for login, account status, and Pro component access.
|
|
6
|
+
|
|
7
|
+
## Example
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# List free components from the public registry
|
|
11
|
+
rails-blocks list --free
|
|
12
|
+
|
|
13
|
+
# Read component documentation in the terminal
|
|
14
|
+
rails-blocks docs accordion
|
|
15
|
+
|
|
16
|
+
# Install one component as shared ERB partials
|
|
17
|
+
rails-blocks install accordion --as erb_template
|
|
18
|
+
|
|
19
|
+
# Preview installing all free components as shared partials
|
|
20
|
+
rails-blocks install --all --free --as partial --dry-run
|
|
21
|
+
|
|
22
|
+
# Review local changes before updating a component package
|
|
23
|
+
rails-blocks diff accordion --as partial
|
|
24
|
+
|
|
25
|
+
# Update a component package after reviewing the diff
|
|
26
|
+
rails-blocks update accordion --as partial
|
|
27
|
+
|
|
28
|
+
# Compare one local Stimulus controller with the latest version
|
|
29
|
+
rails-blocks diff stimulus tooltip
|
|
30
|
+
|
|
31
|
+
# Update all free Stimulus controllers
|
|
32
|
+
rails-blocks update stimulus --all --free
|
|
33
|
+
|
|
34
|
+
# Log in before fetching Pro packages
|
|
35
|
+
rails-blocks login
|
|
36
|
+
|
|
37
|
+
# Install one Pro component as ViewComponent files
|
|
38
|
+
rails-blocks install dropdown --as view_component
|
|
39
|
+
|
|
40
|
+
# Install all Pro components as ViewComponent files
|
|
41
|
+
rails-blocks install --all --pro --as view_component
|
|
42
|
+
|
|
43
|
+
# Update all Pro-access Stimulus controllers
|
|
44
|
+
rails-blocks update stimulus --all --pro
|
|
45
|
+
```
|
data/exe/rails-blocks
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
require "json"
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "open3"
|
|
5
|
+
require "tempfile"
|
|
6
|
+
require "uri"
|
|
7
|
+
require "zip"
|
|
8
|
+
|
|
9
|
+
module RailsBlocks
|
|
10
|
+
class CLI
|
|
11
|
+
DownloadError = Class.new(StandardError)
|
|
12
|
+
|
|
13
|
+
DEFAULT_REGISTRY_URL = "https://raw.githubusercontent.com/Rails-Blocks/components/main/registry.json".freeze
|
|
14
|
+
DEFAULT_API_URL = "https://railsblocks.com".freeze
|
|
15
|
+
|
|
16
|
+
def initialize(argv)
|
|
17
|
+
@argv = argv.dup
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def run
|
|
21
|
+
command = @argv.shift
|
|
22
|
+
|
|
23
|
+
case command
|
|
24
|
+
when "list" then list
|
|
25
|
+
when "search" then search(@argv.join(" "))
|
|
26
|
+
when "show" then show(@argv.shift)
|
|
27
|
+
when "docs" then docs(@argv.shift)
|
|
28
|
+
when "examples" then examples(@argv.shift)
|
|
29
|
+
when "install" then install(@argv.shift)
|
|
30
|
+
when "diff" then diff(@argv.shift)
|
|
31
|
+
when "update" then update(@argv.shift)
|
|
32
|
+
when "login" then login
|
|
33
|
+
when "logout" then logout
|
|
34
|
+
when "whoami" then whoami
|
|
35
|
+
when "agents" then agents
|
|
36
|
+
when "mcp" then mcp
|
|
37
|
+
when "doctor" then doctor
|
|
38
|
+
else help
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
attr_reader :argv
|
|
45
|
+
|
|
46
|
+
def list
|
|
47
|
+
components.each do |component|
|
|
48
|
+
next unless include_component?(component)
|
|
49
|
+
|
|
50
|
+
puts "#{component['slug']}#{component['pro'] ? ' (pro)' : ''} - #{component['title']}"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def search(query)
|
|
55
|
+
abort "Usage: rails-blocks search QUERY" if query.to_s.strip.empty?
|
|
56
|
+
|
|
57
|
+
components.select { |component| component["slug"].include?(query.tr("-", "_")) || component["title"].downcase.include?(query.downcase) }.each do |component|
|
|
58
|
+
puts "#{component['slug']}#{component['pro'] ? ' (pro)' : ''} - #{component['title']}"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def show(slug)
|
|
63
|
+
component = find_component(slug)
|
|
64
|
+
puts JSON.pretty_generate(component)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def docs(slug)
|
|
68
|
+
component = find_component(slug)
|
|
69
|
+
if component["pro"]
|
|
70
|
+
puts api_get("/api/v1/components/#{component['slug']}/docs")["markdown"]
|
|
71
|
+
else
|
|
72
|
+
path = cached_component_file(component["slug"], "#{component['slug'].upcase}.md")
|
|
73
|
+
puts File.exist?(path) ? File.read(path) : "Docs are available at #{component['docs_path']}"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def examples(slug)
|
|
78
|
+
component = find_component(slug)
|
|
79
|
+
if component["pro"]
|
|
80
|
+
puts JSON.pretty_generate(api_get("/api/v1/components/#{component['slug']}/examples"))
|
|
81
|
+
else
|
|
82
|
+
Dir.glob(File.join(cache_dir, "components", component["slug"], "USAGE.*.erb")).each do |path|
|
|
83
|
+
puts "\n# #{File.basename(path)}\n\n"
|
|
84
|
+
puts File.read(path)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def install(slug)
|
|
90
|
+
implementation = package_implementation
|
|
91
|
+
dry_run = argv.include?("--dry-run")
|
|
92
|
+
|
|
93
|
+
return install_all(implementation, dry_run: dry_run) if slug == "--all"
|
|
94
|
+
|
|
95
|
+
abort "Usage: rails-blocks install COMPONENT [--as erb_template|view_component|partial] [--dry-run] [--force]" if slug.to_s.empty?
|
|
96
|
+
|
|
97
|
+
component = find_component(slug)
|
|
98
|
+
install_component(component, implementation, dry_run: dry_run)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def install_all(implementation, dry_run:)
|
|
102
|
+
selected_components = components.select { |component| include_component?(component) }
|
|
103
|
+
abort "No components matched the selected filters." if selected_components.empty?
|
|
104
|
+
|
|
105
|
+
failures = []
|
|
106
|
+
|
|
107
|
+
selected_components.each do |component|
|
|
108
|
+
puts "\nInstalling #{component['slug']}#{component['pro'] ? ' (pro)' : ''} as #{implementation}..."
|
|
109
|
+
install_component(component, implementation, dry_run: dry_run)
|
|
110
|
+
rescue StandardError => error
|
|
111
|
+
failures << [component["slug"], error.message]
|
|
112
|
+
warn "Failed to install #{component['slug']}: #{error.message}"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
puts "\nInstalled #{selected_components.size - failures.size} of #{selected_components.size} components."
|
|
116
|
+
return if failures.empty?
|
|
117
|
+
|
|
118
|
+
warn "Failures: #{failures.map(&:first).join(', ')}"
|
|
119
|
+
exit(1)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def install_component(component, implementation, dry_run:)
|
|
123
|
+
if component["pro"]
|
|
124
|
+
install_pro(component, implementation, dry_run: dry_run)
|
|
125
|
+
else
|
|
126
|
+
install_free(component, implementation, dry_run: dry_run)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def diff(target)
|
|
131
|
+
change_files(target, mode: :diff)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def update(target)
|
|
135
|
+
change_files(target, mode: :update)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def change_files(target, mode:)
|
|
139
|
+
abort "Usage: rails-blocks #{mode} COMPONENT|--all|stimulus [NAME|--all] [--as erb_template|view_component|partial] [--free|--pro] [--dry-run]" if target.to_s.empty?
|
|
140
|
+
|
|
141
|
+
if target == "stimulus"
|
|
142
|
+
change_stimulus_files(argv.shift, mode: mode)
|
|
143
|
+
elsif target == "--all"
|
|
144
|
+
change_all_components(mode: mode)
|
|
145
|
+
else
|
|
146
|
+
component = find_component(target)
|
|
147
|
+
change_component_files(component, mode: mode)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def change_all_components(mode:)
|
|
152
|
+
selected_components = components.select { |component| include_component?(component) }
|
|
153
|
+
abort "No components matched the selected filters." if selected_components.empty?
|
|
154
|
+
|
|
155
|
+
failures = []
|
|
156
|
+
|
|
157
|
+
selected_components.each do |component|
|
|
158
|
+
puts "\n#{mode_label(mode)} #{component['slug']}#{component['pro'] ? ' (pro)' : ''} as #{package_implementation}..."
|
|
159
|
+
change_component_files(component, mode: mode)
|
|
160
|
+
rescue StandardError => error
|
|
161
|
+
failures << [component["slug"], error.message]
|
|
162
|
+
warn "Failed to #{mode} #{component['slug']}: #{error.message}"
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
puts "\n#{mode_label(mode)} #{selected_components.size - failures.size} of #{selected_components.size} components."
|
|
166
|
+
return if failures.empty?
|
|
167
|
+
|
|
168
|
+
warn "Failures: #{failures.map(&:first).join(', ')}"
|
|
169
|
+
exit(1)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def change_component_files(component, mode:)
|
|
173
|
+
data = component["pro"] ? pro_package_data(component, package_implementation) : free_package_data(component, package_implementation)
|
|
174
|
+
process_zip(data, mode: mode) { |entry_name| Pathname.pwd.join(entry_name) }
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def change_stimulus_files(name, mode:)
|
|
178
|
+
abort "Usage: rails-blocks #{mode} stimulus NAME|--all [--free|--pro] [--dry-run]" if name.to_s.empty?
|
|
179
|
+
|
|
180
|
+
process_zip(stimulus_package_data, mode: mode) do |entry_name|
|
|
181
|
+
filename = File.basename(entry_name)
|
|
182
|
+
next unless name == "--all" || filename == stimulus_controller_filename(name)
|
|
183
|
+
|
|
184
|
+
Pathname.pwd.join("app", "javascript", "controllers", filename)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def login
|
|
189
|
+
email = option_value("--email")
|
|
190
|
+
body = { name: "Rails Blocks CLI", scopes: %w[cli mcp_read mcp_install], email: email }.compact
|
|
191
|
+
response = api_post("/api/v1/cli_sessions", body, auth: false)
|
|
192
|
+
puts "Open #{response['verification_uri']} and approve code #{response['code']}."
|
|
193
|
+
puts "Polling for approval..."
|
|
194
|
+
|
|
195
|
+
loop do
|
|
196
|
+
sleep(response["interval"] || 3)
|
|
197
|
+
status = api_get("/api/v1/cli_sessions/#{response['code']}", auth: false)
|
|
198
|
+
next unless status["status"] == "approved"
|
|
199
|
+
|
|
200
|
+
save_config("api_token" => status["token"], "api_url" => api_url)
|
|
201
|
+
puts "Logged in as Rails Blocks Pro token #{status['token_prefix']}."
|
|
202
|
+
break
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def logout
|
|
207
|
+
save_config(config.reject { |key, _| key == "api_token" })
|
|
208
|
+
puts "Logged out."
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def whoami
|
|
212
|
+
puts JSON.pretty_generate(api_get("/api/v1/me"))
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def agents
|
|
216
|
+
subcommand = argv.shift
|
|
217
|
+
case subcommand
|
|
218
|
+
when "create"
|
|
219
|
+
name = argv.shift || "Rails Blocks Agent"
|
|
220
|
+
response = api_post("/api/v1/cli_sessions", { name: name, scopes: %w[cli mcp_read mcp_install] }, auth: false)
|
|
221
|
+
puts "Approve #{response['verification_uri']} for #{name}."
|
|
222
|
+
else
|
|
223
|
+
puts "Agent tokens can be created and revoked from the Rails Blocks account page."
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def mcp
|
|
228
|
+
puts "Add @rails-blocks/mcp to your MCP client and point it at this rails-blocks executable."
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def doctor
|
|
232
|
+
puts "Registry: #{registry_url}"
|
|
233
|
+
puts "API: #{api_url}"
|
|
234
|
+
puts "Token: #{api_token ? 'present' : 'missing'}"
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def help
|
|
238
|
+
puts "Usage: rails-blocks [list|search|show|docs|examples|install|diff|update|login|logout|whoami|agents|mcp|doctor]"
|
|
239
|
+
puts " rails-blocks install COMPONENT [--as erb_template|view_component|partial] [--dry-run] [--force]"
|
|
240
|
+
puts " rails-blocks install --all [--free|--pro] [--as erb_template|view_component|partial] [--dry-run] [--force]"
|
|
241
|
+
puts " rails-blocks diff COMPONENT|--all [--free|--pro] [--as erb_template|view_component|partial]"
|
|
242
|
+
puts " rails-blocks update COMPONENT|--all [--free|--pro] [--as erb_template|view_component|partial] [--dry-run]"
|
|
243
|
+
puts " rails-blocks diff stimulus NAME|--all [--free|--pro]"
|
|
244
|
+
puts " rails-blocks update stimulus NAME|--all [--free|--pro] [--dry-run]"
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def install_free(component, implementation, dry_run:)
|
|
248
|
+
install_zip(free_package_data(component, implementation), dry_run: dry_run)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def install_pro(component, implementation, dry_run:)
|
|
252
|
+
install_zip(pro_package_data(component, implementation), dry_run: dry_run)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def free_package_data(component, implementation)
|
|
256
|
+
zip_url = component["package_url"].to_s.sub("#{component['slug']}.zip", "#{component['slug']}-#{implementation}.zip")
|
|
257
|
+
download(zip_url)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def pro_package_data(component, implementation)
|
|
261
|
+
response = api_get("/api/v1/components/#{component['slug']}/signed_package_url?implementation=#{implementation}")
|
|
262
|
+
download(response["url"], auth: true)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def stimulus_package_data
|
|
266
|
+
type = component_filter == :pro ? "pro" : "free"
|
|
267
|
+
if type == "free"
|
|
268
|
+
stimulus_artifact = registry["stimulus_controllers"]
|
|
269
|
+
return download(stimulus_artifact["package_url"]) if stimulus_artifact&.dig("package_url")
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
download(api_url_for("/api/v1/stimulus_controllers?type=#{type}"), auth: type == "pro")
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def install_zip(data, dry_run:)
|
|
276
|
+
zip_entries(data).each do |entry|
|
|
277
|
+
destination = Pathname.pwd.join(entry[:name])
|
|
278
|
+
puts "#{dry_run ? 'Would write' : 'Writing'} #{destination}"
|
|
279
|
+
next if dry_run
|
|
280
|
+
raise "#{destination} already exists. Use --force when overwrite support is enabled." if destination.exist? && !argv.include?("--force")
|
|
281
|
+
|
|
282
|
+
FileUtils.mkdir_p(destination.dirname)
|
|
283
|
+
File.binwrite(destination, entry[:content])
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def process_zip(data, mode:)
|
|
288
|
+
counts = { changed: 0, unchanged: 0, skipped: 0 }
|
|
289
|
+
|
|
290
|
+
zip_entries(data).each do |entry|
|
|
291
|
+
destination = yield(entry[:name])
|
|
292
|
+
unless destination
|
|
293
|
+
counts[:skipped] += 1
|
|
294
|
+
next
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
if mode == :diff
|
|
298
|
+
diff_file(destination, entry[:content], counts)
|
|
299
|
+
else
|
|
300
|
+
update_file(destination, entry[:content], counts)
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
puts "No matching files found." if counts.values.sum.zero? || (counts[:changed].zero? && counts[:unchanged].zero?)
|
|
305
|
+
counts
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def zip_entries(data)
|
|
309
|
+
entries = []
|
|
310
|
+
Zip::File.open_buffer(data) do |zip|
|
|
311
|
+
entries = zip.entries.reject(&:directory?).map do |entry|
|
|
312
|
+
{ name: entry.name, content: entry.get_input_stream.read }
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
entries
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def diff_file(destination, new_content, counts)
|
|
319
|
+
old_content = destination.exist? ? File.binread(destination) : ""
|
|
320
|
+
if old_content == new_content
|
|
321
|
+
counts[:unchanged] += 1
|
|
322
|
+
puts "No changes #{destination}"
|
|
323
|
+
return
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
counts[:changed] += 1
|
|
327
|
+
puts unified_diff(old_content, new_content, destination.exist? ? destination.to_s : "/dev/null", destination.to_s)
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def update_file(destination, new_content, counts)
|
|
331
|
+
old_content = destination.exist? ? File.binread(destination) : nil
|
|
332
|
+
if old_content == new_content
|
|
333
|
+
counts[:unchanged] += 1
|
|
334
|
+
puts "Unchanged #{destination}"
|
|
335
|
+
return
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
counts[:changed] += 1
|
|
339
|
+
puts "#{argv.include?('--dry-run') ? 'Would update' : old_content.nil? ? 'Writing' : 'Updating'} #{destination}"
|
|
340
|
+
return if argv.include?("--dry-run")
|
|
341
|
+
|
|
342
|
+
FileUtils.mkdir_p(destination.dirname)
|
|
343
|
+
File.binwrite(destination, new_content)
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def unified_diff(old_content, new_content, old_label, new_label)
|
|
347
|
+
old_file = Tempfile.new("rails-blocks-old")
|
|
348
|
+
new_file = Tempfile.new("rails-blocks-new")
|
|
349
|
+
old_file.binmode
|
|
350
|
+
new_file.binmode
|
|
351
|
+
old_file.write(old_content)
|
|
352
|
+
new_file.write(new_content)
|
|
353
|
+
old_file.flush
|
|
354
|
+
new_file.flush
|
|
355
|
+
|
|
356
|
+
output, = Open3.capture2e("diff", "-u", "--label", old_label, "--label", new_label, old_file.path, new_file.path)
|
|
357
|
+
output
|
|
358
|
+
ensure
|
|
359
|
+
old_file&.close!
|
|
360
|
+
new_file&.close!
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def stimulus_controller_filename(name)
|
|
364
|
+
normalized = name.to_s.tr("-", "_")
|
|
365
|
+
normalized.end_with?("_controller.js") ? normalized : "#{normalized}_controller.js"
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def mode_label(mode)
|
|
369
|
+
mode == :diff ? "Diffing" : "Updating"
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def components
|
|
373
|
+
registry.fetch("components")
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def find_component(slug)
|
|
377
|
+
normalized = slug.to_s.tr("-", "_")
|
|
378
|
+
components.find { |component| component["slug"] == normalized } || abort("Component not found: #{slug}")
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def include_component?(component)
|
|
382
|
+
case component_filter
|
|
383
|
+
when :pro then component["pro"]
|
|
384
|
+
when :free then !component["pro"]
|
|
385
|
+
else true
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
def component_filter
|
|
390
|
+
return :pro if argv.include?("--pro")
|
|
391
|
+
return :free if argv.include?("--free")
|
|
392
|
+
|
|
393
|
+
:all
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
def registry
|
|
397
|
+
@registry ||= JSON.parse(download(registry_url))
|
|
398
|
+
rescue DownloadError => error
|
|
399
|
+
abort "Could not load the Rails Blocks registry from #{registry_url}: #{error.message}"
|
|
400
|
+
rescue JSON::ParserError => error
|
|
401
|
+
abort "Could not parse the Rails Blocks registry from #{registry_url}: #{error.message}"
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def download(url, auth: false, redirects: 3)
|
|
405
|
+
uri = URI(url)
|
|
406
|
+
request = Net::HTTP::Get.new(uri)
|
|
407
|
+
request["Authorization"] = "Bearer #{api_token}" if auth && api_token
|
|
408
|
+
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") { |http| http.request(request) }
|
|
409
|
+
|
|
410
|
+
if response.is_a?(Net::HTTPRedirection) && response["location"] && redirects.positive?
|
|
411
|
+
return download(URI.join(url, response["location"]).to_s, redirects: redirects - 1)
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
raise DownloadError, "#{response.code} #{response.message}" unless response.is_a?(Net::HTTPSuccess)
|
|
415
|
+
|
|
416
|
+
response.body
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
def api_get(path, auth: true)
|
|
420
|
+
request_json(Net::HTTP::Get, path, auth: auth)
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def api_post(path, body, auth: true)
|
|
424
|
+
request_json(Net::HTTP::Post, path, body: body, auth: auth)
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
def request_json(method, path, body: nil, auth: true)
|
|
428
|
+
uri = URI(api_url_for(path))
|
|
429
|
+
request = method.new(uri)
|
|
430
|
+
request["Content-Type"] = "application/json"
|
|
431
|
+
request["Authorization"] = "Bearer #{api_token}" if auth && api_token
|
|
432
|
+
request.body = JSON.generate(body) if body
|
|
433
|
+
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") { |http| http.request(request) }
|
|
434
|
+
abort(response.body) unless response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPCreated)
|
|
435
|
+
|
|
436
|
+
JSON.parse(response.body)
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
def api_url_for(path)
|
|
440
|
+
return path if path.to_s.match?(/\Ahttps?:\/\//)
|
|
441
|
+
|
|
442
|
+
URI.join(api_url, path).to_s
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
def cached_component_file(slug, filename)
|
|
446
|
+
File.join(cache_dir, "components", slug, filename)
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
def option_value(name)
|
|
450
|
+
index = argv.index(name)
|
|
451
|
+
index ? argv[index + 1] : nil
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
def package_implementation
|
|
455
|
+
implementation = option_value("--as") || "erb_template"
|
|
456
|
+
return "erb_template" if implementation == "partial"
|
|
457
|
+
return implementation if %w[erb_template view_component].include?(implementation)
|
|
458
|
+
|
|
459
|
+
abort "Unsupported implementation: #{implementation}. Use erb_template, view_component, or partial."
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
def registry_url
|
|
463
|
+
ENV.fetch("RAILS_BLOCKS_REGISTRY_URL", config.fetch("registry_url", DEFAULT_REGISTRY_URL))
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
def api_url
|
|
467
|
+
ENV.fetch("RAILS_BLOCKS_API_URL", config.fetch("api_url", DEFAULT_API_URL))
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
def api_token
|
|
471
|
+
ENV["RAILS_BLOCKS_API_TOKEN"] || config["api_token"]
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
def config
|
|
475
|
+
@config ||= File.exist?(config_path) ? JSON.parse(File.read(config_path)) : {}
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
def save_config(values)
|
|
479
|
+
FileUtils.mkdir_p(File.dirname(config_path))
|
|
480
|
+
File.write(config_path, JSON.pretty_generate(values))
|
|
481
|
+
@config = values
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
def config_path
|
|
485
|
+
File.expand_path("~/.rails_blocks/config.json")
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
def cache_dir
|
|
489
|
+
File.expand_path("~/.rails_blocks/cache")
|
|
490
|
+
end
|
|
491
|
+
end
|
|
492
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rails-blocks-cli
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Rails Blocks
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rubyzip
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.3'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '2.3'
|
|
26
|
+
executables:
|
|
27
|
+
- rails-blocks
|
|
28
|
+
extensions: []
|
|
29
|
+
extra_rdoc_files: []
|
|
30
|
+
files:
|
|
31
|
+
- README.md
|
|
32
|
+
- exe/rails-blocks
|
|
33
|
+
- lib/rails_blocks/cli.rb
|
|
34
|
+
homepage: https://railsblocks.com/docs/cli
|
|
35
|
+
licenses:
|
|
36
|
+
- MIT
|
|
37
|
+
metadata:
|
|
38
|
+
rubygems_mfa_required: 'true'
|
|
39
|
+
rdoc_options: []
|
|
40
|
+
require_paths:
|
|
41
|
+
- lib
|
|
42
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '3.2'
|
|
47
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
48
|
+
requirements:
|
|
49
|
+
- - ">="
|
|
50
|
+
- !ruby/object:Gem::Version
|
|
51
|
+
version: '0'
|
|
52
|
+
requirements: []
|
|
53
|
+
rubygems_version: 4.0.3
|
|
54
|
+
specification_version: 4
|
|
55
|
+
summary: Install Rails Blocks components from the command line
|
|
56
|
+
test_files: []
|