rubycli 0.1.4 → 0.1.6

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.
@@ -47,7 +47,7 @@ module Rubycli
47
47
  EVAL_BINDING.eval(trimmed)
48
48
  rescue SyntaxError, NameError => e
49
49
  if eval_lax_mode?
50
- warn "[rubycli] Failed to evaluate argument as Ruby (#{e.message.strip}). Passing it through because --eval-lax is enabled."
50
+ warn "[WARN] Failed to evaluate argument as Ruby (#{e.message.strip}). Passing it through because --eval-lax is enabled."
51
51
  expression
52
52
  else
53
53
  raise
@@ -45,7 +45,6 @@ module Rubycli
45
45
 
46
46
  puts
47
47
  puts "Detailed command help: #{File.basename($PROGRAM_NAME)} COMMAND help"
48
- puts "Enable debug logging: --debug or RUBYCLI_DEBUG=true"
49
48
  end
50
49
 
51
50
  def method_description(method_obj)
@@ -152,10 +151,10 @@ module Rubycli
152
151
  end
153
152
 
154
153
  def render_positionals(positionals_in_order)
155
- rows = positionals_in_order.map do |info|
154
+ rows = positionals_in_order.map do |info|
156
155
  definition = info[:definition]
157
156
  label = info[:label]
158
- type = formatted_types(definition&.types)
157
+ type = type_display(definition)
159
158
  requirement = positional_requirement(info[:kind])
160
159
  description_parts = []
161
160
  description_parts << info[:description] if info[:description]
@@ -169,7 +168,7 @@ module Rubycli
169
168
  def render_options(options, required_keywords)
170
169
  rows = options.map do |opt|
171
170
  label = option_flag_with_placeholder(opt)
172
- type = formatted_types(opt.types)
171
+ type = type_display(opt)
173
172
  requirement = required_keywords.include?(opt.keyword) ? 'required' : 'optional'
174
173
  description_parts = []
175
174
  description_parts << opt.description if opt.description
@@ -198,10 +197,53 @@ module Rubycli
198
197
  end
199
198
 
200
199
  def formatted_types(types)
201
- type_list = Array(types).compact.map(&:to_s).reject(&:empty?).uniq
200
+ type_list = Array(types).compact.map { |token| token.to_s.strip }.reject(&:empty?)
201
+ type_list = type_list.each_with_object([]) do |token, acc|
202
+ acc << token unless acc.include?(token)
203
+ end
202
204
  return '' if type_list.empty?
203
205
 
204
- "[#{type_list.join(', ')}]"
206
+ nil_tokens, rest = type_list.partition { |token| token.casecmp('nil').zero? }
207
+ ordered = rest + nil_tokens
208
+ "[#{ordered.join(', ')}]"
209
+ end
210
+
211
+ def type_display(definition)
212
+ return formatted_types(definition&.types) unless definition
213
+
214
+ literal_entries = Array(definition.allowed_values).map { |entry| format_allowed_value(entry[:value]) }
215
+ type_tokens = Array(definition.types).map { |token| token.to_s.strip }.reject(&:empty?)
216
+ type_tokens.reject! { |token| literal_token?(token) }
217
+
218
+ combined = (literal_entries + type_tokens).uniq
219
+ return formatted_types(definition.types) if combined.empty?
220
+
221
+ "[#{combined.join(', ')}]"
222
+ end
223
+
224
+ def format_allowed_value(value)
225
+ case value
226
+ when Symbol
227
+ ":#{value}"
228
+ when String
229
+ %("#{value}")
230
+ when Integer, Float
231
+ value.to_s
232
+ when TrueClass, FalseClass
233
+ value.to_s
234
+ when NilClass
235
+ 'nil'
236
+ else
237
+ value.inspect
238
+ end
239
+ end
240
+
241
+ def literal_token?(token)
242
+ return true if token.start_with?('%')
243
+ return true if token.include?('[')
244
+ return true if token.start_with?(':')
245
+
246
+ false
205
247
  end
206
248
 
207
249
  def positional_requirement(kind)
@@ -268,19 +310,25 @@ module Rubycli
268
310
  end
269
311
 
270
312
  def required_placeholder(placeholder, definition, name)
271
- return placeholder.strip unless placeholder.nil? || placeholder.strip.empty?
313
+ unless placeholder.nil? || placeholder.strip.empty? || auto_generated_placeholder?(placeholder, definition, name)
314
+ return placeholder.strip
315
+ end
272
316
 
273
317
  default_positional_label(definition, name, uppercase: true)
274
318
  end
275
319
 
276
320
  def optional_placeholder(placeholder, definition, name)
277
- return placeholder.strip unless placeholder.nil? || placeholder.strip.empty?
321
+ unless placeholder.nil? || placeholder.strip.empty? || auto_generated_placeholder?(placeholder, definition, name)
322
+ return placeholder.strip
323
+ end
278
324
 
279
325
  "[#{default_positional_label(definition, name, uppercase: true)}]"
280
326
  end
281
327
 
282
328
  def rest_placeholder(placeholder, definition, name)
283
- return placeholder.strip unless placeholder.nil? || placeholder.strip.empty?
329
+ unless placeholder.nil? || placeholder.strip.empty? || auto_generated_placeholder?(placeholder, definition, name)
330
+ return placeholder.strip
331
+ end
284
332
 
285
333
  base = default_positional_label(definition, name, uppercase: true)
286
334
  "[#{base}...]"
@@ -320,6 +368,13 @@ module Rubycli
320
368
  first_non_nil_type
321
369
  end
322
370
 
371
+ def auto_generated_placeholder?(placeholder, definition, name)
372
+ return false unless definition
373
+ return false unless definition.respond_to?(:doc_format) && definition.doc_format == :auto_generated
374
+
375
+ placeholder.strip.casecmp(name.to_s).zero?
376
+ end
377
+
323
378
  def ensure_angle_bracket_placeholder(placeholder)
324
379
  raw = placeholder.to_s.strip
325
380
  return raw if raw.empty?
data/lib/rubycli/types.rb CHANGED
@@ -2,13 +2,13 @@ module Rubycli
2
2
  OptionDefinition = Struct.new(
3
3
  :keyword, :long, :short, :value_name, :types, :description, :requires_value,
4
4
  :boolean_flag, :optional_value, :default_value, :inline_type_annotation,
5
- :inline_type_text, :doc_format,
5
+ :inline_type_text, :doc_format, :allowed_values,
6
6
  keyword_init: true
7
7
  )
8
8
 
9
9
  PositionalDefinition = Struct.new(
10
10
  :placeholder, :label, :types, :description, :param_name, :default_value,
11
- :inline_type_annotation, :inline_type_text, :doc_format,
11
+ :inline_type_annotation, :inline_type_text, :doc_format, :allowed_values,
12
12
  keyword_init: true
13
13
  )
14
14
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rubycli
4
- VERSION = '0.1.4'
4
+ VERSION = '0.1.6'
5
5
  end
data/lib/rubycli.rb CHANGED
@@ -216,37 +216,21 @@ module Rubycli
216
216
  constant_mode: nil
217
217
  )
218
218
  raise ArgumentError, 'target_path must be specified' if target_path.nil? || target_path.empty?
219
- original_program_name = $PROGRAM_NAME
220
219
  if json && eval_args
221
220
  raise Error, '--json-args cannot be combined with --eval-args or --eval-lax'
222
221
  end
223
222
 
224
- full_path = find_target_path(target_path)
225
- capture = Rubycli.constant_capture
226
- capture.capture(full_path) { load full_path }
227
- $PROGRAM_NAME = File.basename(full_path)
228
- constant_mode ||= Rubycli.environment.constant_resolution_mode
229
- candidates = build_constant_candidates(full_path, capture.constants_for(full_path))
230
- defined_constants = candidates.map(&:name)
231
-
232
- target = if class_name
233
- constantize(
234
- class_name,
235
- defined_constants: defined_constants,
236
- full_path: full_path
237
- )
238
- else
239
- select_constant_candidate(
240
- full_path,
241
- camelize(File.basename(full_path, '.rb')),
242
- candidates,
243
- constant_mode,
244
- instantiate: new
245
- )
246
- end
247
- runner_target = new ? instantiate_target(target) : target
248
- runner_target = apply_pre_scripts(pre_scripts, target, runner_target)
223
+ runner_target, full_path = prepare_runner_target(
224
+ target_path,
225
+ class_name,
226
+ new: new,
227
+ pre_scripts: pre_scripts,
228
+ constant_mode: constant_mode
229
+ )
249
230
 
231
+ original_program_name = $PROGRAM_NAME
232
+ original_argv = nil
233
+ $PROGRAM_NAME = File.basename(full_path)
250
234
  original_argv = ARGV.dup
251
235
  ARGV.replace(Array(cli_args).dup)
252
236
  run_with_modes(runner_target, json: json, eval_args: eval_args, eval_lax: eval_lax)
@@ -255,6 +239,53 @@ module Rubycli
255
239
  ARGV.replace(original_argv) if original_argv
256
240
  end
257
241
 
242
+ def check(
243
+ target_path,
244
+ class_name = nil,
245
+ new: false,
246
+ pre_scripts: [],
247
+ constant_mode: nil
248
+ )
249
+ raise ArgumentError, 'target_path must be specified' if target_path.nil? || target_path.empty?
250
+ previous_doc_check = Rubycli.environment.doc_check_mode?
251
+ Rubycli.environment.clear_documentation_issues!
252
+ Rubycli.environment.enable_doc_check!
253
+
254
+ runner_target, full_path = prepare_runner_target(
255
+ target_path,
256
+ class_name,
257
+ new: new,
258
+ pre_scripts: pre_scripts,
259
+ constant_mode: constant_mode
260
+ )
261
+
262
+ original_program_name = $PROGRAM_NAME
263
+ $PROGRAM_NAME = File.basename(full_path)
264
+
265
+ catalog = Rubycli.cli.command_catalog_for(runner_target)
266
+ Array(catalog&.entries).each do |entry|
267
+ method_obj = entry&.method
268
+ Rubycli.documentation_registry.metadata_for(method_obj) if method_obj
269
+ end
270
+
271
+ if catalog&.entries&.empty? && runner_target.respond_to?(:call)
272
+ method_obj = runner_target.method(:call) rescue nil
273
+ Rubycli.documentation_registry.metadata_for(method_obj) if method_obj
274
+ end
275
+
276
+ issues = Rubycli.environment.documentation_issues
277
+ if issues.empty?
278
+ puts 'rubycli documentation OK'
279
+ 0
280
+ else
281
+ warn "[ERROR] rubycli documentation check failed (#{issues.size} issue#{issues.size == 1 ? '' : 's'})"
282
+ 1
283
+ end
284
+ ensure
285
+ Rubycli.environment.disable_doc_check! unless previous_doc_check
286
+ $PROGRAM_NAME = original_program_name if original_program_name
287
+ end
288
+
258
289
  def apply_pre_scripts(sources, base_target, initial_target)
259
290
  Array(sources).reduce(initial_target) do |current_target, source|
260
291
  result = evaluate_pre_script(source, base_target, current_target)
@@ -342,6 +373,41 @@ module Rubycli
342
373
  end
343
374
  end
344
375
 
376
+ def prepare_runner_target(
377
+ target_path,
378
+ class_name,
379
+ new: false,
380
+ pre_scripts: [],
381
+ constant_mode: nil
382
+ )
383
+ full_path = find_target_path(target_path)
384
+ capture = Rubycli.constant_capture
385
+ capture.capture(full_path) { load full_path }
386
+ constant_mode ||= Rubycli.environment.constant_resolution_mode
387
+ candidates = build_constant_candidates(full_path, capture.constants_for(full_path))
388
+ defined_constants = candidates.map(&:name)
389
+
390
+ target = if class_name
391
+ constantize(
392
+ class_name,
393
+ defined_constants: defined_constants,
394
+ full_path: full_path
395
+ )
396
+ else
397
+ select_constant_candidate(
398
+ full_path,
399
+ camelize(File.basename(full_path, '.rb')),
400
+ candidates,
401
+ constant_mode,
402
+ instantiate: new
403
+ )
404
+ end
405
+
406
+ runner_target = new ? instantiate_target(target) : target
407
+ runner_target = apply_pre_scripts(pre_scripts, target, runner_target)
408
+ [runner_target, full_path]
409
+ end
410
+
345
411
  def build_constant_candidates(path, constant_names)
346
412
  normalized = normalize_path(path)
347
413
  Array(constant_names).each_with_object([]) do |const_name, memo|
@@ -465,19 +531,24 @@ module Rubycli
465
531
 
466
532
  def build_missing_constant_message(name, defined_constants, full_path, details: nil)
467
533
  lines = ["Could not find definition: #{name}"]
468
- lines << " Loaded file: #{File.expand_path(full_path)}" if full_path
534
+ lines << ''
535
+ lines << "Loaded file: #{File.expand_path(full_path)}" if full_path
469
536
 
470
537
  if defined_constants && !defined_constants.empty?
471
538
  sample = defined_constants.first(5)
472
- suffix = defined_constants.size > sample.size ? " ... (#{defined_constants.size} total)" : ''
473
- lines << " Constants found in this file: #{sample.join(', ')}#{suffix}"
539
+ suffix = defined_constants.size > sample.size ? " (#{defined_constants.size} total)" : ''
540
+ lines << "Constants found in this file: #{sample.join(', ')}#{suffix}"
474
541
  else
475
- lines << " Rubycli could not detect any publicly exposable constants in this file."
542
+ lines << 'Rubycli could not detect any publicly exposable constants in this file.'
476
543
  end
477
544
 
478
- lines << " #{details}" if details
545
+ if details
546
+ lines << ''
547
+ lines << details
548
+ end
479
549
 
480
- lines << " Ensure the CLASS_OR_MODULE argument is correct when invoking the CLI."
550
+ lines << ''
551
+ lines << 'Hint: Ensure the CLASS_OR_MODULE argument is correct when invoking the CLI.'
481
552
  lines.join("\n")
482
553
  end
483
554
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubycli
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - inakaegg
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-11-07 00:00:00.000000000 Z
10
+ date: 2025-11-11 00:00:00.000000000 Z
11
11
  dependencies: []
12
12
  description: Rubycli turns plain Ruby classes and modules into command-line interfaces
13
13
  by reading their documentation comments, inspired by Python Fire but tailored for