inquirex-tty 0.4.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/justfile ADDED
@@ -0,0 +1,56 @@
1
+ # © 2026 Konstantin Gredeskoul
2
+
3
+ set shell := ["bash", "-eu", "-o", "pipefail", "-c"]
4
+
5
+ repo := 'git@github.com:inquirex/inquirex-tty.git'
6
+ version := `grep VERSION lib/inquirex/tty/version.rb | awk '{print $3}' | tr -d '"' | tr -d '\n'`
7
+
8
+ # Run full test suite with coverage
9
+ test:
10
+ bundle exec rspec --format documentation
11
+
12
+ # Run RuboCop linter
13
+ lint:
14
+ bundle exec rubocop
15
+
16
+ # Auto-correct RuboCop offenses
17
+ format:
18
+ bundle exec rubocop -A
19
+
20
+ # Run a flow interactively
21
+ run flow_file:
22
+ bundle exec exe/inquirex-tty run {{flow_file}}
23
+
24
+ # Validate a flow definition
25
+ validate flow_file:
26
+ bundle exec exe/inquirex-tty validate {{flow_file}}
27
+
28
+ # Export a flow as a Mermaid diagram (stdout)
29
+ graph flow_file:
30
+ bundle exec exe/inquirex-tty graph {{flow_file}}
31
+
32
+ # Validate all examples
33
+ examples:
34
+ #!/usr/bin/env bash
35
+ set -e
36
+ for f in examples/*.rb; do
37
+ echo "=== $f ==="
38
+ bundle exec exe/inquirex-tty validate "$f"
39
+ echo ""
40
+ done
41
+
42
+ # Run all tests then lint
43
+ ci: test lint
44
+
45
+ alias check-all := ci
46
+
47
+ version:
48
+ @echo "{{ version }}"
49
+
50
+ # Tag v{{ version }}, publish the GH release, & refresh the Homebrew tap.
51
+ release:
52
+ git fetch --tags
53
+ git tag -f "v{{ version }}"
54
+ git push -f --tags
55
+ gh release delete -y "v{{ version }}" --repo {{ repo }} 2>/dev/null || true
56
+ gh release create "v{{ version }}" --generate-notes --repo {{ repo }}
data/lefthook.yml ADDED
@@ -0,0 +1,36 @@
1
+ output:
2
+ - summary
3
+ - failure
4
+
5
+ pre-commit:
6
+ parallel: true
7
+ jobs:
8
+ - name: lint
9
+ run: bundle exec rubocop -c .rubocop.yml {staged_files}
10
+ glob: "*.{rb,Gemfile}"
11
+ stage_fixed: true
12
+
13
+ - name: check for conflict markers and whitespace issues
14
+ run: git --no-pager diff --check
15
+
16
+ # If tests take >1 second, move this (or just the long-running tests) to pre-push.
17
+ - name: run tests
18
+ run: just test
19
+
20
+ - name: fix rubocop formatting issues
21
+ run: bundle exec rubocop -a {staged_files}
22
+ glob: "*.{rb,Gemfile,gemspec}"
23
+ stage_fixed: true
24
+
25
+ - name: spell check
26
+ run: codespell {staged_files}
27
+ glob: "*.{rb,md,gemspec}"
28
+ exclude: "examples/*"
29
+
30
+ - name: format markdown
31
+ run: mdformat {staged_files}
32
+ glob: "*.md"
33
+ stage_fixed: true
34
+
35
+ - name: scan for secrets
36
+ run: detect-secrets-hook --baseline .secrets.baseline
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Inquirex
4
+ module TTY
5
+ module Commands
6
+ # Exports a flow definition as JSON or YAML (stdout or file).
7
+ #
8
+ # Examples:
9
+ # inquirex export examples/08_tax_preparer.rb # JSON to stdout
10
+ # inquirex export examples/08_tax_preparer.rb -f yml # YAML to stdout
11
+ # inquirex export examples/08_tax_preparer.rb -o . # write 08_tax_preparer.json to cwd
12
+ # inquirex export examples/08_tax_preparer.rb -f yml -o . # write 08_tax_preparer.yml to cwd
13
+ # inquirex export examples/08_tax_preparer.rb -o out.json # write to out.json
14
+ class Export < Dry::CLI::Command
15
+ LONG_DESCRIPTION = "Export a flow definition as JSON or YAML.\n\n" \
16
+ "Example:\n inquirex export examples/08_tax_preparer.rb -f yml -o ."
17
+
18
+ SHORT_DESCRIPTION = "Export a flow definition as JSON or YAML ."
19
+
20
+ if ARGV[0] == "export"
21
+ desc LONG_DESCRIPTION
22
+ else
23
+ desc SHORT_DESCRIPTION
24
+ end
25
+
26
+ argument :flow_file,
27
+ required: true,
28
+ desc: "Path to flow definition (.rb file)"
29
+ option :format,
30
+ aliases: ["-f"],
31
+ default: "json",
32
+ values: %w[json yaml yml],
33
+ desc: "Output format"
34
+ option :output,
35
+ aliases: ["-o"],
36
+ desc: "Output file or directory (default: stdout)"
37
+
38
+ # @param flow_file [String]
39
+ # @param options [Hash]
40
+ # @return [void]
41
+ def call(flow_file:, **options)
42
+ definition = FlowLoader.load(flow_file)
43
+ format = normalize_format(options[:format])
44
+ content = serialize(definition, format)
45
+ write(content, flow_file, options[:output], extension_for(format))
46
+ rescue Inquirex::TTY::Error => e
47
+ warn "Error: #{e.message}"
48
+ exit 1
49
+ end
50
+
51
+ private
52
+
53
+ def normalize_format(format)
54
+ %w[yaml yml].include?(format.to_s) ? "yaml" : "json"
55
+ end
56
+
57
+ def serialize(definition, format)
58
+ case format
59
+ when "yaml" then definition.to_h.to_yaml
60
+ else JSON.pretty_generate(definition.to_h)
61
+ end
62
+ end
63
+
64
+ def extension_for(format)
65
+ format == "yaml" ? ".yml" : ".json"
66
+ end
67
+
68
+ def write(content, flow_file, output, extension)
69
+ path = OutputPath.resolve(flow_file, output, extension)
70
+ unless path
71
+ $stdout.puts content
72
+ return
73
+ end
74
+
75
+ File.write(path, content)
76
+ warn "Exported to #{path}"
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Inquirex
4
+ module TTY
5
+ module Commands
6
+ # Exports a flow definition as a Mermaid flowchart (stdout or file).
7
+ class Graph < Dry::CLI::Command
8
+ LONG_DESCRIPTION = "Export a flow definition as a Mermaid diagram source, an image, or both.\n " \
9
+ "Image generation requires mermaid-cli (npm install -g @mermaid-js/mermaid-cli)\n " \
10
+ "which this gem will attempt to install for you if mmdc command is not available.\n\n" \
11
+ "Example:\n inquirex graph qualify_dsl.rb --format both --output ~/Desktop --open"
12
+
13
+ SHORT_DESCRIPTION = "Export a flow definition as a Mermaid diagram source, an image, or both."
14
+
15
+ if ARGV[0] == "graph"
16
+ desc LONG_DESCRIPTION
17
+ else
18
+ desc SHORT_DESCRIPTION
19
+ end
20
+
21
+ argument :flow_file,
22
+ required: true,
23
+ desc: "Path to flow definition (.rb file)"
24
+ option :output,
25
+ aliases: ["-o"],
26
+ desc: "Output file or a directory (default: stdout)"
27
+ option :format,
28
+ aliases: ["-f"],
29
+ default: "source",
30
+ values: %w[source image both],
31
+ desc: "Output format"
32
+ option :open,
33
+ aliases: ["-p"],
34
+ default: false,
35
+ type: :boolean,
36
+ desc: "Open the SVG in a system viewer"
37
+
38
+ # @param flow_file [String]
39
+ # @param options [Hash]
40
+ # @return [void]
41
+ def call(flow_file:, **options)
42
+ definition = FlowLoader.load(flow_file)
43
+ source = Inquirex::Graph::MermaidExporter.new(definition).export
44
+ format = options[:format] || "source"
45
+ output = options[:output]
46
+
47
+ case format
48
+ when "source"
49
+ write_source(source, OutputPath.resolve(flow_file, output, ".mmd"))
50
+ when "image"
51
+ write_image(source, OutputPath.resolve_with_default(flow_file, output, ".png"), options[:open])
52
+ when "both"
53
+ write_source(source, OutputPath.resolve(flow_file, output, ".mmd"))
54
+ write_image(source, OutputPath.resolve_with_default(flow_file, output, ".png"), options[:open])
55
+ end
56
+ rescue Inquirex::TTY::Error => e
57
+ warn "Error: #{e.message}"
58
+ exit 1
59
+ end
60
+
61
+ private
62
+
63
+ def write_source(source, output_path)
64
+ unless output_path
65
+ $stdout.puts source
66
+ return
67
+ end
68
+ File.write(output_path, source)
69
+ warn "Diagram written to #{output_path}"
70
+ end
71
+
72
+ def write_image(source, output_path, open_file)
73
+ ensure_mermaid_cli_installed!
74
+
75
+ Tempfile.create(%w[inquirex-graph .mmd]) do |temp|
76
+ temp.write(source)
77
+ temp.flush
78
+ run_system_command!(
79
+ "mmdc -w 2000 -i #{Shellwords.escape(temp.path)} -o #{Shellwords.escape(output_path)}",
80
+ "Failed to generate image (mmdc)"
81
+ )
82
+ end
83
+
84
+ warn "Diagram written to #{output_path}"
85
+ open_image_file(output_path) if open_file
86
+ end
87
+
88
+ def ensure_mermaid_cli_installed!
89
+ return if command_available?("mmdc")
90
+
91
+ warn "Installing @mermaid-js/mermaid-cli..."
92
+ installed = system("npm install -g @mermaid-js/mermaid-cli")
93
+ return if installed && command_available?("mmdc")
94
+
95
+ raise Inquirex::TTY::Error,
96
+ "Could not install mermaid-cli. Run: npm install -g @mermaid-js/mermaid-cli"
97
+ end
98
+
99
+ def open_image_file(filename)
100
+ run_system_command!(
101
+ "open #{Shellwords.escape(filename)}",
102
+ "Failed to open #{filename}"
103
+ )
104
+ end
105
+
106
+ def command_available?(command)
107
+ system("command -v #{command} > /dev/null 2>&1")
108
+ end
109
+
110
+ def run_system_command!(command, error_message)
111
+ raise Inquirex::TTY::Error, error_message unless system(command)
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,193 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Inquirex
4
+ module TTY
5
+ module Commands
6
+ # Runs a flow definition interactively via TTY prompts and writes the
7
+ # collected answers (plus metadata) as JSON to stderr or a file.
8
+ class Run < Dry::CLI::Command
9
+ desc "Run a flow definition interactively"
10
+
11
+ argument :flow_file, required: true, desc: "Path to flow definition (.rb file)"
12
+ option :output, aliases: ["-o"], desc: "Write JSON results to this file instead of stderr"
13
+
14
+ # @param flow_file [String]
15
+ # @param options [Hash]
16
+ # @return [void]
17
+ def call(flow_file:, **options)
18
+ engine = run_flow(flow_file)
19
+ json = JSON.pretty_generate(build_result(flow_file, engine))
20
+
21
+ if options[:output]
22
+ File.write(options[:output], json)
23
+ puts "\nResults saved to #{options[:output]}"
24
+ else
25
+ $stderr.puts json # rubocop:disable Style/StderrPuts
26
+ end
27
+ rescue Inquirex::TTY::Error => e
28
+ error(e.message)
29
+ exit 1
30
+ rescue Inquirex::Errors::Error => e
31
+ error("Engine error: #{e.message}")
32
+ exit 1
33
+ end
34
+
35
+ private
36
+
37
+ # @param flow_file [String]
38
+ # @return [Inquirex::Engine]
39
+ def run_flow(flow_file)
40
+ load_dotenv!
41
+ definition = FlowLoader.load(flow_file)
42
+ engine = Inquirex::Engine.new(definition)
43
+ renderer = Renderer.new
44
+ adapter = build_llm_adapter
45
+
46
+ show_banner(definition)
47
+
48
+ until engine.finished?
49
+ step = engine.current_step
50
+ next_step(engine.current_step_id, engine.history.length + 1)
51
+
52
+ if step.respond_to?(:llm_verb?) && step.llm_verb?
53
+ run_llm_step(engine, step, adapter, renderer)
54
+ elsif step.display?
55
+ renderer.render(step)
56
+ engine.advance
57
+ else
58
+ engine.answer(renderer.render(step))
59
+ end
60
+ end
61
+
62
+ engine
63
+ end
64
+
65
+ # Calls the LLM adapter for a clarify/describe/summarize/detour step,
66
+ # stores the result as the step's answer, and for clarify verbs splats
67
+ # extracted fields into top-level answers via Engine#prefill! so that
68
+ # downstream `skip_if not_empty(:key)` rules will fire.
69
+ def run_llm_step(engine, step, adapter, renderer)
70
+ renderer.thinking("🧠 Thinking — asking #{adapter_label(adapter)} to extract structured data…")
71
+ result = adapter.call(step, engine.answers)
72
+ engine.answer(result)
73
+ if step.verb == :clarify && result.is_a?(Hash)
74
+ engine.prefill!(result)
75
+ renderer.show_extraction(result)
76
+ else
77
+ puts "\n#{result.is_a?(String) ? result : JSON.pretty_generate(result)}\n"
78
+ end
79
+ rescue StandardError => e
80
+ error("LLM step #{step.id} failed: #{e.class}: #{e.message}")
81
+ raise
82
+ end
83
+
84
+ # Adapter preference order (first match wins):
85
+ # INQUIREX_LLM_ADAPTER=null → NullAdapter (forced offline)
86
+ # INQUIREX_LLM_ADAPTER=anthropic → AnthropicAdapter (fails if no key)
87
+ # INQUIREX_LLM_ADAPTER=openai → OpenAIAdapter (fails if no key)
88
+ # ANTHROPIC_API_KEY present → AnthropicAdapter
89
+ # OPENAI_API_KEY present → OpenAIAdapter
90
+ # otherwise → NullAdapter
91
+ def build_llm_adapter
92
+ forced = ENV["INQUIREX_LLM_ADAPTER"].to_s.downcase
93
+ return Inquirex::LLM::NullAdapter.new if forced == "null"
94
+ return Inquirex::LLM::AnthropicAdapter.new if forced == "anthropic"
95
+ return Inquirex::LLM::OpenAIAdapter.new if forced == "openai"
96
+
97
+ if env_key?("ANTHROPIC_API_KEY") && defined?(Inquirex::LLM::AnthropicAdapter)
98
+ Inquirex::LLM::AnthropicAdapter.new
99
+ elsif env_key?("OPENAI_API_KEY") && defined?(Inquirex::LLM::OpenAIAdapter)
100
+ Inquirex::LLM::OpenAIAdapter.new
101
+ else
102
+ Inquirex::LLM::NullAdapter.new
103
+ end
104
+ end
105
+
106
+ def env_key?(name)
107
+ v = ENV.fetch(name, nil)
108
+ !v.nil? && !v.empty?
109
+ end
110
+
111
+ def adapter_label(adapter)
112
+ case adapter
113
+ when Inquirex::LLM::AnthropicAdapter then "Claude"
114
+ when Inquirex::LLM::OpenAIAdapter then "GPT"
115
+ else "the null adapter"
116
+ end
117
+ rescue NameError
118
+ "the null adapter"
119
+ end
120
+
121
+ # Minimal .env loader. Walks up from the cwd and the flow file's
122
+ # directory, loading every .env found along the way. Later loads do
123
+ # not overwrite earlier values, and nothing overrides keys already
124
+ # set in the real environment.
125
+ def load_dotenv!
126
+ seen = {}
127
+ [Dir.pwd, File.dirname(File.expand_path(ARGV.last.to_s))].uniq.each do |start|
128
+ walk_ancestors(start).each do |dir|
129
+ path = File.join(dir, ".env")
130
+ next if seen[path] || !File.file?(path)
131
+
132
+ seen[path] = true
133
+ load_env_file(path)
134
+ end
135
+ end
136
+ end
137
+
138
+ def walk_ancestors(dir)
139
+ ancestors = []
140
+ current = File.expand_path(dir)
141
+ loop do
142
+ ancestors << current
143
+ parent = File.dirname(current)
144
+ break if parent == current
145
+
146
+ current = parent
147
+ end
148
+ ancestors
149
+ end
150
+
151
+ def load_env_file(path)
152
+ File.foreach(path) do |line|
153
+ next if line.strip.empty? || line.start_with?("#")
154
+
155
+ key, _, value = line.strip.partition("=")
156
+ next if key.empty?
157
+ # Preserve shell-set values, but fill in keys that are unset or
158
+ # set to the empty string.
159
+ next if ENV.key?(key) && !ENV[key].to_s.empty?
160
+
161
+ ENV[key] = value.gsub(/\A["']|["']\z/, "")
162
+ end
163
+ end
164
+
165
+ # @param definition [Inquirex::Definition]
166
+ # @return [void]
167
+ def show_banner(definition)
168
+ title = definition.meta&.fetch(:title, nil) || "Inquirex"
169
+ font = ::TTY::Font.new(:standard)
170
+ title.split.each do |word|
171
+ puts pastel.bright_yellow(font.write(word))
172
+ end
173
+ sep(:green, "❚")
174
+ rescue StandardError
175
+ puts box(definition.meta&.fetch(:title, nil) || "Inquirex Wizard")
176
+ end
177
+
178
+ # @param flow_file [String]
179
+ # @param engine [Inquirex::Engine]
180
+ # @return [Hash]
181
+ def build_result(flow_file, engine)
182
+ {
183
+ flow_file: flow_file,
184
+ path_taken: engine.history,
185
+ answers: engine.answers,
186
+ steps_completed: engine.history.length,
187
+ completed_at: Time.now.iso8601
188
+ }
189
+ end
190
+ end
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Inquirex
4
+ module TTY
5
+ module Commands
6
+ # Validates a flow definition file: start step exists, all transition targets
7
+ # are known, and every step is reachable from the start step.
8
+ class Validate < Dry::CLI::Command
9
+ desc "Validate a flow definition file"
10
+
11
+ argument :flow_file, required: true, desc: "Path to flow definition (.rb file)"
12
+
13
+ # @param flow_file [String]
14
+ # @return [void]
15
+ def call(flow_file:, **)
16
+ definition = FlowLoader.load(flow_file)
17
+ errors = validate_definition(definition)
18
+
19
+ if errors.empty?
20
+ print_success(definition)
21
+ else
22
+ print_errors(errors)
23
+ exit 1
24
+ end
25
+ rescue Inquirex::TTY::Error => e
26
+ warn "Error: #{e.message}"
27
+ exit 1
28
+ rescue Inquirex::Errors::Error => e
29
+ warn "Definition error: #{e.message}"
30
+ exit 1
31
+ end
32
+
33
+ private
34
+
35
+ def print_success(definition)
36
+ puts "Flow definition is valid!"
37
+ puts " ID: #{definition.id || "(none)"}"
38
+ puts " Version: #{definition.version}"
39
+ puts " Start step: #{definition.start_step_id}"
40
+ puts " Total steps: #{definition.step_ids.length}"
41
+ puts " Steps: #{definition.step_ids.join(", ")}"
42
+ print_meta(definition.meta) if definition.meta && !definition.meta.empty?
43
+ end
44
+
45
+ def print_meta(meta)
46
+ puts " Title: #{meta[:title]}" if meta[:title]
47
+ puts " Subtitle: #{meta[:subtitle]}" if meta[:subtitle]
48
+ end
49
+
50
+ def print_errors(errors)
51
+ warn "Flow definition has #{errors.length} error(s):"
52
+ errors.each { |e| warn " - #{e}" }
53
+ end
54
+
55
+ def validate_definition(definition)
56
+ errors = []
57
+ validate_start_step(definition, errors)
58
+ validate_transition_targets(definition, errors)
59
+ validate_reachability(definition, errors)
60
+ errors
61
+ end
62
+
63
+ def validate_start_step(definition, errors)
64
+ return if definition.step_ids.include?(definition.start_step_id)
65
+
66
+ errors << "Start step :#{definition.start_step_id} not found in steps"
67
+ end
68
+
69
+ def validate_transition_targets(definition, errors)
70
+ definition.step_ids.each do |step_id|
71
+ step = definition.step(step_id)
72
+ step.transitions.each do |transition|
73
+ next if definition.step_ids.include?(transition.target)
74
+
75
+ errors << "Step :#{step_id} transitions to unknown step :#{transition.target}"
76
+ end
77
+ end
78
+ end
79
+
80
+ def validate_reachability(definition, errors)
81
+ reachable = find_reachable_steps(definition)
82
+ orphans = definition.step_ids - reachable
83
+ orphans.each { |o| errors << "Step :#{o} is unreachable from :#{definition.start_step_id}" }
84
+ end
85
+
86
+ def find_reachable_steps(definition)
87
+ visited = Set.new
88
+ queue = [definition.start_step_id]
89
+ known = definition.step_ids
90
+
91
+ until queue.empty?
92
+ current = queue.shift
93
+ next if visited.include?(current)
94
+
95
+ visited << current
96
+ next unless known.include?(current)
97
+
98
+ definition.step(current).transitions.each do |t|
99
+ queue << t.target if known.include?(t.target)
100
+ end
101
+ end
102
+
103
+ visited.to_a
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Inquirex
4
+ module TTY
5
+ module Commands
6
+ # Prints inquirex-tty and inquirex gem versions to stdout.
7
+ class Version < Dry::CLI::Command
8
+ desc "Print version information"
9
+
10
+ # @param **_ [Hash] ignored options
11
+ # @return [void]
12
+ def call(**)
13
+ puts "inquirex-tty #{Inquirex::TTY::VERSION}"
14
+ puts "inquirex #{Inquirex::VERSION}"
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Inquirex
4
+ module TTY
5
+ # Dry::CLI registry for inquirex-tty subcommands.
6
+ # UIHelper is mixed into every command so box/sep/next_step are available.
7
+ module Commands
8
+ extend Dry::CLI::Registry
9
+
10
+ ::Dry::CLI::Command.include(UIHelper)
11
+
12
+ register "run", Run
13
+ register "graph", Graph
14
+ register "export", Export
15
+ register "validate", Validate
16
+ register "version", Version
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Inquirex
4
+ module TTY
5
+ # Loads a flow definition from a Ruby file by evaluating it in a top-level
6
+ # binding. The file is expected to call +Inquirex.define+ and return an
7
+ # +Inquirex::Definition+.
8
+ class FlowLoader
9
+ # @param path [String] path to a .rb flow definition file
10
+ # @return [Inquirex::Definition]
11
+ # @raise [Inquirex::TTY::Error] if file is missing, not .rb, or has syntax errors
12
+ def self.load(path)
13
+ new(path).load
14
+ end
15
+
16
+ # @param path [String] path (will be expanded)
17
+ def initialize(path)
18
+ @path = File.expand_path(path)
19
+ validate_path!
20
+ end
21
+
22
+ # @return [Inquirex::Definition]
23
+ # @raise [Inquirex::TTY::Error] on syntax error
24
+ def load
25
+ content = File.read(@path)
26
+ # rubocop:disable Security/Eval
27
+ eval(content, TOPLEVEL_BINDING.dup, @path, 1)
28
+ # rubocop:enable Security/Eval
29
+ rescue SyntaxError => e
30
+ raise Inquirex::TTY::Error, "Syntax error in #{@path}: #{e.message}"
31
+ end
32
+
33
+ private
34
+
35
+ def validate_path!
36
+ raise Inquirex::TTY::Error, "File not found: #{@path}" unless File.exist?(@path)
37
+ raise Inquirex::TTY::Error, "Not a .rb file: #{@path}" unless @path.end_with?(".rb")
38
+ end
39
+ end
40
+ end
41
+ end