clack 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.
data/lib/clack.rb ADDED
@@ -0,0 +1,576 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "clack/version"
4
+ require_relative "clack/symbols"
5
+ require_relative "clack/colors"
6
+ require_relative "clack/environment"
7
+ require_relative "clack/utils"
8
+ require_relative "clack/core/cursor"
9
+ require_relative "clack/core/settings"
10
+ require_relative "clack/core/key_reader"
11
+ require_relative "clack/core/prompt"
12
+ require_relative "clack/core/options_helper"
13
+ require_relative "clack/core/text_input_helper"
14
+ require_relative "clack/prompts/text"
15
+ require_relative "clack/prompts/password"
16
+ require_relative "clack/prompts/confirm"
17
+ require_relative "clack/prompts/select"
18
+ require_relative "clack/prompts/multiselect"
19
+ require_relative "clack/prompts/spinner"
20
+ require_relative "clack/prompts/autocomplete"
21
+ require_relative "clack/prompts/autocomplete_multiselect"
22
+ require_relative "clack/prompts/path"
23
+ require_relative "clack/prompts/progress"
24
+ require_relative "clack/prompts/select_key"
25
+ require_relative "clack/prompts/tasks"
26
+ require_relative "clack/prompts/group_multiselect"
27
+ require_relative "clack/log"
28
+ require_relative "clack/note"
29
+ require_relative "clack/box"
30
+ require_relative "clack/group"
31
+ require_relative "clack/stream"
32
+ require_relative "clack/task_log"
33
+ require_relative "clack/validators"
34
+
35
+ # Clack - Beautiful CLI prompts for Ruby
36
+ #
37
+ # A faithful Ruby port of @clack/prompts, bringing delightful terminal
38
+ # aesthetics to your Ruby projects.
39
+ #
40
+ # @example Basic usage
41
+ # Clack.intro "Welcome to my-app"
42
+ # name = Clack.text(message: "What's your name?")
43
+ # exit 1 if Clack.cancel?(name)
44
+ # Clack.outro "Nice to meet you, #{name}!"
45
+ #
46
+ # @example Using prompt groups
47
+ # result = Clack.group do |g|
48
+ # g.prompt(:name) { Clack.text(message: "Name?") }
49
+ # g.prompt(:confirm) { Clack.confirm(message: "Continue?") }
50
+ # end
51
+ #
52
+ # @see https://github.com/bombshell-dev/clack Original JavaScript library
53
+ module Clack
54
+ # Sentinel value returned when user cancels a prompt (Escape or Ctrl+C)
55
+ CANCEL = Object.new.tap { |o| o.define_singleton_method(:inspect) { "Clack::CANCEL" } }.freeze
56
+
57
+ class << self
58
+ # Check if a prompt result was cancelled by the user.
59
+ #
60
+ # @param value [Object] the result from a prompt
61
+ # @return [Boolean] true if the user cancelled
62
+ def cancel?(value)
63
+ value.equal?(CANCEL)
64
+ end
65
+ alias_method :cancelled?, :cancel?
66
+
67
+ # Check if cancelled and show cancel message if so.
68
+ # Useful for guard clauses in CLI scripts.
69
+ #
70
+ # @param value [Object] the result from a prompt
71
+ # @param message [String] message to display if cancelled
72
+ # @param output [IO] output stream
73
+ # @return [Boolean] true if cancelled
74
+ #
75
+ # @example Guard clause pattern
76
+ # name = Clack.text(message: "Name?")
77
+ # return if Clack.handle_cancel(name) # Shows "Cancelled" and returns true
78
+ #
79
+ # @example With custom message
80
+ # return if Clack.handle_cancel(name, "Aborted by user")
81
+ def handle_cancel(value, message = "Cancelled", output: $stdout)
82
+ return false unless cancel?(value)
83
+
84
+ cancel(message, output: output)
85
+ true
86
+ end
87
+
88
+ # Display an intro banner at the start of a CLI session.
89
+ #
90
+ # @param title [String, nil] optional title text
91
+ # @param output [IO] output stream (default: $stdout)
92
+ # @return [void]
93
+ def intro(title = nil, output: $stdout)
94
+ output.puts "#{Colors.gray(Symbols::S_BAR_START)} #{title}"
95
+ end
96
+
97
+ # Display an outro banner at the end of a CLI session.
98
+ #
99
+ # @param message [String, nil] optional closing message
100
+ # @param output [IO] output stream (default: $stdout)
101
+ # @return [void]
102
+ def outro(message = nil, output: $stdout)
103
+ output.puts Colors.gray(Symbols::S_BAR)
104
+ output.puts "#{Colors.gray(Symbols::S_BAR_END)} #{message}"
105
+ output.puts
106
+ end
107
+
108
+ # Display a cancellation message (typically after user presses Escape).
109
+ #
110
+ # @param message [String, nil] optional cancellation message
111
+ # @param output [IO] output stream (default: $stdout)
112
+ # @return [void]
113
+ def cancel(message = nil, output: $stdout)
114
+ output.puts Colors.gray(Symbols::S_BAR)
115
+ output.puts "#{Colors.gray(Symbols::S_BAR_END)} #{Colors.red(message)}"
116
+ output.puts
117
+ end
118
+
119
+ # Prompt for single-line text input.
120
+ #
121
+ # @param message [String] the prompt message
122
+ # @param placeholder [String, nil] dim text shown when input is empty
123
+ # @param default_value [String, nil] value used if submitted empty
124
+ # @param initial_value [String, nil] pre-filled editable text
125
+ # @param validate [Proc, nil] validation function returning error message or nil
126
+ # @return [String, CANCEL] user input or CANCEL if cancelled
127
+ def text(message:, **opts)
128
+ Prompts::Text.new(message:, **opts).run
129
+ end
130
+
131
+ # Prompt for password input (masked display).
132
+ #
133
+ # @param message [String] the prompt message
134
+ # @param mask [String] character to display for each input character (default: ▪)
135
+ # @param validate [Proc, nil] validation function
136
+ # @return [String, CANCEL] password or CANCEL if cancelled
137
+ def password(message:, **opts)
138
+ Prompts::Password.new(message:, **opts).run
139
+ end
140
+
141
+ # Prompt for yes/no confirmation.
142
+ #
143
+ # @param message [String] the prompt message
144
+ # @param active [String] label for "yes" option (default: "Yes")
145
+ # @param inactive [String] label for "no" option (default: "No")
146
+ # @param initial_value [Boolean] default selection (default: true)
147
+ # @return [Boolean, CANCEL] true/false or CANCEL if cancelled
148
+ def confirm(message:, **opts)
149
+ Prompts::Confirm.new(message:, **opts).run
150
+ end
151
+
152
+ # Prompt to select one option from a list.
153
+ #
154
+ # @param message [String] the prompt message
155
+ # @param options [Array<Hash, String>] list of options
156
+ # @param initial_value [Object, nil] value of initially selected option
157
+ # @param max_items [Integer, nil] max visible items (enables scrolling)
158
+ # @return [Object, CANCEL] selected value or CANCEL if cancelled
159
+ def select(message:, options:, **opts)
160
+ Prompts::Select.new(message:, options: options, **opts).run
161
+ end
162
+
163
+ # Prompt to select multiple options from a list.
164
+ #
165
+ # @param message [String] the prompt message
166
+ # @param options [Array<Hash, String>] list of options
167
+ # @param initial_values [Array, nil] initially selected values
168
+ # @param required [Boolean] require at least one selection (default: true)
169
+ # @param max_items [Integer, nil] max visible items (enables scrolling)
170
+ # @return [Array, CANCEL] selected values or CANCEL if cancelled
171
+ def multiselect(message:, options:, **opts)
172
+ Prompts::Multiselect.new(message:, options: options, **opts).run
173
+ end
174
+
175
+ # Create an animated spinner for async operations.
176
+ #
177
+ # @return [Prompts::Spinner] spinner instance (call #start, #stop, #error)
178
+ def spinner(**opts)
179
+ Prompts::Spinner.new(**opts)
180
+ end
181
+
182
+ # Run a block with a spinner, handling success/error automatically.
183
+ #
184
+ # @param message [String] initial spinner message
185
+ # @param success_message [String, nil] message on success (defaults to message)
186
+ # @param error_message [String, nil] message on error (defaults to exception message)
187
+ # @return [Object] the block's return value
188
+ # @raise [Exception] re-raises any exception from the block
189
+ #
190
+ # @example Basic usage
191
+ # result = Clack.spin("Installing dependencies...") { system("npm install") }
192
+ #
193
+ # @example With custom success message
194
+ # Clack.spin("Compiling...", success: "Build complete!") { build_project }
195
+ #
196
+ # @example Access spinner inside block
197
+ # Clack.spin("Working...") do |s|
198
+ # s.message "Step 1..."
199
+ # do_step_1
200
+ # s.message "Step 2..."
201
+ # do_step_2
202
+ # end
203
+ def spin(message, success: nil, error: nil, **opts)
204
+ s = spinner(**opts)
205
+ s.start(message)
206
+ begin
207
+ result = yield(s)
208
+ s.stop(success || message)
209
+ result
210
+ rescue => exception
211
+ s.error(error || exception.message)
212
+ raise
213
+ end
214
+ end
215
+
216
+ # Prompt with type-to-filter autocomplete.
217
+ #
218
+ # @param message [String] the prompt message
219
+ # @param options [Array<Hash, String>] list of options to filter
220
+ # @param placeholder [String, nil] placeholder text
221
+ # @return [Object, CANCEL] selected value or CANCEL if cancelled
222
+ def autocomplete(message:, options:, **opts)
223
+ Prompts::Autocomplete.new(message:, options: options, **opts).run
224
+ end
225
+
226
+ # Prompt with type-to-filter autocomplete and multiselect.
227
+ #
228
+ # @param message [String] the prompt message
229
+ # @param options [Array<Hash, String>] list of options to filter
230
+ # @param placeholder [String, nil] placeholder text
231
+ # @param required [Boolean] require at least one selection (default: true)
232
+ # @param initial_values [Array, nil] initially selected values
233
+ # @return [Array, CANCEL] selected values or CANCEL if cancelled
234
+ def autocomplete_multiselect(message:, options:, **opts)
235
+ Prompts::AutocompleteMultiselect.new(message:, options: options, **opts).run
236
+ end
237
+
238
+ # Prompt for file/directory path with filesystem navigation.
239
+ #
240
+ # @param message [String] the prompt message
241
+ # @param root [String] starting directory (default: ".")
242
+ # @param only_directories [Boolean] only show directories (default: false)
243
+ # @return [String, CANCEL] selected path or CANCEL if cancelled
244
+ def path(message:, **opts)
245
+ Prompts::Path.new(message:, **opts).run
246
+ end
247
+
248
+ # Create a progress bar for measurable operations.
249
+ #
250
+ # @param total [Integer] total number of steps
251
+ # @param message [String, nil] optional message
252
+ # @return [Prompts::Progress] progress instance (call #start, #advance, #stop)
253
+ def progress(total:, **opts)
254
+ Prompts::Progress.new(total: total, **opts)
255
+ end
256
+
257
+ # Prompt to select an option by pressing a key.
258
+ #
259
+ # @param message [String] the prompt message
260
+ # @param options [Array<Hash>] options with :value, :label, and :key
261
+ # @return [Object, CANCEL] selected value or CANCEL if cancelled
262
+ def select_key(message:, options:, **opts)
263
+ Prompts::SelectKey.new(message:, options: options, **opts).run
264
+ end
265
+
266
+ # Run multiple tasks with progress indicators.
267
+ #
268
+ # @param tasks [Array<Hash>] tasks with :title and :task (Proc)
269
+ # @return [Array<Hash>] task results
270
+ def tasks(tasks:, **opts)
271
+ Prompts::Tasks.new(tasks: tasks, **opts).run
272
+ end
273
+
274
+ # Prompt to select multiple options organized in groups.
275
+ #
276
+ # @param message [String] the prompt message
277
+ # @param options [Array<Hash>] groups with :label and :options
278
+ # @param initial_values [Array, nil] initially selected values
279
+ # @param required [Boolean] require at least one selection (default: true)
280
+ # @return [Array, CANCEL] selected values or CANCEL if cancelled
281
+ def group_multiselect(message:, options:, **opts)
282
+ Prompts::GroupMultiselect.new(message:, options: options, **opts).run
283
+ end
284
+
285
+ # Access the Log module for styled console output.
286
+ #
287
+ # @return [Module] the Log module
288
+ def log
289
+ Log
290
+ end
291
+
292
+ # Access the Stream module for streaming output.
293
+ #
294
+ # @return [Module] the Stream module
295
+ def stream
296
+ Stream
297
+ end
298
+
299
+ # Display a note box with optional title.
300
+ #
301
+ # @param message [String] the note content
302
+ # @param title [String, nil] optional title
303
+ # @return [void]
304
+ def note(message = "", title: nil, **opts)
305
+ Note.render(message, title: title, **opts)
306
+ end
307
+
308
+ # Display content in a customizable box
309
+ # @param message [String] the box content
310
+ # @param title [String] optional title
311
+ # @param content_align [:left, :center, :right] content alignment
312
+ # @param title_align [:left, :center, :right] title alignment
313
+ # @param width [Integer, :auto] box width
314
+ # @param rounded [Boolean] use rounded corners
315
+ # @return [void]
316
+ def box(message = "", title: "", **opts)
317
+ Box.render(message, title: title, **opts)
318
+ end
319
+
320
+ # Create a streaming task log that clears on success, shows on error.
321
+ # Useful for build output, npm install style streaming, etc.
322
+ #
323
+ # @param title [String] title displayed at the top
324
+ # @param limit [Integer, nil] max lines to show (older lines scroll out)
325
+ # @param retain_log [Boolean] keep full log history for display on error
326
+ # @return [TaskLog] task log instance
327
+ def task_log(title:, **opts)
328
+ TaskLog.new(title: title, **opts)
329
+ end
330
+
331
+ # Access global settings
332
+ # @return [Hash] Current configuration
333
+ # @see Core::Settings.update for modifying settings
334
+ def settings
335
+ Core::Settings.config
336
+ end
337
+
338
+ # Update global settings
339
+ # @param aliases [Hash, nil] Custom key to action mappings
340
+ # @param with_guide [Boolean, nil] Whether to show guide bars
341
+ # @return [Hash] Updated configuration
342
+ #
343
+ # @example Custom key bindings
344
+ # Clack.update_settings(aliases: { "y" => :enter, "n" => :cancel })
345
+ #
346
+ # @example Disable guide bars
347
+ # Clack.update_settings(with_guide: false)
348
+ def update_settings(**opts)
349
+ Core::Settings.update(**opts)
350
+ end
351
+
352
+ # Check if running in a CI environment
353
+ # @return [Boolean]
354
+ def ci?
355
+ Environment.ci?
356
+ end
357
+
358
+ # Check if running on Windows
359
+ # @return [Boolean]
360
+ def windows?
361
+ Environment.windows?
362
+ end
363
+
364
+ # Check if stdout is a TTY
365
+ # @param output [IO] Output stream to check
366
+ # @return [Boolean]
367
+ def tty?(output = $stdout)
368
+ Environment.tty?(output)
369
+ end
370
+
371
+ # Get terminal columns (width)
372
+ # @param output [IO] Output stream
373
+ # @param default [Integer] Default if detection fails
374
+ # @return [Integer]
375
+ def columns(output = $stdout, default: 80)
376
+ Environment.columns(output, default: default)
377
+ end
378
+
379
+ # Get terminal rows (height)
380
+ # @param output [IO] Output stream
381
+ # @param default [Integer] Default if detection fails
382
+ # @return [Integer]
383
+ def rows(output = $stdout, default: 24)
384
+ Environment.rows(output, default: default)
385
+ end
386
+
387
+ # :nocov:
388
+ # :reek:TooManyStatements :reek:NestedIterators :reek:UncommunicativeVariableName
389
+ # Demo - showcases all Clack features (interactive, tested manually)
390
+ def demo
391
+ intro "clack-demo"
392
+
393
+ result = group(on_cancel: ->(_) { cancel("Operation cancelled.") }) do |g|
394
+ g.prompt(:name) do
395
+ text(
396
+ message: "What is your project named?",
397
+ placeholder: "my-app",
398
+ validate: ->(v) { "Project name is required" if v.to_s.strip.empty? }
399
+ )
400
+ end
401
+
402
+ g.prompt(:directory) do |r|
403
+ text(
404
+ message: "Where should we create your project?",
405
+ initial_value: "./#{r[:name]}"
406
+ )
407
+ end
408
+
409
+ g.prompt(:template) do
410
+ select(
411
+ message: "Which template would you like to use?",
412
+ options: [
413
+ {value: "default", label: "Default", hint: "recommended"},
414
+ {value: "minimal", label: "Minimal", hint: "bare bones"},
415
+ {value: "api", label: "API Only", hint: "no frontend"},
416
+ {value: "full", label: "Full Stack", hint: "everything included"}
417
+ ]
418
+ )
419
+ end
420
+
421
+ g.prompt(:typescript) do
422
+ confirm(
423
+ message: "Would you like to use TypeScript?",
424
+ initial_value: true
425
+ )
426
+ end
427
+
428
+ g.prompt(:features) do
429
+ multiselect(
430
+ message: "Which features would you like to include?",
431
+ options: [
432
+ {value: "eslint", label: "ESLint", hint: "code linting"},
433
+ {value: "prettier", label: "Prettier", hint: "code formatting"},
434
+ {value: "tailwind", label: "Tailwind CSS", hint: "utility-first CSS"},
435
+ {value: "docker", label: "Docker", hint: "containerization"},
436
+ {value: "ci", label: "GitHub Actions", hint: "CI/CD pipeline"}
437
+ ],
438
+ initial_values: %w[eslint prettier],
439
+ required: false
440
+ )
441
+ end
442
+
443
+ g.prompt(:package_manager) do
444
+ select(
445
+ message: "Which package manager do you prefer?",
446
+ options: [
447
+ {value: "npm", label: "npm"},
448
+ {value: "yarn", label: "yarn"},
449
+ {value: "pnpm", label: "pnpm", hint: "recommended"},
450
+ {value: "bun", label: "bun", hint: "fast"}
451
+ ],
452
+ initial_value: "pnpm"
453
+ )
454
+ end
455
+ end
456
+
457
+ return if cancel?(result)
458
+
459
+ # Autocomplete prompt
460
+ color = autocomplete(
461
+ message: "Pick a theme color:",
462
+ options: %w[red orange yellow green blue indigo violet pink cyan magenta]
463
+ )
464
+ return if handle_cancel(color)
465
+
466
+ # Select key prompt (quick keyboard shortcuts)
467
+ action = select_key(
468
+ message: "What would you like to do first?",
469
+ options: [
470
+ {value: "dev", label: "Start dev server", key: "d"},
471
+ {value: "build", label: "Build for production", key: "b"},
472
+ {value: "test", label: "Run tests", key: "t"}
473
+ ]
474
+ )
475
+ return if handle_cancel(action)
476
+
477
+ # Path prompt
478
+ config_path = path(
479
+ message: "Select config directory:",
480
+ only_directories: true
481
+ )
482
+ return if handle_cancel(config_path)
483
+
484
+ # Group multiselect
485
+ stack = group_multiselect(
486
+ message: "Select additional integrations:",
487
+ options: [
488
+ {
489
+ label: "Frontend",
490
+ options: [
491
+ {value: "react", label: "React"},
492
+ {value: "vue", label: "Vue"},
493
+ {value: "svelte", label: "Svelte"}
494
+ ]
495
+ },
496
+ {
497
+ label: "Backend",
498
+ options: [
499
+ {value: "express", label: "Express"},
500
+ {value: "fastify", label: "Fastify"},
501
+ {value: "hono", label: "Hono"}
502
+ ]
503
+ },
504
+ {
505
+ label: "Database",
506
+ options: [
507
+ {value: "postgres", label: "PostgreSQL"},
508
+ {value: "mysql", label: "MySQL"},
509
+ {value: "sqlite", label: "SQLite"}
510
+ ]
511
+ }
512
+ ],
513
+ required: false
514
+ )
515
+ return if handle_cancel(stack)
516
+
517
+ # Progress bar
518
+ prog = progress(total: 100, message: "Downloading assets...")
519
+ prog.start
520
+ 20.times do
521
+ sleep 0.03
522
+ prog.advance(5)
523
+ end
524
+ prog.stop("Assets downloaded!")
525
+
526
+ # Tasks
527
+ tasks(tasks: [
528
+ {title: "Validating configuration", task: -> { sleep 0.3 }},
529
+ {title: "Generating types", task: -> { sleep 0.4 }},
530
+ {title: "Compiling assets", task: -> { sleep 0.3 }}
531
+ ])
532
+
533
+ # Spinner
534
+ s = spinner
535
+ s.start "Installing dependencies via #{result[:package_manager]}..."
536
+ sleep 1.0
537
+ s.message "Configuring #{result[:template]} template..."
538
+ sleep 0.6
539
+ s.stop "Project created successfully!"
540
+
541
+ # Summary
542
+ log.step "Project: #{result[:name]}"
543
+ log.step "Directory: #{result[:directory]}"
544
+ log.step "Template: #{result[:template]}"
545
+ log.step "TypeScript: #{result[:typescript] ? "Yes" : "No"}"
546
+ log.step "Features: #{result[:features].join(", ")}" unless result[:features].empty?
547
+ log.step "Color: #{color}"
548
+ log.step "Action: #{action}"
549
+ log.step "Config: #{config_path}"
550
+ log.step "Stack: #{stack.join(", ")}" unless stack.empty?
551
+
552
+ note <<~MSG, title: "Next steps"
553
+ cd #{result[:directory]}
554
+ #{result[:package_manager]} run dev
555
+ MSG
556
+
557
+ outro "Happy coding!"
558
+ end
559
+ # :nocov:
560
+ end
561
+ end
562
+
563
+ # Terminal cleanup on exit - show cursor if it was hidden
564
+ at_exit do
565
+ print "\e[?25h"
566
+ end
567
+
568
+ # Chain INT handler to restore cursor before passing to previous handler
569
+ previous_int_handler = trap("INT") do
570
+ print "\e[?25h"
571
+ case previous_int_handler
572
+ when Proc then previous_int_handler.call
573
+ when "DEFAULT", "SYSTEM_DEFAULT" then exit(130)
574
+ else exit(130)
575
+ end
576
+ end
metadata ADDED
@@ -0,0 +1,83 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: clack
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Steve Whittaker
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: Ruby port of Clack — effortlessly build beautiful command-line apps with
13
+ the modern, minimal aesthetic popularized by Vercel and Astro.
14
+ email:
15
+ - swhitt@gmail.com
16
+ executables:
17
+ - clack-demo
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - CHANGELOG.md
22
+ - LICENSE
23
+ - README.md
24
+ - exe/clack-demo
25
+ - lib/clack.rb
26
+ - lib/clack/box.rb
27
+ - lib/clack/colors.rb
28
+ - lib/clack/core/cursor.rb
29
+ - lib/clack/core/key_reader.rb
30
+ - lib/clack/core/options_helper.rb
31
+ - lib/clack/core/prompt.rb
32
+ - lib/clack/core/settings.rb
33
+ - lib/clack/core/text_input_helper.rb
34
+ - lib/clack/environment.rb
35
+ - lib/clack/group.rb
36
+ - lib/clack/log.rb
37
+ - lib/clack/note.rb
38
+ - lib/clack/prompts/autocomplete.rb
39
+ - lib/clack/prompts/autocomplete_multiselect.rb
40
+ - lib/clack/prompts/confirm.rb
41
+ - lib/clack/prompts/group_multiselect.rb
42
+ - lib/clack/prompts/multiselect.rb
43
+ - lib/clack/prompts/password.rb
44
+ - lib/clack/prompts/path.rb
45
+ - lib/clack/prompts/progress.rb
46
+ - lib/clack/prompts/select.rb
47
+ - lib/clack/prompts/select_key.rb
48
+ - lib/clack/prompts/spinner.rb
49
+ - lib/clack/prompts/tasks.rb
50
+ - lib/clack/prompts/text.rb
51
+ - lib/clack/stream.rb
52
+ - lib/clack/symbols.rb
53
+ - lib/clack/task_log.rb
54
+ - lib/clack/utils.rb
55
+ - lib/clack/validators.rb
56
+ - lib/clack/version.rb
57
+ homepage: https://github.com/swhitt/clackrb
58
+ licenses:
59
+ - MIT
60
+ metadata:
61
+ homepage_uri: https://github.com/swhitt/clackrb
62
+ source_code_uri: https://github.com/swhitt/clackrb
63
+ changelog_uri: https://github.com/swhitt/clackrb/blob/main/CHANGELOG.md
64
+ bug_tracker_uri: https://github.com/swhitt/clackrb/issues
65
+ rubygems_mfa_required: 'true'
66
+ rdoc_options: []
67
+ require_paths:
68
+ - lib
69
+ required_ruby_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: 3.2.0
74
+ required_rubygems_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ requirements: []
80
+ rubygems_version: 3.6.9
81
+ specification_version: 4
82
+ summary: Beautiful, minimal CLI prompts
83
+ test_files: []