ukiryu 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 (115) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/docs.yml +63 -0
  3. data/.github/workflows/links.yml +99 -0
  4. data/.github/workflows/rake.yml +19 -0
  5. data/.github/workflows/release.yml +27 -0
  6. data/.gitignore +18 -4
  7. data/.rubocop.yml +1 -0
  8. data/.rubocop_todo.yml +213 -0
  9. data/Gemfile +12 -8
  10. data/README.adoc +613 -0
  11. data/Rakefile +2 -2
  12. data/docs/assets/logo.svg +1 -0
  13. data/exe/ukiryu +11 -0
  14. data/lib/ukiryu/action/base.rb +77 -0
  15. data/lib/ukiryu/cache.rb +199 -0
  16. data/lib/ukiryu/cli.rb +133 -307
  17. data/lib/ukiryu/cli_commands/base_command.rb +155 -0
  18. data/lib/ukiryu/cli_commands/commands_command.rb +120 -0
  19. data/lib/ukiryu/cli_commands/commands_command.rb.fixed +40 -0
  20. data/lib/ukiryu/cli_commands/config_command.rb +249 -0
  21. data/lib/ukiryu/cli_commands/describe_command.rb +326 -0
  22. data/lib/ukiryu/cli_commands/describe_command.rb.fixed +254 -0
  23. data/lib/ukiryu/cli_commands/exec_inline_command.rb.fixed +180 -0
  24. data/lib/ukiryu/cli_commands/extract_command.rb +84 -0
  25. data/lib/ukiryu/cli_commands/info_command.rb +156 -0
  26. data/lib/ukiryu/cli_commands/list_command.rb +70 -0
  27. data/lib/ukiryu/cli_commands/opts_command.rb +106 -0
  28. data/lib/ukiryu/cli_commands/opts_command.rb.fixed +105 -0
  29. data/lib/ukiryu/cli_commands/response_formatter.rb +240 -0
  30. data/lib/ukiryu/cli_commands/run_command.rb +375 -0
  31. data/lib/ukiryu/cli_commands/run_file_command.rb +215 -0
  32. data/lib/ukiryu/cli_commands/system_command.rb +90 -0
  33. data/lib/ukiryu/cli_commands/validate_command.rb +87 -0
  34. data/lib/ukiryu/cli_commands/version_command.rb +16 -0
  35. data/lib/ukiryu/cli_commands/which_command.rb +166 -0
  36. data/lib/ukiryu/command_builder.rb +205 -0
  37. data/lib/ukiryu/config/env_provider.rb +64 -0
  38. data/lib/ukiryu/config/env_schema.rb +63 -0
  39. data/lib/ukiryu/config/override_resolver.rb +68 -0
  40. data/lib/ukiryu/config/type_converter.rb +59 -0
  41. data/lib/ukiryu/config.rb +249 -0
  42. data/lib/ukiryu/errors.rb +3 -0
  43. data/lib/ukiryu/executable_locator.rb +114 -0
  44. data/lib/ukiryu/execution/command_info.rb +64 -0
  45. data/lib/ukiryu/execution/metadata.rb +97 -0
  46. data/lib/ukiryu/execution/output.rb +144 -0
  47. data/lib/ukiryu/execution/result.rb +194 -0
  48. data/lib/ukiryu/execution.rb +15 -0
  49. data/lib/ukiryu/execution_context.rb +251 -0
  50. data/lib/ukiryu/executor.rb +76 -493
  51. data/lib/ukiryu/extractors/base_extractor.rb +63 -0
  52. data/lib/ukiryu/extractors/extractor.rb +150 -0
  53. data/lib/ukiryu/extractors/help_parser.rb +188 -0
  54. data/lib/ukiryu/extractors/native_extractor.rb +47 -0
  55. data/lib/ukiryu/io.rb +196 -0
  56. data/lib/ukiryu/logger.rb +544 -0
  57. data/lib/ukiryu/models/argument.rb +28 -0
  58. data/lib/ukiryu/models/argument_definition.rb +119 -0
  59. data/lib/ukiryu/models/arguments.rb +113 -0
  60. data/lib/ukiryu/models/command_definition.rb +176 -0
  61. data/lib/ukiryu/models/command_info.rb +37 -0
  62. data/lib/ukiryu/models/components.rb +107 -0
  63. data/lib/ukiryu/models/env_var_definition.rb +30 -0
  64. data/lib/ukiryu/models/error_response.rb +41 -0
  65. data/lib/ukiryu/models/execution_metadata.rb +31 -0
  66. data/lib/ukiryu/models/execution_report.rb +236 -0
  67. data/lib/ukiryu/models/exit_codes.rb +74 -0
  68. data/lib/ukiryu/models/flag_definition.rb +67 -0
  69. data/lib/ukiryu/models/option_definition.rb +102 -0
  70. data/lib/ukiryu/models/output_info.rb +25 -0
  71. data/lib/ukiryu/models/platform_profile.rb +153 -0
  72. data/lib/ukiryu/models/routing.rb +211 -0
  73. data/lib/ukiryu/models/search_paths.rb +39 -0
  74. data/lib/ukiryu/models/success_response.rb +85 -0
  75. data/lib/ukiryu/models/tool_definition.rb +145 -0
  76. data/lib/ukiryu/models/tool_metadata.rb +82 -0
  77. data/lib/ukiryu/models/validation_result.rb +80 -0
  78. data/lib/ukiryu/models/version_compatibility.rb +152 -0
  79. data/lib/ukiryu/models/version_detection.rb +39 -0
  80. data/lib/ukiryu/models.rb +23 -0
  81. data/lib/ukiryu/options/base.rb +95 -0
  82. data/lib/ukiryu/options_builder/formatter.rb +87 -0
  83. data/lib/ukiryu/options_builder/validator.rb +43 -0
  84. data/lib/ukiryu/options_builder.rb +311 -0
  85. data/lib/ukiryu/platform.rb +6 -6
  86. data/lib/ukiryu/registry.rb +143 -183
  87. data/lib/ukiryu/response/base.rb +217 -0
  88. data/lib/ukiryu/runtime.rb +179 -0
  89. data/lib/ukiryu/schema_validator.rb +8 -10
  90. data/lib/ukiryu/shell/bash.rb +3 -3
  91. data/lib/ukiryu/shell/cmd.rb +4 -4
  92. data/lib/ukiryu/shell/fish.rb +1 -1
  93. data/lib/ukiryu/shell/powershell.rb +3 -3
  94. data/lib/ukiryu/shell/sh.rb +1 -1
  95. data/lib/ukiryu/shell/zsh.rb +1 -1
  96. data/lib/ukiryu/shell.rb +146 -39
  97. data/lib/ukiryu/thor_ext.rb +208 -0
  98. data/lib/ukiryu/tool.rb +649 -258
  99. data/lib/ukiryu/tool_index.rb +224 -0
  100. data/lib/ukiryu/tools/base.rb +381 -0
  101. data/lib/ukiryu/tools/class_generator.rb +132 -0
  102. data/lib/ukiryu/tools/executable_finder.rb +29 -0
  103. data/lib/ukiryu/tools/generator.rb +154 -0
  104. data/lib/ukiryu/tools.rb +109 -0
  105. data/lib/ukiryu/type.rb +28 -43
  106. data/lib/ukiryu/validation/constraints.rb +281 -0
  107. data/lib/ukiryu/validation/validator.rb +188 -0
  108. data/lib/ukiryu/validation.rb +21 -0
  109. data/lib/ukiryu/version.rb +1 -1
  110. data/lib/ukiryu/version_detector.rb +51 -0
  111. data/lib/ukiryu.rb +31 -15
  112. data/ukiryu-proposal.md +2952 -0
  113. data/ukiryu.gemspec +18 -14
  114. metadata +137 -5
  115. data/.github/workflows/test.yml +0 -143
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "open3"
4
- require "timeout"
3
+ require 'open3'
4
+ require 'timeout'
5
+ require_relative 'execution'
6
+ require_relative 'shell'
5
7
 
6
8
  module Ukiryu
7
9
  # Command execution with platform-specific methods
@@ -22,7 +24,10 @@ module Ukiryu
22
24
  # @option options [Hash] :env environment variables
23
25
  # @option options [String] :cwd working directory
24
26
  # @option options [Symbol] :shell shell to use (default: auto-detect)
25
- # @return [Result] execution result with composed OOP classes
27
+ # @option options [String, IO] :stdin stdin input data (string or IO object)
28
+ # @option options [String] :tool_name tool name for exit code lookups
29
+ # @option options [String] :command_name command name for exit code lookups
30
+ # @return [Execution::Result] execution result with composed OOP classes
26
31
  # @raise [TimeoutError] if command times out
27
32
  # @raise [ExecutionError] if command fails
28
33
  def execute(executable, args = [], options = {})
@@ -38,31 +43,38 @@ module Ukiryu
38
43
  # Execute with timeout
39
44
  timeout = options[:timeout] || 90
40
45
  cwd = options[:cwd]
46
+ stdin = options[:stdin]
41
47
 
42
48
  started_at = Time.now
43
49
  begin
44
- result = execute_with_timeout(command, env, timeout, cwd)
50
+ result = if stdin
51
+ execute_with_stdin(command, env, timeout, cwd, stdin)
52
+ else
53
+ execute_with_timeout(command, env, timeout, cwd)
54
+ end
45
55
  rescue Timeout::Error
46
- finished_at = Time.now
56
+ Time.now
47
57
  raise TimeoutError, "Command timed out after #{timeout} seconds: #{executable}"
48
58
  end
49
59
  finished_at = Time.now
50
60
 
51
- # Create OOP result components
52
- command_info = CommandInfo.new(
61
+ # Create OOP result components using Execution namespace
62
+ command_info = Execution::CommandInfo.new(
53
63
  executable: executable,
54
64
  arguments: args,
55
65
  full_command: command,
56
- shell: shell_name
66
+ shell: shell_name,
67
+ tool_name: options[:tool_name],
68
+ command_name: options[:command_name]
57
69
  )
58
70
 
59
- output = Output.new(
71
+ output = Execution::Output.new(
60
72
  stdout: result[:stdout],
61
73
  stderr: result[:stderr],
62
74
  exit_status: result[:status]
63
75
  )
64
76
 
65
- metadata = ExecutionMetadata.new(
77
+ metadata = Execution::ExecutionMetadata.new(
66
78
  started_at: started_at,
67
79
  finished_at: finished_at,
68
80
  timeout: timeout
@@ -70,10 +82,11 @@ module Ukiryu
70
82
 
71
83
  # Check exit status
72
84
  if result[:status] != 0 && !options[:allow_failure]
73
- raise ExecutionError, format_error(executable, command, result)
85
+ raise ExecutionError,
86
+ format_error(executable, command, result)
74
87
  end
75
88
 
76
- Result.new(
89
+ Execution::Result.new(
77
90
  command_info: command_info,
78
91
  output: output,
79
92
  metadata: metadata
@@ -88,7 +101,7 @@ module Ukiryu
88
101
  # @return [String, nil] the full path to the executable, or nil if not found
89
102
  def find_executable(command, options = {})
90
103
  # Try with PATHEXT extensions (Windows executables)
91
- exts = ENV["PATHEXT"] ? ENV["PATHEXT"].split(";") : [""]
104
+ exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
92
105
 
93
106
  search_paths = Platform.executable_search_paths
94
107
  search_paths.concat(options[:additional_paths]) if options[:additional_paths]
@@ -168,6 +181,56 @@ module Ukiryu
168
181
  end
169
182
  end
170
183
 
184
+ # Execute command with stdin input
185
+ #
186
+ # @param command [String] the command to execute
187
+ # @param env [Hash] environment variables
188
+ # @param timeout [Integer] timeout in seconds
189
+ # @param cwd [String, nil] working directory (nil for current directory)
190
+ # @param stdin_data [String, IO] stdin input data
191
+ # @return [Hash] execution result
192
+ def execute_with_stdin(command, env, timeout, cwd, stdin_data)
193
+ Timeout.timeout(timeout) do
194
+ execution = lambda do
195
+ Open3.popen3(env, command) do |stdin, stdout, stderr, wait_thr|
196
+ # Write stdin data
197
+ begin
198
+ if stdin_data.is_a?(IO)
199
+ IO.copy_stream(stdin_data, stdin)
200
+ elsif stdin_data.is_a?(String)
201
+ stdin.write(stdin_data)
202
+ end
203
+ rescue Errno::EPIPE
204
+ # Process closed stdin early (e.g., 'head' command)
205
+ ensure
206
+ stdin.close
207
+ end
208
+
209
+ # Read output
210
+ out = stdout.read
211
+ err = stderr.read
212
+
213
+ # Wait for process to complete
214
+ status = wait_thr.value
215
+
216
+ {
217
+ status: extract_status(status),
218
+ stdout: out,
219
+ stderr: err
220
+ }
221
+ end
222
+ end
223
+
224
+ if cwd
225
+ Dir.chdir(cwd) do
226
+ execution.call
227
+ end
228
+ else
229
+ execution.call
230
+ end
231
+ end
232
+ end
233
+
171
234
  # Extract exit status from Process::Status
172
235
  #
173
236
  # @param status [Process::Status] the process status
@@ -232,485 +295,5 @@ module Ukiryu
232
295
  ERROR
233
296
  end
234
297
  end
235
-
236
- # Execution command information
237
- #
238
- # Encapsulates details about the executed command
239
- class CommandInfo
240
- attr_reader :executable, :arguments, :full_command, :shell
241
-
242
- def initialize(executable:, arguments:, full_command:, shell: nil)
243
- @executable = executable
244
- @arguments = arguments
245
- @full_command = full_command
246
- @shell = shell
247
- end
248
-
249
- # Get the executable name only
250
- #
251
- # @return [String] executable name
252
- def executable_name
253
- File.basename(@executable)
254
- end
255
-
256
- # Get argument count
257
- #
258
- # @return [Integer] number of arguments
259
- def argument_count
260
- @arguments.count
261
- end
262
-
263
- # String representation
264
- #
265
- # @return [String] command string
266
- def to_s
267
- @full_command
268
- end
269
-
270
- # Inspect
271
- #
272
- # @return [String] inspection string
273
- def inspect
274
- "#<Ukiryu::Executor::CommandInfo exe=#{executable_name.inspect} args=#{argument_count}>"
275
- end
276
-
277
- # Convert to hash
278
- #
279
- # @return [Hash] command info as hash
280
- def to_h
281
- {
282
- executable: @executable,
283
- executable_name: executable_name,
284
- arguments: @arguments,
285
- full_command: @full_command,
286
- shell: @shell
287
- }
288
- end
289
- end
290
-
291
- # Captured output from command execution
292
- #
293
- # Provides typed access to stdout and stderr with parsing utilities
294
- class Output
295
- attr_reader :raw_stdout, :raw_stderr, :exit_status
296
-
297
- def initialize(stdout:, stderr:, exit_status:)
298
- @raw_stdout = stdout
299
- @raw_stderr = stderr
300
- @exit_status = exit_status
301
- end
302
-
303
- # Get stdout as a string (stripped)
304
- #
305
- # @return [String] stripped stdout
306
- def stdout
307
- @raw_stdout.strip
308
- end
309
-
310
- # Get stderr as a string (stripped)
311
- #
312
- # @return [String] stripped stderr
313
- def stderr
314
- @raw_stderr.strip
315
- end
316
-
317
- # Get stdout lines as an array
318
- #
319
- # @return [Array<String>] stdout split by lines
320
- def stdout_lines
321
- @raw_stdout.split("\n")
322
- end
323
-
324
- # Get stderr lines as an array
325
- #
326
- # @return [Array<String>] stderr split by lines
327
- def stderr_lines
328
- @raw_stderr.split("\n")
329
- end
330
-
331
- # Check if stdout contains a pattern
332
- #
333
- # @param pattern [String, Regexp] pattern to search for
334
- # @return [Boolean] true if pattern is found
335
- def stdout_contains?(pattern)
336
- if pattern.is_a?(Regexp)
337
- @raw_stdout.match?(pattern)
338
- else
339
- @raw_stdout.include?(pattern.to_s)
340
- end
341
- end
342
-
343
- # Check if stderr contains a pattern
344
- #
345
- # @param pattern [String, Regexp] pattern to search for
346
- # @return [Boolean] true if pattern is found
347
- def stderr_contains?(pattern)
348
- if pattern.is_a?(Regexp)
349
- @raw_stderr.match?(pattern)
350
- else
351
- @raw_stderr.include?(pattern.to_s)
352
- end
353
- end
354
-
355
- # Check if stdout is empty
356
- #
357
- # @return [Boolean] true if stdout is empty
358
- def stdout_empty?
359
- @raw_stdout.strip.empty?
360
- end
361
-
362
- # Check if stderr is empty
363
- #
364
- # @return [Boolean] true if stderr is empty
365
- def stderr_empty?
366
- @raw_stderr.strip.empty?
367
- end
368
-
369
- # Get stdout length
370
- #
371
- # @return [Integer] byte length of stdout
372
- def stdout_length
373
- @raw_stdout.length
374
- end
375
-
376
- # Get stderr length
377
- #
378
- # @return [Integer] byte length of stderr
379
- def stderr_length
380
- @raw_stderr.length
381
- end
382
-
383
- # Check if command succeeded
384
- #
385
- # @return [Boolean] true if exit status is 0
386
- def success?
387
- @exit_status == 0
388
- end
389
-
390
- # Check if command failed
391
- #
392
- # @return [Boolean] true if exit status is non-zero
393
- def failure?
394
- @exit_status != 0
395
- end
396
-
397
- # Convert to hash
398
- #
399
- # @return [Hash] output as hash
400
- def to_h
401
- {
402
- stdout: @raw_stdout,
403
- stderr: @raw_stderr,
404
- exit_status: @exit_status,
405
- success: success?,
406
- stdout_lines: stdout_lines,
407
- stderr_lines: stderr_lines
408
- }
409
- end
410
-
411
- # String representation
412
- #
413
- # @return [String] summary string
414
- def to_s
415
- if success?
416
- "Success (exit: #{@exit_status}, stdout: #{stdout_length} bytes, stderr: #{stderr_length} bytes)"
417
- else
418
- "Failed (exit: #{@exit_status}, stdout: #{stdout_length} bytes, stderr: #{stderr_length} bytes)"
419
- end
420
- end
421
-
422
- # Inspect
423
- #
424
- # @return [String] inspection string
425
- def inspect
426
- "#<Ukiryu::Executor::Output exit=#{@exit_status} success=#{success?}>"
427
- end
428
- end
429
-
430
- # Execution metadata
431
- #
432
- # Provides timing and execution environment information
433
- class ExecutionMetadata
434
- attr_reader :started_at, :finished_at, :duration, :timeout
435
-
436
- def initialize(started_at:, finished_at:, timeout: nil)
437
- @started_at = started_at
438
- @finished_at = finished_at
439
- @timeout = timeout
440
- @duration = calculate_duration
441
- end
442
-
443
- # Calculate duration from start and finish times
444
- #
445
- # @return [Float, nil] duration in seconds
446
- def calculate_duration
447
- return nil unless @started_at && @finished_at
448
-
449
- @finished_at - @started_at
450
- end
451
-
452
- # Get execution duration in seconds
453
- #
454
- # @return [Float, nil] duration in seconds
455
- def duration_seconds
456
- @duration
457
- end
458
-
459
- # Get execution duration in milliseconds
460
- #
461
- # @return [Float, nil] duration in milliseconds
462
- def duration_milliseconds
463
- @duration ? @duration * 1000 : nil
464
- end
465
-
466
- # Check if execution timed out
467
- #
468
- # @return [Boolean] true if timeout was set and exceeded
469
- def timed_out?
470
- return false unless @timeout && @duration
471
-
472
- @duration > @timeout
473
- end
474
-
475
- # Format duration for display
476
- #
477
- # @return [String] formatted duration
478
- def formatted_duration
479
- return "N/A" unless @duration
480
-
481
- if @duration < 1
482
- "#{(@duration * 1000).round(2)}ms"
483
- elsif @duration < 60
484
- "#{@duration.round(3)}s"
485
- else
486
- minutes = @duration / 60
487
- seconds = @duration % 60
488
- "#{minutes.to_i}m#{seconds.round(1)}s"
489
- end
490
- end
491
-
492
- # Convert to hash
493
- #
494
- # @return [Hash] metadata as hash
495
- def to_h
496
- {
497
- started_at: @started_at,
498
- finished_at: @finished_at,
499
- duration: @duration,
500
- duration_seconds: @duration,
501
- duration_milliseconds: duration_milliseconds,
502
- timeout: @timeout,
503
- timed_out: timed_out?
504
- }
505
- end
506
-
507
- # String representation
508
- #
509
- # @return [String] formatted string
510
- def to_s
511
- "duration: #{formatted_duration}"
512
- end
513
-
514
- # Inspect
515
- #
516
- # @return [String] inspection string
517
- def inspect
518
- "#<Ukiryu::Executor::ExecutionMetadata duration=#{formatted_duration}>"
519
- end
520
- end
521
-
522
- # Result class for command execution
523
- #
524
- # Provides a rich, object-oriented interface to command execution results.
525
- # Composes CommandInfo, Output, and ExecutionMetadata for a fully OOP design.
526
- class Result
527
- attr_reader :command_info, :output, :metadata
528
-
529
- # Initialize a new result
530
- #
531
- # @param command_info [CommandInfo] the command execution info
532
- # @param output [Output] the captured output
533
- # @param metadata [ExecutionMetadata] execution metadata
534
- def initialize(command_info:, output:, metadata:)
535
- @command_info = command_info
536
- @output = output
537
- @metadata = metadata
538
- end
539
-
540
- # Get the full command string
541
- #
542
- # @return [String] executed command
543
- def command
544
- @command_info.full_command
545
- end
546
-
547
- # Get the executable
548
- #
549
- # @return [String] executable path
550
- def executable
551
- @command_info.executable
552
- end
553
-
554
- # Get the executable name only
555
- #
556
- # @return [String] executable name
557
- def executable_name
558
- @command_info.executable_name
559
- end
560
-
561
- # Get raw stdout
562
- #
563
- # @return [String] raw stdout
564
- def stdout
565
- @output.raw_stdout
566
- end
567
-
568
- # Get raw stderr
569
- #
570
- # @return [String] raw stderr
571
- def stderr
572
- @output.raw_stderr
573
- end
574
-
575
- # Get exit status code
576
- #
577
- # @return [Integer] exit status
578
- def status
579
- @output.exit_status
580
- end
581
- alias exit_code status
582
-
583
- # Get the exit code (alias for status)
584
- #
585
- # @return [Integer] exit status
586
- def exit_status
587
- @output.exit_status
588
- end
589
-
590
- # Get start time
591
- #
592
- # @return [Time] when command started
593
- def started_at
594
- @metadata.started_at
595
- end
596
-
597
- # Get finish time
598
- #
599
- # @return [Time] when command finished
600
- def finished_at
601
- @metadata.finished_at
602
- end
603
-
604
- # Get execution duration
605
- #
606
- # @return [Float, nil] duration in seconds
607
- def duration
608
- @metadata.duration
609
- end
610
-
611
- # Get execution duration (alias)
612
- #
613
- # @return [Float, nil] duration in seconds
614
- def execution_time
615
- @metadata.duration_seconds
616
- end
617
-
618
- # Check if the command succeeded
619
- #
620
- # @return [Boolean]
621
- def success?
622
- @output.success?
623
- end
624
-
625
- # Check if the command failed
626
- #
627
- # @return [Boolean]
628
- def failure?
629
- @output.failure?
630
- end
631
-
632
- # Get stdout as a stripped string
633
- #
634
- # @return [String] stripped stdout
635
- def output
636
- @output.stdout
637
- end
638
-
639
- # Get stderr as a stripped string
640
- #
641
- # @return [String] stripped stderr
642
- def error_output
643
- @output.stderr
644
- end
645
-
646
- # Get stdout lines
647
- #
648
- # @return [Array<String>] stdout split by lines
649
- def stdout_lines
650
- @output.stdout_lines
651
- end
652
-
653
- # Get stderr lines
654
- #
655
- # @return [Array<String>] stderr split by lines
656
- def stderr_lines
657
- @output.stderr_lines
658
- end
659
-
660
- # Check if stdout contains a pattern
661
- #
662
- # @param pattern [String, Regexp] pattern to search for
663
- # @return [Boolean] true if pattern is found
664
- def stdout_contains?(pattern)
665
- @output.stdout_contains?(pattern)
666
- end
667
-
668
- # Check if stderr contains a pattern
669
- #
670
- # @param pattern [String, Regexp] pattern to search for
671
- # @return [Boolean] true if pattern is found
672
- def stderr_contains?(pattern)
673
- @output.stderr_contains?(pattern)
674
- end
675
-
676
- # Get a hash representation of the result
677
- #
678
- # @return [Hash] result data as a hash
679
- def to_h
680
- {
681
- command: @command_info.to_h,
682
- output: @output.to_h,
683
- metadata: @metadata.to_h,
684
- success: success?,
685
- status: status
686
- }
687
- end
688
-
689
- # Get a JSON representation of the result
690
- #
691
- # @return [String] result data as JSON
692
- def to_json(*args)
693
- require 'json'
694
- to_h.to_json(*args)
695
- end
696
-
697
- # String representation of the result
698
- #
699
- # @return [String] summary of the result
700
- def to_s
701
- if success?
702
- "Success (#{@command_info.executable_name}, status: #{status}, duration: #{@metadata.formatted_duration})"
703
- else
704
- "Failed (#{@command_info.executable_name}, status: #{status}, duration: #{@metadata.formatted_duration})"
705
- end
706
- end
707
-
708
- # Inspect the result (for debugging)
709
- #
710
- # @return [String] detailed inspection string
711
- def inspect
712
- "#<Ukiryu::Executor::Result exe=#{@command_info.executable_name.inspect} status=#{status} duration=#{@metadata.formatted_duration}>"
713
- end
714
- end
715
298
  end
716
299
  end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ukiryu
4
+ module Extractors
5
+ # Base class for definition extraction strategies
6
+ #
7
+ # Each extraction strategy implements a different approach to
8
+ # extracting tool definitions from CLI tools.
9
+ #
10
+ # @abstract Subclasses must implement the `extract` method
11
+ class BaseExtractor
12
+ # Initialize the extractor
13
+ #
14
+ # @param tool_name [String, Symbol] the tool name
15
+ # @param options [Hash] extraction options
16
+ def initialize(tool_name, options = {})
17
+ @tool_name = tool_name
18
+ @options = options
19
+ end
20
+
21
+ # Extract definition from the tool
22
+ #
23
+ # Subclasses must implement this method
24
+ #
25
+ # @return [String, nil] the YAML definition or nil if extraction failed
26
+ # @raise [NotImplementedError] if not implemented in subclass
27
+ def extract
28
+ raise NotImplementedError, "#{self.class} must implement #extract"
29
+ end
30
+
31
+ # Check if this extractor can extract from the tool
32
+ #
33
+ # @return [Boolean] true if extraction is possible
34
+ def available?
35
+ raise NotImplementedError, "#{self.class} must implement #available?"
36
+ end
37
+
38
+ private
39
+
40
+ # Execute a command and capture output
41
+ #
42
+ # @param command [Array<String>] the command to execute
43
+ # @return [Hash] result with :stdout, :stderr, :exit_status keys
44
+ def execute_command(command)
45
+ require 'open3'
46
+
47
+ stdout, stderr, status = Open3.capture3(*command)
48
+ {
49
+ stdout: stdout,
50
+ stderr: stderr,
51
+ exit_status: status.exitstatus
52
+ }
53
+ rescue Errno::ENOENT
54
+ # Command not found
55
+ {
56
+ stdout: '',
57
+ stderr: 'Command not found',
58
+ exit_status: 127
59
+ }
60
+ end
61
+ end
62
+ end
63
+ end