rails-blocks-cli 0.1.0 → 0.1.1

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 +155 -28
  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: c4f132c448c8e065b701c21e0e5b777d24fea8ab32c32870f51c84d4a68626cc
4
+ data.tar.gz: 4e703d0337c0a8504773d803afef6b83b6cf6a62ec9dca8a2d048fcd4175bec0
5
5
  SHA512:
6
- metadata.gz: 1ea4fe353fbe84f63aa493a9040b4ac51c736f47715e4be550912b2c307c4a188cf727b2f7c7522969954f94d2bc01a42b1909cca99c2807cdbf13dcdc68415e
7
- data.tar.gz: a9f560d47c9f4dae1ea9dfa3b14f43fa49287913a9bbb787ab361ae84411a8b06d3da7762898de4c4d05c63a9aa091f046426188c5f1f550567c3f38408792d2
6
+ metadata.gz: e616247429678b871ce785aa979b2b5922deaaa153b94f0fefeec5261fa8791318a8938ae67c11a602db1a48f884e794334f3df25dac0a82c2be8fa667ab671a
7
+ data.tar.gz: d8271aef17019e1244f64fb098bdfbd35ce975f16f5b989143a5ba2df72f4946c121a8dd4e9da48a267bc45c78cb3999e83411f385b9302ee330775b3051ef77
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.
@@ -2,6 +2,7 @@ require "fileutils"
2
2
  require "json"
3
3
  require "net/http"
4
4
  require "open3"
5
+ require "pathname"
5
6
  require "tempfile"
6
7
  require "uri"
7
8
  require "zip"
@@ -44,18 +45,22 @@ module RailsBlocks
44
45
  attr_reader :argv
45
46
 
46
47
  def list
47
- components.each do |component|
48
- next unless include_component?(component)
48
+ selected = components.select { |component| include_component?(component) }
49
+ width = selected.map { |component| component_label(component).length }.max || 0
49
50
 
50
- puts "#{component['slug']}#{component['pro'] ? ' (pro)' : ''} - #{component['title']}"
51
+ selected.each do |component|
52
+ puts "#{component_label(component).ljust(width)} #{muted('-')} #{component['title']}"
51
53
  end
52
54
  end
53
55
 
54
56
  def search(query)
55
57
  abort "Usage: rails-blocks search QUERY" if query.to_s.strip.empty?
56
58
 
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
+ selected = components.select { |component| component["slug"].include?(query.tr("-", "_")) || component["title"].downcase.include?(query.downcase) }
60
+ width = selected.map { |component| component_label(component).length }.max || 0
61
+
62
+ selected.each do |component|
63
+ puts "#{component_label(component).ljust(width)} #{muted('-')} #{component['title']}"
59
64
  end
60
65
  end
61
66
 
@@ -65,12 +70,22 @@ module RailsBlocks
65
70
  end
66
71
 
67
72
  def docs(slug)
73
+ return docs_usage if slug.to_s.strip.empty?
74
+
68
75
  component = find_component(slug)
69
76
  if component["pro"]
70
77
  puts api_get("/api/v1/components/#{component['slug']}/docs")["markdown"]
71
78
  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']}"
79
+ begin
80
+ path = cached_component_file(component["slug"], "#{component['slug'].upcase}.md")
81
+ if File.exist?(path)
82
+ puts File.read(path)
83
+ else
84
+ puts download(component["public_markdown_url"] || markdown_docs_url(component))
85
+ end
86
+ rescue DownloadError
87
+ docs_links(component)
88
+ end
74
89
  end
75
90
  end
76
91
 
@@ -87,12 +102,12 @@ module RailsBlocks
87
102
  end
88
103
 
89
104
  def install(slug)
90
- implementation = package_implementation
105
+ implementation = package_implementation(prompt: true)
91
106
  dry_run = argv.include?("--dry-run")
92
107
 
93
108
  return install_all(implementation, dry_run: dry_run) if slug == "--all"
94
109
 
95
- abort "Usage: rails-blocks install COMPONENT [--as erb_template|view_component|partial] [--dry-run] [--force]" if slug.to_s.empty?
110
+ 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
111
 
97
112
  component = find_component(slug)
98
113
  install_component(component, implementation, dry_run: dry_run)
@@ -105,7 +120,7 @@ module RailsBlocks
105
120
  failures = []
106
121
 
107
122
  selected_components.each do |component|
108
- puts "\nInstalling #{component['slug']}#{component['pro'] ? ' (pro)' : ''} as #{implementation}..."
123
+ say_heading("Installing #{component['slug']}#{component['pro'] ? ' (pro)' : ''} as #{implementation}")
109
124
  install_component(component, implementation, dry_run: dry_run)
110
125
  rescue StandardError => error
111
126
  failures << [component["slug"], error.message]
@@ -125,6 +140,8 @@ module RailsBlocks
125
140
  else
126
141
  install_free(component, implementation, dry_run: dry_run)
127
142
  end
143
+
144
+ install_component_stimulus(component, dry_run: dry_run)
128
145
  end
129
146
 
130
147
  def diff(target)
@@ -171,7 +188,7 @@ module RailsBlocks
171
188
 
172
189
  def change_component_files(component, mode:)
173
190
  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) }
191
+ process_zip(data, mode: mode) { |entry_name| component_destination(entry_name, package_implementation) }
175
192
  end
176
193
 
177
194
  def change_stimulus_files(name, mode:)
@@ -229,27 +246,32 @@ module RailsBlocks
229
246
  end
230
247
 
231
248
  def doctor
232
- puts "Registry: #{registry_url}"
233
- puts "API: #{api_url}"
234
- puts "Token: #{api_token ? 'present' : 'missing'}"
249
+ say_heading("Rails Blocks CLI")
250
+ puts "Registry #{muted(registry_url)}"
251
+ puts "API #{muted(api_url)}"
252
+ puts "Token #{api_token ? success('present') : 'missing'}"
235
253
  end
236
254
 
237
255
  def help
238
256
  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]"
257
+ puts " rails-blocks install COMPONENT [--as erb_template|view_component|partial] [--path PATH] [--stimulus-path PATH] [--dry-run] [--force]"
258
+ puts " rails-blocks install --all [--free|--pro] [--as erb_template|view_component|partial] [--path PATH] [--stimulus-path PATH] [--dry-run] [--force]"
259
+ puts " rails-blocks diff COMPONENT|--all [--free|--pro] [--as erb_template|view_component|partial] [--path PATH]"
260
+ puts " rails-blocks update COMPONENT|--all [--free|--pro] [--as erb_template|view_component|partial] [--path PATH] [--dry-run]"
243
261
  puts " rails-blocks diff stimulus NAME|--all [--free|--pro]"
244
262
  puts " rails-blocks update stimulus NAME|--all [--free|--pro] [--dry-run]"
245
263
  end
246
264
 
247
265
  def install_free(component, implementation, dry_run:)
248
- install_zip(free_package_data(component, implementation), dry_run: dry_run)
266
+ install_zip(free_package_data(component, implementation), dry_run: dry_run) do |entry_name|
267
+ component_destination(entry_name, implementation)
268
+ end
249
269
  end
250
270
 
251
271
  def install_pro(component, implementation, dry_run:)
252
- install_zip(pro_package_data(component, implementation), dry_run: dry_run)
272
+ install_zip(pro_package_data(component, implementation), dry_run: dry_run) do |entry_name|
273
+ component_destination(entry_name, implementation)
274
+ end
253
275
  end
254
276
 
255
277
  def free_package_data(component, implementation)
@@ -272,18 +294,104 @@ module RailsBlocks
272
294
  download(api_url_for("/api/v1/stimulus_controllers?type=#{type}"), auth: type == "pro")
273
295
  end
274
296
 
297
+ def component_stimulus_package_data(component)
298
+ if component["stimulus_package_url"]
299
+ download(component["stimulus_package_url"])
300
+ else
301
+ stimulus_package_data
302
+ end
303
+ end
304
+
275
305
  def install_zip(data, dry_run:)
276
306
  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")
307
+ destination = block_given? ? yield(entry[:name]) : Pathname.pwd.join(entry[:name])
308
+ write_file(destination, entry[:content], dry_run: dry_run)
309
+ end
310
+ end
311
+
312
+ def install_component_stimulus(component, dry_run:)
313
+ controllers = Array(component["required_stimulus_controllers"])
314
+ return if controllers.empty?
315
+
316
+ say_heading("Stimulus controllers")
317
+ zip_entries(component_stimulus_package_data(component)).each do |entry|
318
+ filename = File.basename(entry[:name])
319
+ next unless controllers.include?(filename)
281
320
 
282
- FileUtils.mkdir_p(destination.dirname)
283
- File.binwrite(destination, entry[:content])
321
+ destination = stimulus_destination(filename)
322
+ if destination.exist? && !argv.include?("--force")
323
+ puts "#{muted('Skip')} #{destination} #{muted('(already exists)')}"
324
+ next
325
+ end
326
+
327
+ write_file(destination, entry[:content], dry_run: dry_run)
284
328
  end
285
329
  end
286
330
 
331
+ def write_file(destination, content, dry_run:)
332
+ puts "#{dry_run ? muted('Would write') : success('Writing')} #{destination}"
333
+ return if dry_run
334
+
335
+ raise "#{destination} already exists. Use --force to overwrite it." if destination.exist? && !argv.include?("--force")
336
+
337
+ FileUtils.mkdir_p(destination.dirname)
338
+ File.binwrite(destination, content)
339
+ end
340
+
341
+ def component_destination(entry_name, implementation)
342
+ root = Pathname.pwd.join(option_value("--path") || default_component_path(implementation))
343
+ root.join(entry_name)
344
+ end
345
+
346
+ def stimulus_destination(filename)
347
+ Pathname.pwd.join(option_value("--stimulus-path") || "app/javascript/controllers", filename)
348
+ end
349
+
350
+ def default_component_path(implementation)
351
+ implementation == "view_component" ? "app/components" : "app/views/shared"
352
+ end
353
+
354
+ def docs_usage
355
+ puts "Usage: rails-blocks docs COMPONENT"
356
+ puts "Example: rails-blocks docs accordion"
357
+ end
358
+
359
+ def docs_links(component)
360
+ puts "Docs for #{component['title']}:"
361
+ puts " Web: #{web_docs_url(component)}"
362
+ puts " Markdown: #{markdown_docs_url(component)}"
363
+ end
364
+
365
+ def web_docs_url(component)
366
+ component["docs_url"] || "https://railsblocks.com/docs/#{component['slug'].tr('_', '-')}"
367
+ end
368
+
369
+ def markdown_docs_url(component)
370
+ component["markdown_url"] || "https://railsblocks.com/docs/#{component['slug'].tr('_', '-')}.md"
371
+ end
372
+
373
+ def say_heading(message)
374
+ puts "\n#{bold(message)}"
375
+ end
376
+
377
+ def success(message)
378
+ color(message, 32)
379
+ end
380
+
381
+ def muted(message)
382
+ color(message, 2)
383
+ end
384
+
385
+ def bold(message)
386
+ color(message, 1)
387
+ end
388
+
389
+ def color(message, code)
390
+ return message unless $stdout.tty? && ENV["NO_COLOR"].to_s.empty?
391
+
392
+ "\e[#{code}m#{message}\e[0m"
393
+ end
394
+
287
395
  def process_zip(data, mode:)
288
396
  counts = { changed: 0, unchanged: 0, skipped: 0 }
289
397
 
@@ -386,6 +494,10 @@ module RailsBlocks
386
494
  end
387
495
  end
388
496
 
497
+ def component_label(component)
498
+ component["pro"] ? "#{component['slug']} (pro)" : component["slug"]
499
+ end
500
+
389
501
  def component_filter
390
502
  return :pro if argv.include?("--pro")
391
503
  return :free if argv.include?("--free")
@@ -451,14 +563,29 @@ module RailsBlocks
451
563
  index ? argv[index + 1] : nil
452
564
  end
453
565
 
454
- def package_implementation
455
- implementation = option_value("--as") || "erb_template"
566
+ def package_implementation(prompt: false)
567
+ explicit = option_value("--as")
568
+ implementation = explicit || (prompt ? prompt_for_implementation : "erb_template")
456
569
  return "erb_template" if implementation == "partial"
457
570
  return implementation if %w[erb_template view_component].include?(implementation)
458
571
 
459
572
  abort "Unsupported implementation: #{implementation}. Use erb_template, view_component, or partial."
460
573
  end
461
574
 
575
+ def prompt_for_implementation
576
+ return "erb_template" unless $stdin.tty? && $stdout.tty?
577
+
578
+ puts bold("Choose an install format")
579
+ puts " 1. ERB partials #{muted('app/views/shared/<component>/')}"
580
+ puts " 2. ViewComponent #{muted('app/components/<component>/')}"
581
+ print "Select an option [1]: "
582
+
583
+ case $stdin.gets&.strip
584
+ when "2", "view_component" then "view_component"
585
+ else "erb_template"
586
+ end
587
+ end
588
+
462
589
  def registry_url
463
590
  ENV.fetch("RAILS_BLOCKS_REGISTRY_URL", config.fetch("registry_url", DEFAULT_REGISTRY_URL))
464
591
  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.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rails Blocks