rubycli 0.1.5 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9287ea2ca51772eb53c50fe717e648c7d23a9fb1e5cb0e25990eef7f07cde225
4
- data.tar.gz: aab6e71f2beafb924bb4bc29689c6ce64391d55bc43936a789278ad764e91592
3
+ metadata.gz: 521eac399843d8fe002c1017b24b0cf72644f179460d3a599a1b9bf9d0949d7e
4
+ data.tar.gz: d245d65f31c08f15c0f9bad5d4e13e3d7cb1785936772ab6a83887cd4fdf4564
5
5
  SHA512:
6
- metadata.gz: 98baa8a62366c9babedda68174dbc1fadaafbdf1ea4354393bf1a525b9b1d1f2ce0459f20ef8aff81cc4a3448f0686da9bf7940a8fde2526a3b42caf3d2feb63
7
- data.tar.gz: ac5044eec17148e3bb26ef1aa2fd67b78e6578acd6693747bb9a95e8d043e1842f9ebc853e6e50297130d0351fdfb9cfad04c1d217738c6462eda94c1c5b039f
6
+ metadata.gz: 286af4633230ddd40c39e40c2ec92f810bd47cc533c1d578dc42178179552518bfc057f992beef340cc4a34592c603d0ddc6d10eef1df928ce83be383cd9857c
7
+ data.tar.gz: 82e587aac72882cb1b3e317d20d2dc37c54ed8e927fc39ffa9d28dd871d77b05c6d2fc64ad13dc5e3fe7c903b6f5312dfa0b34e9e46391e66788435286c909e9
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.6] - 2025-11-11
4
+
5
+ ### Changed
6
+ - `rubycli --check` now reports unknown type tokens and enumerated allowed values (with DidYouMean suggestions) instead of silently treating them as strings, while `--strict` continues to enforce the surviving annotations at runtime.
7
+
3
8
  ## [0.1.5] - 2025-11-10
4
9
 
5
10
  ### Added
data/README.ja.md CHANGED
@@ -156,13 +156,13 @@ Rubycli は「ファイル名を CamelCase にした定数」を公開対象だ
156
156
 
157
157
  大規模なコードベースでも安全側を保ちながら、どうしても自動選択したいときだけ 1 フラグで切り替えられます。
158
158
 
159
- > **インスタンスメソッド専用のクラスについて** – 公開メソッドがインスタンス側(`def greet` など)にしか無い場合は、`--new` を付けて事前にインスタンス化しないと CLI から呼び出せません。クラスメソッドを 1 つ用意するか、`--new` を明示して実行してください。
159
+ > **インスタンスメソッド専用のクラスについて** – 公開メソッドがインスタンス側(`def greet` など)にしか無い場合は、`--new` を付けて事前にインスタンス化しないと CLI から呼び出せません。クラスメソッドを 1 つ用意するか、`--new` を明示して実行してください。`--new` を付ければ `rubycli --help` でもインスタンスメソッドが一覧に現れ、`rubycli --check --new` でコメントの lint も実行できます。
160
160
 
161
161
  ## 開発方針
162
162
 
163
163
  - **便利さが最優先** – 既存の Ruby スクリプトを最小の手間で CLI 化できることを目的にしており、Python Fire の完全移植は目指していません。
164
164
  - **インスパイアであってポートではない** – アイデアの出自は Fire ですが、同等機能を揃える予定は基本的にありません。Fire 由来の未実装機能は仕様です。
165
- - **メソッド定義が土台、コメントが挙動を補強** – 公開メソッドのシグネチャが CLI に露出する範囲と必須/任意を決めますが、コメントに `TAG...` や `[Integer]` を書くと同じ引数でも配列化や型変換が行われます。さらに Rubycli は `--names='["Alice","Bob"]'` のような JSON/YAML らしい入力を自動的に安全なリテラルとして評価します。`rubycli --check パス/対象.rb` でコメントと実装のズレを検査しつつ、通常実行時に `--strict` を付ければドキュメント通りでない入力をその場でエラーにできます。(現状の `--check` は「コメントが揃っているか」を中心にチェックしており、`Booalean` のように型トークンを誤記しても検出できません。)
165
+ - **メソッド定義が土台、コメントが挙動を補強** – 公開メソッドのシグネチャが CLI に露出する範囲と必須/任意を決めますが、コメントに `TAG...` や `[Integer]` を書くと同じ引数でも配列化や型変換が行われます。さらに Rubycli は `--names='["Alice","Bob"]'` のような JSON/YAML らしい入力を自動的に安全なリテラルとして評価します。`rubycli --check パス/対象.rb` でコメントと実装のズレ(未定義の型ラベルや列挙値の誤記を含む)を DidYouMean の候補付きで検査し、通常実行時に `--strict` を付ければドキュメント通りでない入力をその場でエラーにできます。
166
166
  - **軽量メンテナンス** – 実装の多くは AI 支援で作られており、深い Ruby メタプログラミングを伴う大規模拡張は想定外です。Fire 互換を求める PR は事前相談をお願いします。
167
167
 
168
168
  ## 特徴
@@ -173,11 +173,13 @@ Rubycli は「ファイル名を CamelCase にした定数」を公開対象だ
173
173
  - `--pre-script`(エイリアス: `--init`)で任意の Ruby コードを評価し、その結果オブジェクトを公開
174
174
  - `--check` でコメント整合性を lint、`--strict` で入力値をドキュメント通りに強制する二段構えのガード
175
175
 
176
+ > 補足: `--strict` はコメントに書かれた型/許可値をそのまま信頼して検証するため、コメントが誤記だと実行時には検出できません。CI では必ず `rubycli --check` を走らせ、`--strict` は「 lint を通過したドキュメントを本番で厳密に守る」用途に使ってください。
177
+
176
178
  ## Python Fire との違い
177
179
 
178
180
  - **コメント対応のヘルプ生成**: コメントがあればヘルプに反映しつつ、最終的な判断は常にライブなメソッド定義に基づきます。
179
181
  - **型に基づく解析**: `NAME [String]` や YARD タグから型を推論し、真偽値・配列・数値などを自動変換します。
180
- - **厳密な整合性チェック**: `rubycli --check` でコメントと実装のズレを実行前に検査し、通常実行時に `--strict` を付ければドキュメントで宣言した型・許可値以外の入力を拒否できます。
182
+ - **厳密な整合性チェック**: `rubycli --check` でコメントと実装のズレ(未定義の型ラベルや列挙値の誤記など)をコード実行前に検査し、通常実行時に `--strict` を付ければドキュメントで宣言した型・許可値以外の入力を拒否できます。
181
183
  - **Ruby 向け拡張**: キーワード引数やブロック (`@yield*`) といった Ruby 固有の構文に合わせたパーサや `RUBYCLI_*` 環境変数を用意しています。
182
184
 
183
185
  | 機能 | Python Fire | Rubycli |
data/README.md CHANGED
@@ -163,13 +163,13 @@ Rubycli assumes that the file name (CamelCased) matches the class or module you
163
163
 
164
164
  This keeps large projects safe by default but still provides a one-flag escape hatch when you prefer the fully automatic behaviour.
165
165
 
166
- > **Instance-only classes** – If a class only defines public *instance* methods (for example, it exposes functionality via `def greet` on the instance), you must run Rubycli with `--new` so the class is instantiated before commands are resolved. Otherwise Rubycli cannot see any CLI-callable methods. Add at least one public class method when you do not want to rely on `--new`.
166
+ > **Instance-only classes** – If a class only defines public *instance* methods (for example, it exposes functionality via `def greet` on the instance), you must run Rubycli with `--new` so the class is instantiated before commands are resolved. Otherwise Rubycli cannot see any CLI-callable methods. Add at least one public class method when you do not want to rely on `--new`. Passing `--new` also makes those instance methods appear in `rubycli --help` output and allows `rubycli --check --new` to lint their documentation.
167
167
 
168
168
  ## Project Philosophy
169
169
 
170
170
  - **Convenience first** – The goal is to wrap existing Ruby scripts in a CLI with almost no manual plumbing. Fidelity with Python Fire is not a requirement.
171
171
  - **Inspired, not a port** – We borrow ideas from Python Fire, but we do not aim for feature parity. Missing Fire features are generally “by design.”
172
- - **Method definitions first, comments augment behavior** – Public method signatures determine what gets exposed (and which arguments are required), while doc comments like `TAG...` or `[Integer]` can turn the very same CLI value into arrays, integers, booleans, etc. Rubycli also auto-parses inputs that look like JSON/YAML literals (for example `--names='["Alice","Bob"]'`) before enforcing the documented type. Run `rubycli --check path/to/script.rb` to lint documentation mismatches, and pass `--strict` during normal runs when you want invalid input to abort instead of merely warning. (Current limitation: `--check` verifies that comments cover every argument, but it does not yet validate the spelling of type tokens—`Booalean` will be treated as a plain string.)
172
+ - **Method definitions first, comments augment behavior** – Public method signatures determine what gets exposed (and which arguments are required), while doc comments like `TAG...` or `[Integer]` can turn the very same CLI value into arrays, integers, booleans, etc. Rubycli also auto-parses inputs that look like JSON/YAML literals (for example `--names='["Alice","Bob"]'`) before enforcing the documented type. Run `rubycli --check path/to/script.rb` to lint documentation mismatches—including undefined type labels or enumerated values, with DidYouMean suggestions for `Booalean`-style typos—and pass `--strict` during normal runs when you want invalid input to abort instead of merely warning.
173
173
  - **Lightweight maintenance** – Much of the implementation was generated with AI assistance; contributions that diverge into deep Ruby metaprogramming are out of scope. Please discuss expectations before opening parity PRs.
174
174
 
175
175
  ## Features
@@ -180,11 +180,13 @@ This keeps large projects safe by default but still provides a one-flag escape h
180
180
  - Optional pre-script hook (`--pre-script` / `--init`) to evaluate Ruby and expose the resulting object
181
181
  - Dedicated CLI flags for quality gates: `--check` lints documentation/comments without running commands, and `--strict` treats documented types/choices as hard requirements
182
182
 
183
+ > Tip: `--strict` trusts whatever types/choices your comments spell out—if the annotations are misspelled, runtime enforcement has nothing reliable to compare against. Keep `rubycli --check` in CI so documentation typos are caught before production runs that rely on `--strict`.
184
+
183
185
  ## How it differs from Python Fire
184
186
 
185
187
  - **Comment-aware help** – Rubycli leans on doc comments when present but still reflects the live method signature, keeping code as the ultimate authority.
186
188
  - **Type-aware parsing** – Placeholder syntax (`NAME [String]`) and YARD tags let Rubycli coerce arguments to booleans, arrays, numerics, etc. without additional code.
187
- - **Strict validation** – `rubycli --check` lint runs catch documentation drift before execution, while runtime `--strict` runs turn documented types/choices into enforceable contracts.
189
+ - **Strict validation** – `rubycli --check` lint runs catch documentation drift (including undefined type labels or enumerated values) without executing commands, while runtime `--strict` runs turn those documented types/choices into enforceable contracts.
188
190
  - **Ruby-centric tooling** – Supports Ruby-specific conventions such as optional keyword arguments, block documentation (`@yield*` tags), and `RUBYCLI_*` environment toggles.
189
191
 
190
192
  | Capability | Python Fire | Rubycli |
@@ -55,7 +55,7 @@ module Rubycli
55
55
  next
56
56
  end
57
57
 
58
- if (return_meta = parse_return_metadata(stripped))
58
+ if (return_meta = parse_return_metadata(stripped, method_obj))
59
59
  metadata[:returns] << return_meta
60
60
  summary_phase = false
61
61
  next
@@ -68,7 +68,7 @@ module Rubycli
68
68
  next
69
69
  end
70
70
 
71
- if (positional = parse_positional_line(stripped))
71
+ if (positional = parse_positional_line(stripped, method_obj))
72
72
  metadata[:positionals] << positional
73
73
  summary_phase = false
74
74
  next
@@ -137,6 +137,7 @@ module Rubycli
137
137
  description = nil if description&.empty?
138
138
 
139
139
  raw_types = parse_type_annotation(type_str)
140
+ audit_type_annotation_tokens(raw_types, method_obj)
140
141
  types, allowed_values = partition_type_tokens(raw_types)
141
142
 
142
143
  long_option = nil
@@ -193,6 +194,7 @@ module Rubycli
193
194
 
194
195
  if (types.nil? || types.empty?) && type_token
195
196
  inline_raw_types = parse_type_annotation(type_token)
197
+ audit_type_annotation_tokens(inline_raw_types, method_obj)
196
198
  inline_types, inline_allowed = partition_type_tokens(inline_raw_types)
197
199
  types = inline_types
198
200
  allowed_values = merge_allowed_values(allowed_values, inline_allowed)
@@ -309,6 +311,7 @@ module Rubycli
309
311
  description = description_tokens.join(' ').strip
310
312
  description = nil if description.empty?
311
313
  raw_types = parse_type_annotation(type_token)
314
+ audit_type_annotation_tokens(raw_types, method_obj)
312
315
  types, allowed_values = partition_type_tokens(raw_types)
313
316
 
314
317
  keyword = long_option.delete_prefix('--').tr('-', '_').to_sym
@@ -327,7 +330,7 @@ module Rubycli
327
330
  )
328
331
  end
329
332
 
330
- def parse_positional_line(line)
333
+ def parse_positional_line(line, method_obj)
331
334
  return nil if line.start_with?('--') || line.start_with?('-')
332
335
 
333
336
  tokens = combine_bracketed_tokens(line.split(/\s+/))
@@ -346,6 +349,7 @@ module Rubycli
346
349
  description = nil if description.empty?
347
350
 
348
351
  raw_types = parse_type_annotation(type_token)
352
+ audit_type_annotation_tokens(raw_types, method_obj)
349
353
  types, allowed_values = partition_type_tokens(raw_types)
350
354
  placeholder_info = analyze_placeholder(placeholder)
351
355
  inferred_types = infer_types_from_placeholder(
@@ -371,10 +375,11 @@ module Rubycli
371
375
  )
372
376
  end
373
377
 
374
- def parse_return_metadata(line)
378
+ def parse_return_metadata(line, method_obj)
375
379
  yard_match = /\A@return\s+\[([^\]]+)\](?:\s+(.*))?\z/.match(line)
376
380
  if yard_match
377
381
  types = parse_type_annotation(yard_match[1])
382
+ audit_type_annotation_tokens(types, method_obj)
378
383
  description = yard_match[2]&.strip
379
384
  return ReturnDefinition.new(types: types, description: description)
380
385
  end
@@ -382,6 +387,7 @@ module Rubycli
382
387
  shorthand_match = /\A=>\s+(\[[^\]]+\]|[^\s]+)(?:\s+(.*))?\z/.match(line)
383
388
  if shorthand_match
384
389
  types = parse_type_annotation(shorthand_match[1])
390
+ audit_type_annotation_tokens(types, method_obj)
385
391
  description = shorthand_match[2]&.strip
386
392
  return ReturnDefinition.new(types: types, description: description)
387
393
  end
@@ -390,11 +396,74 @@ module Rubycli
390
396
  stripped = line.sub(/\Areturn\s+/, '')
391
397
  type_token, description = stripped.split(/\s+/, 2)
392
398
  types = parse_type_annotation(type_token)
399
+ audit_type_annotation_tokens(types, method_obj)
393
400
  description = description&.strip
394
401
  return ReturnDefinition.new(types: types, description: description)
395
402
  end
396
403
  end
397
404
 
405
+ def doc_issue_location(method_obj)
406
+ return [nil, nil] unless method_obj.respond_to?(:source_location)
407
+
408
+ source_file, source_line = method_obj.source_location
409
+ line_number = source_line ? [source_line - 1, 1].max : nil
410
+ [source_file, line_number]
411
+ end
412
+
413
+ def audit_type_annotation_tokens(tokens, method_obj)
414
+ return if tokens.nil? || tokens.empty?
415
+ return unless @environment.doc_check_mode?
416
+
417
+ source_file, line_number = doc_issue_location(method_obj)
418
+ literal_tokens_present = Array(tokens).any? do |token|
419
+ literal_entry = literal_entry_from_token(token)
420
+ literal_entry || literal_hint_token?(token)
421
+ end
422
+
423
+ Array(tokens).each do |token|
424
+ audit_single_token(token, source_file, line_number, literal_tokens_present)
425
+ end
426
+ end
427
+
428
+ def audit_single_token(token, source_file, line_number, literal_context)
429
+ normalized = token.to_s.strip
430
+ return if normalized.empty?
431
+
432
+ if literal_hint_token?(normalized)
433
+ expand_annotation_token(normalized).each do |entry|
434
+ audit_literal_entry(entry, source_file, line_number)
435
+ end
436
+ return
437
+ end
438
+
439
+ literal_entry = literal_entry_from_token(normalized)
440
+ if literal_entry
441
+ return
442
+ end
443
+
444
+ if literal_context && literal_token_candidate?(normalized, include_uppercase: true) && !known_type_token?(normalized)
445
+ warn_unknown_allowed_value(normalized, source_file, line_number)
446
+ return
447
+ end
448
+
449
+ if literal_token_candidate?(normalized, include_uppercase: false)
450
+ warn_unknown_allowed_value(normalized, source_file, line_number)
451
+ return
452
+ end
453
+
454
+ return if inline_type_hint?(normalized)
455
+ return if known_type_token?(normalized)
456
+
457
+ warn_unknown_type_token(normalized, source_file, line_number)
458
+ end
459
+
460
+ def audit_literal_entry(token, source_file, line_number)
461
+ literal_entry = literal_entry_from_token(token)
462
+ return if literal_entry
463
+
464
+ warn_unknown_allowed_value(token, source_file, line_number)
465
+ end
466
+
398
467
  def extract_parameter_defaults(method_obj)
399
468
  location = method_obj.source_location
400
469
  return {} unless location
@@ -735,6 +804,93 @@ module Rubycli
735
804
  nil
736
805
  end
737
806
 
807
+ def literal_token_candidate?(token, include_uppercase: false)
808
+ return false unless token
809
+
810
+ stripped = token.strip
811
+ return false if stripped.empty?
812
+
813
+ lowered = stripped.downcase
814
+ return true if %w[true false nil null ~].include?(lowered)
815
+ return true if stripped.start_with?(':', '"', "'")
816
+ return true if stripped.match?(/\A-?\d/)
817
+
818
+ pattern = include_uppercase ? /\A[a-z0-9._-]+\z/i : /\A[a-z0-9._-]+\z/
819
+ stripped.match?(pattern)
820
+ end
821
+
822
+ def warn_unknown_type_token(token, source_file, line_number)
823
+ return if token.nil? || token.empty?
824
+
825
+ suggestions = type_token_suggestions(token)
826
+ message = "Unknown type token '#{token}'"
827
+ if suggestions.any?
828
+ hint = suggestions.first(2).join(' or ')
829
+ message = "#{message} (did you mean #{hint}?)"
830
+ end
831
+ @environment.handle_documentation_issue(message, file: source_file, line: line_number)
832
+ end
833
+
834
+ def warn_unknown_allowed_value(token, source_file, line_number)
835
+ return if token.nil? || token.empty?
836
+
837
+ suggestions = allowed_value_suggestions(token)
838
+ message = "Unknown allowed value token '#{token}'"
839
+ if suggestions.any?
840
+ hint = suggestions.first(2).join(' or ')
841
+ message = "#{message} (did you mean #{hint}?)"
842
+ end
843
+ @environment.handle_documentation_issue(message, file: source_file, line: line_number)
844
+ end
845
+
846
+ def type_token_suggestions(token)
847
+ dictionary = type_dictionary
848
+ return [] if dictionary.empty?
849
+
850
+ require 'did_you_mean'
851
+ checker = DidYouMean::SpellChecker.new(dictionary: dictionary)
852
+ checker.correct(token.to_s).take(3)
853
+ rescue LoadError, NameError
854
+ []
855
+ end
856
+
857
+ def allowed_value_suggestions(token)
858
+ stripped = token.to_s.strip
859
+ return [] if stripped.empty?
860
+
861
+ candidates = []
862
+ if stripped.match?(/\A[a-z0-9._-]+\z/i)
863
+ candidates << ":#{stripped.downcase}"
864
+ candidates << stripped.downcase.inspect
865
+ end
866
+ return [] if candidates.empty?
867
+
868
+ require 'did_you_mean'
869
+ checker = DidYouMean::SpellChecker.new(dictionary: candidates)
870
+ checker.correct(stripped).take(2)
871
+ rescue LoadError, NameError
872
+ candidates.first(1)
873
+ end
874
+
875
+ def type_dictionary
876
+ builtins = (INLINE_TYPE_HINTS + %w[NilClass Fixnum Decimal Struct]).uniq
877
+ constant_names = []
878
+ begin
879
+ ObjectSpace.each_object(Module) do |mod|
880
+ name = mod.name
881
+ next unless name && !name.empty?
882
+
883
+ constant_names << name
884
+ parts = name.split('::')
885
+ constant_names << parts.last if parts.size > 1
886
+ end
887
+ rescue StandardError
888
+ constant_names = Object.constants.map(&:to_s)
889
+ end
890
+
891
+ (builtins + constant_names).map(&:to_s).map(&:strip).reject(&:empty?).uniq
892
+ end
893
+
738
894
 
739
895
  def placeholder_token?(token)
740
896
  return false unless token
@@ -786,8 +942,16 @@ module Rubycli
786
942
  def known_type_token?(token)
787
943
  return false unless token
788
944
 
789
- candidate = token.start_with?('@') ? token[1..] : token
790
- candidate =~ /\A[A-Z][A-Za-z0-9_:<>\[\]]*\z/
945
+ normalized = normalize_type_token(token)
946
+ return false if normalized.empty?
947
+
948
+ return true if primitive_type_token?(normalized)
949
+
950
+ if (inner = array_inner_type_token(normalized))
951
+ return known_type_token?(inner)
952
+ end
953
+
954
+ !safe_constant_lookup(normalized).nil?
791
955
  end
792
956
 
793
957
  def inline_type_hint?(token)
@@ -805,6 +969,53 @@ module Rubycli
805
969
  INLINE_TYPE_HINTS.include?(base)
806
970
  end
807
971
 
972
+ def primitive_type_token?(token)
973
+ return false if token.nil? || token.empty?
974
+
975
+ base = token.to_s
976
+ INLINE_TYPE_HINTS.include?(base) || %w[NilClass Fixnum Decimal Struct].include?(base)
977
+ end
978
+
979
+ def array_inner_type_token(token)
980
+ return nil unless token
981
+
982
+ stripped = token.to_s.strip
983
+ if stripped.end_with?('[]')
984
+ stripped[0..-3]
985
+ elsif stripped.start_with?('Array<') && stripped.end_with?('>')
986
+ stripped[6..-2].strip
987
+ else
988
+ nil
989
+ end
990
+ end
991
+
992
+ def safe_constant_lookup(name)
993
+ parts = name.to_s.split('::').reject(&:empty?)
994
+ return nil if parts.empty?
995
+
996
+ context = Object
997
+ parts.each do |const_name|
998
+ return nil unless context.const_defined?(const_name, false)
999
+
1000
+ context = context.const_get(const_name)
1001
+ end
1002
+ context
1003
+ rescue NameError
1004
+ nil
1005
+ end
1006
+
1007
+ def literal_hint_token?(token)
1008
+ return false unless token
1009
+
1010
+ stripped = token.to_s.strip
1011
+ return false if stripped.empty?
1012
+
1013
+ stripped.start_with?('%i[') ||
1014
+ stripped.start_with?('%I[') ||
1015
+ stripped.start_with?('%w[') ||
1016
+ stripped.start_with?('%W[')
1017
+ end
1018
+
808
1019
  def parameter_role(method_obj, keyword)
809
1020
  return nil unless method_obj.respond_to?(:parameters)
810
1021
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rubycli
4
- VERSION = '0.1.5'
4
+ VERSION = '0.1.6'
5
5
  end
data/lib/rubycli.rb CHANGED
@@ -531,19 +531,24 @@ module Rubycli
531
531
 
532
532
  def build_missing_constant_message(name, defined_constants, full_path, details: nil)
533
533
  lines = ["Could not find definition: #{name}"]
534
- 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
535
536
 
536
537
  if defined_constants && !defined_constants.empty?
537
538
  sample = defined_constants.first(5)
538
- suffix = defined_constants.size > sample.size ? " ... (#{defined_constants.size} total)" : ''
539
- 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}"
540
541
  else
541
- 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.'
542
543
  end
543
544
 
544
- lines << " #{details}" if details
545
+ if details
546
+ lines << ''
547
+ lines << details
548
+ end
545
549
 
546
- 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.'
547
552
  lines.join("\n")
548
553
  end
549
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.5
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-09 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