rails-blocks-cli 0.1.0 → 0.1.2
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 +4 -4
- data/README.md +8 -0
- data/lib/rails_blocks/cli.rb +181 -44
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 63dd443055d6116deb6a9945e9aa025d6cee699603ac48278dc56d541c567e58
|
|
4
|
+
data.tar.gz: f8f4993d4a4b63083b8644040e02bf35653cefcbb04ce9f03d319ef3820dacde
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ae54a971c219716f16a227a27185078bc00dd33e94a6eb5d858a4e665e653b9ea921485cb3b334e36610d324fa300d8eb691bb50eb89a53d1b0fa18327b53fd6
|
|
7
|
+
data.tar.gz: 397f72909bbd30ad2e94991e59c34a25ac4e3d3f409062a3dbac3044af7bfb7ada4f22f54c0e6bab7236b446744d09c633bda996def8cecfa07c4d39ccba17af
|
data/README.md
CHANGED
|
@@ -16,6 +16,12 @@ rails-blocks docs accordion
|
|
|
16
16
|
# Install one component as shared ERB partials
|
|
17
17
|
rails-blocks install accordion --as erb_template
|
|
18
18
|
|
|
19
|
+
# Install interactively and let the CLI ask for ERB partials or ViewComponent
|
|
20
|
+
rails-blocks install accordion
|
|
21
|
+
|
|
22
|
+
# Override the component destination
|
|
23
|
+
rails-blocks install accordion --as erb_template --path app/views/components
|
|
24
|
+
|
|
19
25
|
# Preview installing all free components as shared partials
|
|
20
26
|
rails-blocks install --all --free --as partial --dry-run
|
|
21
27
|
|
|
@@ -43,3 +49,5 @@ rails-blocks install --all --pro --as view_component
|
|
|
43
49
|
# Update all Pro-access Stimulus controllers
|
|
44
50
|
rails-blocks update stimulus --all --pro
|
|
45
51
|
```
|
|
52
|
+
|
|
53
|
+
Default install paths are `app/views/shared/<component>/` for ERB partials, `app/components/<component>/` for ViewComponents, and `app/javascript/controllers/` for required Stimulus controllers. Component installs automatically add missing required Stimulus controllers.
|
data/lib/rails_blocks/cli.rb
CHANGED
|
@@ -44,18 +44,22 @@ module RailsBlocks
|
|
|
44
44
|
attr_reader :argv
|
|
45
45
|
|
|
46
46
|
def list
|
|
47
|
-
components.
|
|
48
|
-
|
|
47
|
+
selected = components.select { |component| include_component?(component) }
|
|
48
|
+
width = selected.map { |component| component_label(component).length }.max || 0
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
selected.each do |component|
|
|
51
|
+
puts "#{component_label(component).ljust(width)} #{muted('-')} #{component['title']}"
|
|
51
52
|
end
|
|
52
53
|
end
|
|
53
54
|
|
|
54
55
|
def search(query)
|
|
55
56
|
abort "Usage: rails-blocks search QUERY" if query.to_s.strip.empty?
|
|
56
57
|
|
|
57
|
-
components.select { |component| component["slug"].include?(query.tr("-", "_")) || component["title"].downcase.include?(query.downcase) }
|
|
58
|
-
|
|
58
|
+
selected = components.select { |component| component["slug"].include?(query.tr("-", "_")) || component["title"].downcase.include?(query.downcase) }
|
|
59
|
+
width = selected.map { |component| component_label(component).length }.max || 0
|
|
60
|
+
|
|
61
|
+
selected.each do |component|
|
|
62
|
+
puts "#{component_label(component).ljust(width)} #{muted('-')} #{component['title']}"
|
|
59
63
|
end
|
|
60
64
|
end
|
|
61
65
|
|
|
@@ -65,12 +69,22 @@ module RailsBlocks
|
|
|
65
69
|
end
|
|
66
70
|
|
|
67
71
|
def docs(slug)
|
|
72
|
+
return docs_usage if slug.to_s.strip.empty?
|
|
73
|
+
|
|
68
74
|
component = find_component(slug)
|
|
69
75
|
if component["pro"]
|
|
70
76
|
puts api_get("/api/v1/components/#{component['slug']}/docs")["markdown"]
|
|
71
77
|
else
|
|
72
|
-
|
|
73
|
-
|
|
78
|
+
begin
|
|
79
|
+
path = cached_component_file(component["slug"], "#{component['slug'].upcase}.md")
|
|
80
|
+
if File.exist?(path)
|
|
81
|
+
puts File.read(path)
|
|
82
|
+
else
|
|
83
|
+
puts download(component["public_markdown_url"] || markdown_docs_url(component))
|
|
84
|
+
end
|
|
85
|
+
rescue DownloadError
|
|
86
|
+
docs_links(component)
|
|
87
|
+
end
|
|
74
88
|
end
|
|
75
89
|
end
|
|
76
90
|
|
|
@@ -87,12 +101,12 @@ module RailsBlocks
|
|
|
87
101
|
end
|
|
88
102
|
|
|
89
103
|
def install(slug)
|
|
90
|
-
implementation = package_implementation
|
|
104
|
+
implementation = package_implementation(prompt: true)
|
|
91
105
|
dry_run = argv.include?("--dry-run")
|
|
92
106
|
|
|
93
107
|
return install_all(implementation, dry_run: dry_run) if slug == "--all"
|
|
94
108
|
|
|
95
|
-
abort "Usage: rails-blocks install COMPONENT [--as erb_template|view_component|partial] [--dry-run] [--force]" if slug.to_s.empty?
|
|
109
|
+
abort "Usage: rails-blocks install COMPONENT [--as erb_template|view_component|partial] [--path PATH] [--stimulus-path PATH] [--dry-run] [--force]" if slug.to_s.empty?
|
|
96
110
|
|
|
97
111
|
component = find_component(slug)
|
|
98
112
|
install_component(component, implementation, dry_run: dry_run)
|
|
@@ -105,11 +119,11 @@ module RailsBlocks
|
|
|
105
119
|
failures = []
|
|
106
120
|
|
|
107
121
|
selected_components.each do |component|
|
|
108
|
-
|
|
122
|
+
say_heading("Installing #{component['slug']}#{' (pro)' if component['pro']} as #{implementation}")
|
|
109
123
|
install_component(component, implementation, dry_run: dry_run)
|
|
110
|
-
rescue StandardError =>
|
|
111
|
-
failures << [component["slug"],
|
|
112
|
-
warn "Failed to install #{component['slug']}: #{
|
|
124
|
+
rescue StandardError => e
|
|
125
|
+
failures << [component["slug"], e.message]
|
|
126
|
+
warn "Failed to install #{component['slug']}: #{e.message}"
|
|
113
127
|
end
|
|
114
128
|
|
|
115
129
|
puts "\nInstalled #{selected_components.size - failures.size} of #{selected_components.size} components."
|
|
@@ -125,6 +139,8 @@ module RailsBlocks
|
|
|
125
139
|
else
|
|
126
140
|
install_free(component, implementation, dry_run: dry_run)
|
|
127
141
|
end
|
|
142
|
+
|
|
143
|
+
install_component_stimulus(component, dry_run: dry_run)
|
|
128
144
|
end
|
|
129
145
|
|
|
130
146
|
def diff(target)
|
|
@@ -155,11 +171,11 @@ module RailsBlocks
|
|
|
155
171
|
failures = []
|
|
156
172
|
|
|
157
173
|
selected_components.each do |component|
|
|
158
|
-
puts "\n#{mode_label(mode)} #{component['slug']}#{
|
|
174
|
+
puts "\n#{mode_label(mode)} #{component['slug']}#{' (pro)' if component['pro']} as #{package_implementation}..."
|
|
159
175
|
change_component_files(component, mode: mode)
|
|
160
|
-
rescue StandardError =>
|
|
161
|
-
failures << [component["slug"],
|
|
162
|
-
warn "Failed to #{mode} #{component['slug']}: #{
|
|
176
|
+
rescue StandardError => e
|
|
177
|
+
failures << [component["slug"], e.message]
|
|
178
|
+
warn "Failed to #{mode} #{component['slug']}: #{e.message}"
|
|
163
179
|
end
|
|
164
180
|
|
|
165
181
|
puts "\n#{mode_label(mode)} #{selected_components.size - failures.size} of #{selected_components.size} components."
|
|
@@ -171,7 +187,7 @@ module RailsBlocks
|
|
|
171
187
|
|
|
172
188
|
def change_component_files(component, mode:)
|
|
173
189
|
data = component["pro"] ? pro_package_data(component, package_implementation) : free_package_data(component, package_implementation)
|
|
174
|
-
process_zip(data, mode: mode) { |entry_name|
|
|
190
|
+
process_zip(data, mode: mode) { |entry_name| component_destination(entry_name, package_implementation) }
|
|
175
191
|
end
|
|
176
192
|
|
|
177
193
|
def change_stimulus_files(name, mode:)
|
|
@@ -229,27 +245,32 @@ module RailsBlocks
|
|
|
229
245
|
end
|
|
230
246
|
|
|
231
247
|
def doctor
|
|
232
|
-
|
|
233
|
-
puts "
|
|
234
|
-
puts "
|
|
248
|
+
say_heading("Rails Blocks CLI")
|
|
249
|
+
puts "Registry #{muted(registry_url)}"
|
|
250
|
+
puts "API #{muted(api_url)}"
|
|
251
|
+
puts "Token #{api_token ? success('present') : 'missing'}"
|
|
235
252
|
end
|
|
236
253
|
|
|
237
254
|
def help
|
|
238
255
|
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]"
|
|
256
|
+
puts " rails-blocks install COMPONENT [--as erb_template|view_component|partial] [--path PATH] [--stimulus-path PATH] [--dry-run] [--force]"
|
|
257
|
+
puts " rails-blocks install --all [--free|--pro] [--as erb_template|view_component|partial] [--path PATH] [--stimulus-path PATH] [--dry-run] [--force]"
|
|
258
|
+
puts " rails-blocks diff COMPONENT|--all [--free|--pro] [--as erb_template|view_component|partial] [--path PATH]"
|
|
259
|
+
puts " rails-blocks update COMPONENT|--all [--free|--pro] [--as erb_template|view_component|partial] [--path PATH] [--dry-run]"
|
|
243
260
|
puts " rails-blocks diff stimulus NAME|--all [--free|--pro]"
|
|
244
261
|
puts " rails-blocks update stimulus NAME|--all [--free|--pro] [--dry-run]"
|
|
245
262
|
end
|
|
246
263
|
|
|
247
264
|
def install_free(component, implementation, dry_run:)
|
|
248
|
-
install_zip(free_package_data(component, implementation), dry_run: dry_run)
|
|
265
|
+
install_zip(free_package_data(component, implementation), dry_run: dry_run) do |entry_name|
|
|
266
|
+
component_destination(entry_name, implementation)
|
|
267
|
+
end
|
|
249
268
|
end
|
|
250
269
|
|
|
251
270
|
def install_pro(component, implementation, dry_run:)
|
|
252
|
-
install_zip(pro_package_data(component, implementation), dry_run: dry_run)
|
|
271
|
+
install_zip(pro_package_data(component, implementation), dry_run: dry_run) do |entry_name|
|
|
272
|
+
component_destination(entry_name, implementation)
|
|
273
|
+
end
|
|
253
274
|
end
|
|
254
275
|
|
|
255
276
|
def free_package_data(component, implementation)
|
|
@@ -272,18 +293,104 @@ module RailsBlocks
|
|
|
272
293
|
download(api_url_for("/api/v1/stimulus_controllers?type=#{type}"), auth: type == "pro")
|
|
273
294
|
end
|
|
274
295
|
|
|
296
|
+
def component_stimulus_package_data(component)
|
|
297
|
+
if component["stimulus_package_url"]
|
|
298
|
+
download(component["stimulus_package_url"])
|
|
299
|
+
else
|
|
300
|
+
stimulus_package_data
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
275
304
|
def install_zip(data, dry_run:)
|
|
276
305
|
zip_entries(data).each do |entry|
|
|
277
|
-
destination = Pathname.pwd.join(entry[:name])
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
306
|
+
destination = block_given? ? yield(entry[:name]) : Pathname.pwd.join(entry[:name])
|
|
307
|
+
write_file(destination, entry[:content], dry_run: dry_run)
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def install_component_stimulus(component, dry_run:)
|
|
312
|
+
controllers = Array(component["required_stimulus_controllers"])
|
|
313
|
+
return if controllers.empty?
|
|
314
|
+
|
|
315
|
+
say_heading("Stimulus controllers")
|
|
316
|
+
zip_entries(component_stimulus_package_data(component)).each do |entry|
|
|
317
|
+
filename = File.basename(entry[:name])
|
|
318
|
+
next unless controllers.include?(filename)
|
|
281
319
|
|
|
282
|
-
|
|
283
|
-
|
|
320
|
+
destination = stimulus_destination(filename)
|
|
321
|
+
if destination.exist? && !argv.include?("--force")
|
|
322
|
+
puts "#{muted('Skip')} #{display_path(destination)} #{muted('(already exists)')}"
|
|
323
|
+
next
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
write_file(destination, entry[:content], dry_run: dry_run)
|
|
284
327
|
end
|
|
285
328
|
end
|
|
286
329
|
|
|
330
|
+
def write_file(destination, content, dry_run:)
|
|
331
|
+
puts "#{dry_run ? muted('Would write') : success('Writing')} #{display_path(destination)}"
|
|
332
|
+
return if dry_run
|
|
333
|
+
|
|
334
|
+
raise "#{destination} already exists. Use --force to overwrite it." if destination.exist? && !argv.include?("--force")
|
|
335
|
+
|
|
336
|
+
FileUtils.mkdir_p(destination.dirname)
|
|
337
|
+
File.binwrite(destination, content)
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def component_destination(entry_name, implementation)
|
|
341
|
+
root = Pathname.pwd.join(option_value("--path") || default_component_path(implementation))
|
|
342
|
+
root.join(entry_name)
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def stimulus_destination(filename)
|
|
346
|
+
Pathname.pwd.join(option_value("--stimulus-path") || "app/javascript/controllers", filename)
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def default_component_path(implementation)
|
|
350
|
+
implementation == "view_component" ? "app/components" : "app/views/shared"
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def docs_usage
|
|
354
|
+
puts "Usage: rails-blocks docs COMPONENT"
|
|
355
|
+
puts "Example: rails-blocks docs accordion"
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def docs_links(component)
|
|
359
|
+
puts "Docs for #{component['title']}:"
|
|
360
|
+
puts " Web: #{web_docs_url(component)}"
|
|
361
|
+
puts " Markdown: #{markdown_docs_url(component)}"
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def web_docs_url(component)
|
|
365
|
+
component["docs_url"] || "https://railsblocks.com/docs/#{component['slug'].tr('_', '-')}"
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def markdown_docs_url(component)
|
|
369
|
+
component["markdown_url"] || "https://railsblocks.com/docs/#{component['slug'].tr('_', '-')}.md"
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def say_heading(message)
|
|
373
|
+
puts "\n#{bold(message)}"
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def success(message)
|
|
377
|
+
color(message, 32)
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def muted(message)
|
|
381
|
+
color(message, 2)
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def bold(message)
|
|
385
|
+
color(message, 1)
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def color(message, code)
|
|
389
|
+
return message unless $stdout.tty? && ENV["NO_COLOR"].to_s.empty?
|
|
390
|
+
|
|
391
|
+
"\e[#{code}m#{message}\e[0m"
|
|
392
|
+
end
|
|
393
|
+
|
|
287
394
|
def process_zip(data, mode:)
|
|
288
395
|
counts = { changed: 0, unchanged: 0, skipped: 0 }
|
|
289
396
|
|
|
@@ -319,24 +426,28 @@ module RailsBlocks
|
|
|
319
426
|
old_content = destination.exist? ? File.binread(destination) : ""
|
|
320
427
|
if old_content == new_content
|
|
321
428
|
counts[:unchanged] += 1
|
|
322
|
-
puts "No changes #{destination}"
|
|
429
|
+
puts "No changes #{display_path(destination)}"
|
|
323
430
|
return
|
|
324
431
|
end
|
|
325
432
|
|
|
326
433
|
counts[:changed] += 1
|
|
327
|
-
puts unified_diff(old_content, new_content, destination.exist? ? destination
|
|
434
|
+
puts unified_diff(old_content, new_content, destination.exist? ? display_path(destination) : "/dev/null", display_path(destination))
|
|
328
435
|
end
|
|
329
436
|
|
|
330
437
|
def update_file(destination, new_content, counts)
|
|
331
438
|
old_content = destination.exist? ? File.binread(destination) : nil
|
|
332
439
|
if old_content == new_content
|
|
333
440
|
counts[:unchanged] += 1
|
|
334
|
-
puts "Unchanged #{destination}"
|
|
441
|
+
puts "Unchanged #{display_path(destination)}"
|
|
335
442
|
return
|
|
336
443
|
end
|
|
337
444
|
|
|
338
445
|
counts[:changed] += 1
|
|
339
|
-
puts "#{argv.include?('--dry-run')
|
|
446
|
+
puts "#{if argv.include?('--dry-run')
|
|
447
|
+
'Would update'
|
|
448
|
+
else
|
|
449
|
+
old_content.nil? ? 'Writing' : 'Updating'
|
|
450
|
+
end} #{display_path(destination)}"
|
|
340
451
|
return if argv.include?("--dry-run")
|
|
341
452
|
|
|
342
453
|
FileUtils.mkdir_p(destination.dirname)
|
|
@@ -369,6 +480,13 @@ module RailsBlocks
|
|
|
369
480
|
mode == :diff ? "Diffing" : "Updating"
|
|
370
481
|
end
|
|
371
482
|
|
|
483
|
+
def display_path(path)
|
|
484
|
+
pathname = Pathname.new(path)
|
|
485
|
+
pathname.relative_path_from(Pathname.pwd).to_s
|
|
486
|
+
rescue ArgumentError
|
|
487
|
+
pathname.to_s
|
|
488
|
+
end
|
|
489
|
+
|
|
372
490
|
def components
|
|
373
491
|
registry.fetch("components")
|
|
374
492
|
end
|
|
@@ -386,6 +504,10 @@ module RailsBlocks
|
|
|
386
504
|
end
|
|
387
505
|
end
|
|
388
506
|
|
|
507
|
+
def component_label(component)
|
|
508
|
+
component["pro"] ? "#{component['slug']} (pro)" : component["slug"]
|
|
509
|
+
end
|
|
510
|
+
|
|
389
511
|
def component_filter
|
|
390
512
|
return :pro if argv.include?("--pro")
|
|
391
513
|
return :free if argv.include?("--free")
|
|
@@ -395,10 +517,10 @@ module RailsBlocks
|
|
|
395
517
|
|
|
396
518
|
def registry
|
|
397
519
|
@registry ||= JSON.parse(download(registry_url))
|
|
398
|
-
rescue DownloadError =>
|
|
399
|
-
abort "Could not load the Rails Blocks registry from #{registry_url}: #{
|
|
400
|
-
rescue JSON::ParserError =>
|
|
401
|
-
abort "Could not parse the Rails Blocks registry from #{registry_url}: #{
|
|
520
|
+
rescue DownloadError => e
|
|
521
|
+
abort "Could not load the Rails Blocks registry from #{registry_url}: #{e.message}"
|
|
522
|
+
rescue JSON::ParserError => e
|
|
523
|
+
abort "Could not parse the Rails Blocks registry from #{registry_url}: #{e.message}"
|
|
402
524
|
end
|
|
403
525
|
|
|
404
526
|
def download(url, auth: false, redirects: 3)
|
|
@@ -437,7 +559,7 @@ module RailsBlocks
|
|
|
437
559
|
end
|
|
438
560
|
|
|
439
561
|
def api_url_for(path)
|
|
440
|
-
return path if path.to_s.match?(
|
|
562
|
+
return path if path.to_s.match?(%r{\Ahttps?://})
|
|
441
563
|
|
|
442
564
|
URI.join(api_url, path).to_s
|
|
443
565
|
end
|
|
@@ -451,14 +573,29 @@ module RailsBlocks
|
|
|
451
573
|
index ? argv[index + 1] : nil
|
|
452
574
|
end
|
|
453
575
|
|
|
454
|
-
def package_implementation
|
|
455
|
-
|
|
576
|
+
def package_implementation(prompt: false)
|
|
577
|
+
explicit = option_value("--as")
|
|
578
|
+
implementation = explicit || (prompt ? prompt_for_implementation : "erb_template")
|
|
456
579
|
return "erb_template" if implementation == "partial"
|
|
457
580
|
return implementation if %w[erb_template view_component].include?(implementation)
|
|
458
581
|
|
|
459
582
|
abort "Unsupported implementation: #{implementation}. Use erb_template, view_component, or partial."
|
|
460
583
|
end
|
|
461
584
|
|
|
585
|
+
def prompt_for_implementation
|
|
586
|
+
return "erb_template" unless $stdin.tty? && $stdout.tty?
|
|
587
|
+
|
|
588
|
+
puts bold("Choose an install format")
|
|
589
|
+
puts " 1. ERB partials #{muted('app/views/shared/<component>/')}"
|
|
590
|
+
puts " 2. ViewComponent #{muted('app/components/<component>/')}"
|
|
591
|
+
print "Select an option [1 by default]: "
|
|
592
|
+
|
|
593
|
+
case $stdin.gets&.strip
|
|
594
|
+
when "2", "view_component" then "view_component"
|
|
595
|
+
else "erb_template"
|
|
596
|
+
end
|
|
597
|
+
end
|
|
598
|
+
|
|
462
599
|
def registry_url
|
|
463
600
|
ENV.fetch("RAILS_BLOCKS_REGISTRY_URL", config.fetch("registry_url", DEFAULT_REGISTRY_URL))
|
|
464
601
|
end
|