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.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +8 -0
  3. data/lib/rails_blocks/cli.rb +181 -44
  4. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a208908dfd35e07556c86dc68f85642d7f0b4428927f370898fa91611155881f
4
- data.tar.gz: ea4678983d9e63e30a99450b665c3ec4be126e4b72ca1e616f0953e497345561
3
+ metadata.gz: 63dd443055d6116deb6a9945e9aa025d6cee699603ac48278dc56d541c567e58
4
+ data.tar.gz: f8f4993d4a4b63083b8644040e02bf35653cefcbb04ce9f03d319ef3820dacde
5
5
  SHA512:
6
- metadata.gz: 1ea4fe353fbe84f63aa493a9040b4ac51c736f47715e4be550912b2c307c4a188cf727b2f7c7522969954f94d2bc01a42b1909cca99c2807cdbf13dcdc68415e
7
- data.tar.gz: a9f560d47c9f4dae1ea9dfa3b14f43fa49287913a9bbb787ab361ae84411a8b06d3da7762898de4c4d05c63a9aa091f046426188c5f1f550567c3f38408792d2
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.
@@ -44,18 +44,22 @@ module RailsBlocks
44
44
  attr_reader :argv
45
45
 
46
46
  def list
47
- components.each do |component|
48
- next unless include_component?(component)
47
+ selected = components.select { |component| include_component?(component) }
48
+ width = selected.map { |component| component_label(component).length }.max || 0
49
49
 
50
- puts "#{component['slug']}#{component['pro'] ? ' (pro)' : ''} - #{component['title']}"
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) }.each do |component|
58
- puts "#{component['slug']}#{component['pro'] ? ' (pro)' : ''} - #{component['title']}"
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
- 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']}"
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
- puts "\nInstalling #{component['slug']}#{component['pro'] ? ' (pro)' : ''} as #{implementation}..."
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 => error
111
- failures << [component["slug"], error.message]
112
- warn "Failed to install #{component['slug']}: #{error.message}"
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']}#{component['pro'] ? ' (pro)' : ''} as #{package_implementation}..."
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 => error
161
- failures << [component["slug"], error.message]
162
- warn "Failed to #{mode} #{component['slug']}: #{error.message}"
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| Pathname.pwd.join(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
- puts "Registry: #{registry_url}"
233
- puts "API: #{api_url}"
234
- puts "Token: #{api_token ? 'present' : 'missing'}"
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
- 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")
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
- FileUtils.mkdir_p(destination.dirname)
283
- File.binwrite(destination, entry[:content])
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.to_s : "/dev/null", destination.to_s)
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') ? 'Would update' : old_content.nil? ? 'Writing' : 'Updating'} #{destination}"
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 => 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}"
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?(/\Ahttps?:\/\//)
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
- implementation = option_value("--as") || "erb_template"
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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-blocks-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rails Blocks