rubycli 0.1.2 → 0.1.5
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 +4 -4
- data/CHANGELOG.md +44 -0
- data/README.ja.md +82 -14
- data/README.md +82 -14
- data/lib/rubycli/argument_mode_controller.rb +69 -0
- data/lib/rubycli/argument_parser.rb +287 -102
- data/lib/rubycli/arguments/token_stream.rb +41 -0
- data/lib/rubycli/arguments/value_converter.rb +85 -0
- data/lib/rubycli/cli.rb +14 -7
- data/lib/rubycli/command_line.rb +58 -6
- data/lib/rubycli/constant_capture.rb +50 -0
- data/lib/rubycli/documentation/comment_extractor.rb +52 -0
- data/lib/rubycli/documentation/metadata_parser.rb +973 -0
- data/lib/rubycli/documentation_registry.rb +11 -853
- data/lib/rubycli/environment.rb +50 -8
- data/lib/rubycli/eval_coercer.rb +16 -1
- data/lib/rubycli/help_renderer.rb +64 -9
- data/lib/rubycli/types.rb +2 -2
- data/lib/rubycli/version.rb +1 -1
- data/lib/rubycli.rb +265 -121
- metadata +8 -2
data/lib/rubycli.rb
CHANGED
|
@@ -12,11 +12,15 @@ require_relative 'rubycli/type_utils'
|
|
|
12
12
|
require_relative 'rubycli/documentation_registry'
|
|
13
13
|
require_relative 'rubycli/json_coercer'
|
|
14
14
|
require_relative 'rubycli/eval_coercer'
|
|
15
|
+
require_relative 'rubycli/arguments/token_stream'
|
|
16
|
+
require_relative 'rubycli/arguments/value_converter'
|
|
17
|
+
require_relative 'rubycli/argument_mode_controller'
|
|
15
18
|
require_relative 'rubycli/argument_parser'
|
|
16
19
|
require_relative 'rubycli/help_renderer'
|
|
17
20
|
require_relative 'rubycli/result_emitter'
|
|
18
21
|
require_relative 'rubycli/cli'
|
|
19
22
|
require_relative 'rubycli/command_line'
|
|
23
|
+
require_relative 'rubycli/constant_capture'
|
|
20
24
|
|
|
21
25
|
module Rubycli
|
|
22
26
|
class Error < StandardError; end
|
|
@@ -40,6 +44,13 @@ module Rubycli
|
|
|
40
44
|
@eval_coercer ||= EvalCoercer.new
|
|
41
45
|
end
|
|
42
46
|
|
|
47
|
+
def argument_mode_controller
|
|
48
|
+
@argument_mode_controller ||= ArgumentModeController.new(
|
|
49
|
+
json_coercer: json_coercer,
|
|
50
|
+
eval_coercer: eval_coercer
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
43
54
|
def argument_parser
|
|
44
55
|
@argument_parser ||= ArgumentParser.new(
|
|
45
56
|
environment: environment,
|
|
@@ -57,6 +68,10 @@ module Rubycli
|
|
|
57
68
|
@result_emitter ||= ResultEmitter.new(environment: environment)
|
|
58
69
|
end
|
|
59
70
|
|
|
71
|
+
def constant_capture
|
|
72
|
+
@constant_capture ||= ConstantCapture.new
|
|
73
|
+
end
|
|
74
|
+
|
|
60
75
|
def cli
|
|
61
76
|
@cli ||= CLI.new(
|
|
62
77
|
environment: environment,
|
|
@@ -68,7 +83,10 @@ module Rubycli
|
|
|
68
83
|
end
|
|
69
84
|
|
|
70
85
|
def run(target, args = ARGV, cli_mode = true)
|
|
71
|
-
cli.run(target, args.dup, cli_mode)
|
|
86
|
+
status = cli.run(target, args.dup, cli_mode)
|
|
87
|
+
return status unless cli_mode
|
|
88
|
+
|
|
89
|
+
exit(status.to_i)
|
|
72
90
|
end
|
|
73
91
|
|
|
74
92
|
def parse_arguments(args, method = nil)
|
|
@@ -106,15 +124,11 @@ module Rubycli
|
|
|
106
124
|
end
|
|
107
125
|
|
|
108
126
|
def json_mode?
|
|
109
|
-
|
|
127
|
+
argument_mode_controller.json_mode?
|
|
110
128
|
end
|
|
111
129
|
|
|
112
130
|
def with_json_mode(enabled = true)
|
|
113
|
-
|
|
114
|
-
raise Rubycli::ArgumentError, '--json-args and --eval-args cannot be used together'
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
json_coercer.with_json_mode(enabled) { yield }
|
|
131
|
+
argument_mode_controller.with_json_mode(enabled) { yield }
|
|
118
132
|
end
|
|
119
133
|
|
|
120
134
|
def coerce_json_value(value)
|
|
@@ -122,15 +136,15 @@ module Rubycli
|
|
|
122
136
|
end
|
|
123
137
|
|
|
124
138
|
def eval_mode?
|
|
125
|
-
|
|
139
|
+
argument_mode_controller.eval_mode?
|
|
126
140
|
end
|
|
127
141
|
|
|
128
|
-
def
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
end
|
|
142
|
+
def eval_lax_mode?
|
|
143
|
+
eval_coercer.eval_lax_mode?
|
|
144
|
+
end
|
|
132
145
|
|
|
133
|
-
|
|
146
|
+
def with_eval_mode(enabled = true, **options)
|
|
147
|
+
argument_mode_controller.with_eval_mode(enabled, **options) { yield }
|
|
134
148
|
end
|
|
135
149
|
|
|
136
150
|
def coerce_eval_value(value)
|
|
@@ -138,25 +152,7 @@ module Rubycli
|
|
|
138
152
|
end
|
|
139
153
|
|
|
140
154
|
def apply_argument_coercions(pos_args, kw_args)
|
|
141
|
-
|
|
142
|
-
raise Rubycli::ArgumentError, '--json-args and --eval-args cannot be used together'
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
if json_mode?
|
|
146
|
-
pos_args.map! { |value| coerce_json_value(value) }
|
|
147
|
-
kw_args.keys.each do |key|
|
|
148
|
-
kw_args[key] = coerce_json_value(kw_args[key])
|
|
149
|
-
end
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
return unless eval_mode?
|
|
153
|
-
|
|
154
|
-
pos_args.map! { |value| coerce_eval_value(value) }
|
|
155
|
-
kw_args.keys.each do |key|
|
|
156
|
-
kw_args[key] = coerce_eval_value(kw_args[key])
|
|
157
|
-
end
|
|
158
|
-
rescue ArgumentError => e
|
|
159
|
-
raise Rubycli::ArgumentError, e.message
|
|
155
|
+
argument_mode_controller.apply_argument_coercions(pos_args, kw_args)
|
|
160
156
|
end
|
|
161
157
|
|
|
162
158
|
def apply_json_coercion(pos_args, kw_args)
|
|
@@ -168,6 +164,44 @@ module Rubycli
|
|
|
168
164
|
class Error < Rubycli::Error; end
|
|
169
165
|
class PreScriptError < Error; end
|
|
170
166
|
|
|
167
|
+
ConstantCandidate = Struct.new(
|
|
168
|
+
:name,
|
|
169
|
+
:constant,
|
|
170
|
+
:class_methods,
|
|
171
|
+
:instance_methods,
|
|
172
|
+
keyword_init: true
|
|
173
|
+
) do
|
|
174
|
+
def callable?(instantiate: false)
|
|
175
|
+
return true if class_methods.any?
|
|
176
|
+
|
|
177
|
+
instantiate && instance_methods.any?
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def matches?(base_name)
|
|
181
|
+
name.split('::').last == base_name
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def instance_only?
|
|
185
|
+
instance_methods.any? && class_methods.empty?
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def summary
|
|
189
|
+
parts = []
|
|
190
|
+
parts << "class: #{format_methods(class_methods)}" if class_methods.any?
|
|
191
|
+
parts << "instance: #{format_methods(instance_methods)}" if instance_methods.any?
|
|
192
|
+
parts << 'no CLI methods' if parts.empty?
|
|
193
|
+
parts.join(' | ')
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
private
|
|
197
|
+
|
|
198
|
+
def format_methods(methods)
|
|
199
|
+
list = methods.first(3).map(&:to_s)
|
|
200
|
+
list << '...' if methods.size > 3
|
|
201
|
+
list.join(', ')
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
171
205
|
module_function
|
|
172
206
|
|
|
173
207
|
def execute(
|
|
@@ -177,36 +211,81 @@ module Rubycli
|
|
|
177
211
|
new: false,
|
|
178
212
|
json: false,
|
|
179
213
|
eval_args: false,
|
|
180
|
-
|
|
214
|
+
eval_lax: false,
|
|
215
|
+
pre_scripts: [],
|
|
216
|
+
constant_mode: nil
|
|
181
217
|
)
|
|
182
218
|
raise ArgumentError, 'target_path must be specified' if target_path.nil? || target_path.empty?
|
|
183
|
-
original_program_name = $PROGRAM_NAME
|
|
184
219
|
if json && eval_args
|
|
185
|
-
raise Error, '--json-args
|
|
220
|
+
raise Error, '--json-args cannot be combined with --eval-args or --eval-lax'
|
|
186
221
|
end
|
|
187
222
|
|
|
188
|
-
full_path =
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
target = constantize(
|
|
195
|
-
constant_name,
|
|
196
|
-
defined_constants: defined_constants,
|
|
197
|
-
full_path: full_path
|
|
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
|
|
198
229
|
)
|
|
199
|
-
runner_target = new ? instantiate_target(target) : target
|
|
200
|
-
runner_target = apply_pre_scripts(pre_scripts, target, runner_target)
|
|
201
230
|
|
|
231
|
+
original_program_name = $PROGRAM_NAME
|
|
232
|
+
original_argv = nil
|
|
233
|
+
$PROGRAM_NAME = File.basename(full_path)
|
|
202
234
|
original_argv = ARGV.dup
|
|
203
235
|
ARGV.replace(Array(cli_args).dup)
|
|
204
|
-
run_with_modes(runner_target, json: json, eval_args: eval_args)
|
|
236
|
+
run_with_modes(runner_target, json: json, eval_args: eval_args, eval_lax: eval_lax)
|
|
205
237
|
ensure
|
|
206
238
|
$PROGRAM_NAME = original_program_name if original_program_name
|
|
207
239
|
ARGV.replace(original_argv) if original_argv
|
|
208
240
|
end
|
|
209
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
|
+
|
|
210
289
|
def apply_pre_scripts(sources, base_target, initial_target)
|
|
211
290
|
Array(sources).reduce(initial_target) do |current_target, source|
|
|
212
291
|
result = evaluate_pre_script(source, base_target, current_target)
|
|
@@ -251,12 +330,6 @@ module Rubycli
|
|
|
251
330
|
end
|
|
252
331
|
end
|
|
253
332
|
|
|
254
|
-
def infer_class_name(path)
|
|
255
|
-
base = File.basename(path, '.rb')
|
|
256
|
-
base_const = camelize(base)
|
|
257
|
-
detect_constant_for_file(path, base_const) || base_const
|
|
258
|
-
end
|
|
259
|
-
|
|
260
333
|
def camelize(name)
|
|
261
334
|
name.split(/[^a-zA-Z0-9]+/).reject(&:empty?).map { |part|
|
|
262
335
|
part[0].upcase + part[1..].downcase
|
|
@@ -288,106 +361,175 @@ module Rubycli
|
|
|
288
361
|
raise Error, "Failed to instantiate target: #{e.message}"
|
|
289
362
|
end
|
|
290
363
|
|
|
291
|
-
def run_with_modes(target, json:, eval_args:)
|
|
364
|
+
def run_with_modes(target, json:, eval_args:, eval_lax:)
|
|
292
365
|
runner = proc { Rubycli.run(target) }
|
|
293
366
|
|
|
294
367
|
if json
|
|
295
368
|
Rubycli.with_json_mode(true, &runner)
|
|
296
369
|
elsif eval_args
|
|
297
|
-
Rubycli.with_eval_mode(true, &runner)
|
|
370
|
+
Rubycli.with_eval_mode(true, lax: eval_lax, &runner)
|
|
298
371
|
else
|
|
299
372
|
runner.call
|
|
300
373
|
end
|
|
301
374
|
end
|
|
302
375
|
|
|
303
|
-
def
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
|
308
405
|
|
|
309
|
-
|
|
310
|
-
|
|
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
|
|
311
410
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
411
|
+
def build_constant_candidates(path, constant_names)
|
|
412
|
+
normalized = normalize_path(path)
|
|
413
|
+
Array(constant_names).each_with_object([]) do |const_name, memo|
|
|
414
|
+
constant = safe_constant_lookup(const_name)
|
|
415
|
+
next unless constant.is_a?(Module)
|
|
316
416
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
next unless location && location[0]
|
|
320
|
-
next unless File.expand_path(location[0]) == normalized
|
|
417
|
+
class_methods = collect_defined_methods(constant.singleton_class, normalized)
|
|
418
|
+
instance_methods = collect_defined_methods(constant, normalized)
|
|
321
419
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
420
|
+
memo << ConstantCandidate.new(
|
|
421
|
+
name: const_name,
|
|
422
|
+
constant: constant,
|
|
423
|
+
class_methods: class_methods,
|
|
424
|
+
instance_methods: instance_methods
|
|
425
|
+
)
|
|
426
|
+
end
|
|
325
427
|
end
|
|
326
428
|
|
|
327
|
-
def
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
mod_name = module_name_for(mod)
|
|
333
|
-
next unless mod_name
|
|
334
|
-
next unless safe_const_defined?(mod, base_const)
|
|
429
|
+
def collect_defined_methods(owner, normalized_path)
|
|
430
|
+
owner.public_instance_methods(false).each_with_object([]) do |method_name, memo|
|
|
431
|
+
method_object = owner.instance_method(method_name)
|
|
432
|
+
location = method_object.source_location
|
|
433
|
+
next unless location && normalize_path(location[0]) == normalized_path
|
|
335
434
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
end.uniq.sort_by { |name| [-name.count('::'), name] }
|
|
435
|
+
memo << method_name
|
|
436
|
+
end
|
|
437
|
+
rescue TypeError
|
|
438
|
+
[]
|
|
341
439
|
end
|
|
342
440
|
|
|
343
|
-
def
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
name = mod.name
|
|
347
|
-
return nil if name.nil? || name.start_with?('#<')
|
|
441
|
+
def safe_constant_lookup(name)
|
|
442
|
+
parts = name.split('::').reject(&:empty?)
|
|
443
|
+
context = Object
|
|
348
444
|
|
|
349
|
-
|
|
350
|
-
|
|
445
|
+
parts.each do |const_name|
|
|
446
|
+
return nil unless context.const_defined?(const_name, false)
|
|
351
447
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
end
|
|
448
|
+
context = context.const_get(const_name)
|
|
449
|
+
end
|
|
355
450
|
|
|
356
|
-
|
|
357
|
-
mod.constants(false)
|
|
451
|
+
context
|
|
358
452
|
rescue NameError
|
|
359
|
-
|
|
453
|
+
nil
|
|
360
454
|
end
|
|
361
455
|
|
|
362
|
-
def
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
456
|
+
def select_constant_candidate(path, base_const, candidates, constant_mode, instantiate: false)
|
|
457
|
+
if candidates.empty?
|
|
458
|
+
raise Error, build_missing_constant_message(
|
|
459
|
+
base_const,
|
|
460
|
+
[],
|
|
461
|
+
path,
|
|
462
|
+
details: 'Rubycli could not detect any constants in this file.'
|
|
463
|
+
)
|
|
464
|
+
end
|
|
367
465
|
|
|
368
|
-
|
|
369
|
-
|
|
466
|
+
matching = candidates.find { |candidate| candidate.matches?(base_const) }
|
|
467
|
+
if matching
|
|
468
|
+
return matching.constant if matching.callable?(instantiate: instantiate)
|
|
469
|
+
|
|
470
|
+
detail = if matching.instance_only?
|
|
471
|
+
"#{matching.name} only defines instance methods in this file. Run with --new to instantiate before invoking CLI commands."
|
|
472
|
+
else
|
|
473
|
+
"#{matching.name} does not define any CLI-callable methods in this file. Add a public class or instance method defined in this file."
|
|
474
|
+
end
|
|
475
|
+
raise Error, build_missing_constant_message(
|
|
476
|
+
base_const,
|
|
477
|
+
candidates.map(&:name),
|
|
478
|
+
path,
|
|
479
|
+
details: detail
|
|
480
|
+
)
|
|
481
|
+
end
|
|
370
482
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
483
|
+
callable = candidates.select { |candidate| candidate.callable?(instantiate: instantiate) }
|
|
484
|
+
if callable.empty?
|
|
485
|
+
raise Error, build_missing_constant_message(
|
|
486
|
+
base_const,
|
|
487
|
+
candidates.map(&:name),
|
|
488
|
+
path,
|
|
489
|
+
details: 'Rubycli detected constants in this file, but none define CLI-callable methods. Add a public class or instance method defined in this file.'
|
|
490
|
+
)
|
|
491
|
+
end
|
|
375
492
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
493
|
+
if constant_mode == :auto && callable.size == 1
|
|
494
|
+
return callable.first.constant
|
|
495
|
+
end
|
|
379
496
|
|
|
380
|
-
|
|
381
|
-
|
|
497
|
+
details = build_ambiguous_constant_details(callable, path)
|
|
498
|
+
raise Error, build_missing_constant_message(
|
|
499
|
+
base_const,
|
|
500
|
+
candidates.map(&:name),
|
|
501
|
+
path,
|
|
502
|
+
details: details
|
|
503
|
+
)
|
|
504
|
+
end
|
|
382
505
|
|
|
383
|
-
|
|
506
|
+
def build_ambiguous_constant_details(candidates, path)
|
|
507
|
+
command_target = File.basename(path)
|
|
508
|
+
if candidates.size == 1
|
|
509
|
+
candidate = candidates.first
|
|
510
|
+
lines = []
|
|
511
|
+
lines << "This file defines #{candidate.name}, but its name does not match #{command_target}."
|
|
512
|
+
lines << 'Re-run by specifying the constant explicitly:'
|
|
513
|
+
lines << " rubycli #{command_target} #{candidate.name} ..."
|
|
514
|
+
lines << 'Alternatively pass --auto-target (or RUBYCLI_AUTO_TARGET=auto) to auto-select it.'
|
|
515
|
+
return lines.join("\n")
|
|
384
516
|
end
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
517
|
+
|
|
518
|
+
lines = ['Multiple CLI-capable constants were found in this file:']
|
|
519
|
+
candidates.each do |candidate|
|
|
520
|
+
hint = candidate.instance_only? ? ' (instance methods only; use --new)' : ''
|
|
521
|
+
lines << " - #{candidate.name}: #{candidate.summary}#{hint}"
|
|
522
|
+
end
|
|
523
|
+
lines << "Specify one explicitly, e.g. rubycli #{command_target} MyRunner"
|
|
524
|
+
lines << 'Or pass --auto-target to allow Rubycli to auto-select a single candidate.'
|
|
525
|
+
lines.join("\n")
|
|
388
526
|
end
|
|
389
527
|
|
|
390
|
-
def
|
|
528
|
+
def normalize_path(path)
|
|
529
|
+
File.expand_path(path.to_s)
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
def build_missing_constant_message(name, defined_constants, full_path, details: nil)
|
|
391
533
|
lines = ["Could not find definition: #{name}"]
|
|
392
534
|
lines << " Loaded file: #{File.expand_path(full_path)}" if full_path
|
|
393
535
|
|
|
@@ -399,6 +541,8 @@ module Rubycli
|
|
|
399
541
|
lines << " Rubycli could not detect any publicly exposable constants in this file."
|
|
400
542
|
end
|
|
401
543
|
|
|
544
|
+
lines << " #{details}" if details
|
|
545
|
+
|
|
402
546
|
lines << " Ensure the CLASS_OR_MODULE argument is correct when invoking the CLI."
|
|
403
547
|
lines.join("\n")
|
|
404
548
|
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
|
+
version: 0.1.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- inakaegg
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2025-11-
|
|
10
|
+
date: 2025-11-09 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
|
|
@@ -25,9 +25,15 @@ files:
|
|
|
25
25
|
- README.md
|
|
26
26
|
- exe/rubycli
|
|
27
27
|
- lib/rubycli.rb
|
|
28
|
+
- lib/rubycli/argument_mode_controller.rb
|
|
28
29
|
- lib/rubycli/argument_parser.rb
|
|
30
|
+
- lib/rubycli/arguments/token_stream.rb
|
|
31
|
+
- lib/rubycli/arguments/value_converter.rb
|
|
29
32
|
- lib/rubycli/cli.rb
|
|
30
33
|
- lib/rubycli/command_line.rb
|
|
34
|
+
- lib/rubycli/constant_capture.rb
|
|
35
|
+
- lib/rubycli/documentation/comment_extractor.rb
|
|
36
|
+
- lib/rubycli/documentation/metadata_parser.rb
|
|
31
37
|
- lib/rubycli/documentation_registry.rb
|
|
32
38
|
- lib/rubycli/environment.rb
|
|
33
39
|
- lib/rubycli/eval_coercer.rb
|