flowengine-cli 0.1.0 → 0.1.2
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/.rubocop_todo.yml +16 -1
- data/README.md +675 -33
- data/Rakefile +11 -12
- data/examples/01_hello_world.rb +29 -0
- data/examples/02_yes_or_no.rb +31 -0
- data/examples/03_food_preferences.rb +52 -0
- data/examples/04_event_registration.rb +75 -0
- data/examples/05_job_application.rb +103 -0
- data/examples/06_health_assessment.rb +153 -0
- data/examples/07_loan_application.rb +187 -0
- data/examples/08_intake.rb +33 -0
- data/justfile +39 -2
- data/lib/flowengine/cli/commands/graph.rb +4 -0
- data/lib/flowengine/cli/commands/run.rb +22 -28
- data/lib/flowengine/cli/commands/validate_flow.rb +26 -0
- data/lib/flowengine/cli/commands/version.rb +3 -0
- data/lib/flowengine/cli/commands.rb +4 -0
- data/lib/flowengine/cli/flow_loader.rb +12 -0
- data/lib/flowengine/cli/renderer.rb +40 -0
- data/lib/flowengine/cli/ui_helper.rb +82 -0
- data/lib/flowengine/cli/version.rb +2 -1
- data/lib/flowengine/cli.rb +3 -0
- metadata +123 -2
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# This is the first example in the README
|
|
2
|
+
|
|
3
|
+
FlowEngine.define do
|
|
4
|
+
start :filing_status
|
|
5
|
+
|
|
6
|
+
step :filing_status do
|
|
7
|
+
type :single_select
|
|
8
|
+
question "What is your filing status?"
|
|
9
|
+
options %w[Single Married HeadOfHousehold]
|
|
10
|
+
transition to: :income_types
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
step :income_types do
|
|
14
|
+
type :multi_select
|
|
15
|
+
question "Select all income types that apply:"
|
|
16
|
+
options %w[W2 1099 Business Investment Rental]
|
|
17
|
+
transition to: :business_details, if_rule: contains(:income_types, "Business")
|
|
18
|
+
transition to: :summary
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
step :business_details do
|
|
22
|
+
type :number_matrix
|
|
23
|
+
question "How many of each business type?"
|
|
24
|
+
fields %w[LLC SCorp CCorp]
|
|
25
|
+
transition to: :summary
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
step :summary do
|
|
29
|
+
type :header
|
|
30
|
+
decorations("Success!")
|
|
31
|
+
question "Thank you for completing the intake!"
|
|
32
|
+
end
|
|
33
|
+
end
|
data/justfile
CHANGED
|
@@ -2,10 +2,18 @@ set shell := ["bash", "-c"]
|
|
|
2
2
|
|
|
3
3
|
set dotenv-load
|
|
4
4
|
|
|
5
|
+
[no-exit-message]
|
|
6
|
+
recipes:
|
|
7
|
+
@just --choose
|
|
8
|
+
|
|
5
9
|
# Boot the app
|
|
6
10
|
test:
|
|
11
|
+
@echo "Ensuring bundle install is up to date..."
|
|
7
12
|
@bundle check || bundle install -j 12
|
|
13
|
+
@echo "Running specs..."
|
|
8
14
|
@bundle exec rspec --format documentation
|
|
15
|
+
@echo "Running rubocop..."
|
|
16
|
+
@bundle exec rubocop
|
|
9
17
|
|
|
10
18
|
# Setup Ruby dependencies
|
|
11
19
|
setup-ruby:
|
|
@@ -29,15 +37,44 @@ setup: setup-node setup-ruby
|
|
|
29
37
|
cli *ARGS:
|
|
30
38
|
./cli {{ARGS}}
|
|
31
39
|
|
|
32
|
-
|
|
33
|
-
|
|
40
|
+
# Run a flow interactively: just run examples/01_hello_world.rb
|
|
41
|
+
run file *ARGS:
|
|
42
|
+
@bundle exec exe/flowengine-cli run {{file}} {{ARGS}}
|
|
43
|
+
|
|
44
|
+
# Export a flow as a Mermaid diagram: just graph examples/07_loan_application.rb
|
|
45
|
+
graph file *ARGS:
|
|
46
|
+
@bundle exec exe/flowengine-cli graph {{file}} {{ARGS}}
|
|
47
|
+
|
|
48
|
+
# Validate a flow definition: just validate examples/04_event_registration.rb
|
|
49
|
+
validate file:
|
|
50
|
+
@bundle exec exe/flowengine-cli validate {{file}}
|
|
34
51
|
|
|
52
|
+
# List available example flows
|
|
53
|
+
examples:
|
|
54
|
+
#!/usr/bin/env bash
|
|
55
|
+
if command fd>/dev/null 2>&1; then
|
|
56
|
+
fd --type file '.rb$' examples/
|
|
57
|
+
else
|
|
58
|
+
find examples -type f -name '*.rb'
|
|
59
|
+
fi
|
|
60
|
+
echo
|
|
61
|
+
echo "To run an example, type 'flow run examples/<example-file.rb>'"
|
|
62
|
+
|
|
63
|
+
examples-list:
|
|
64
|
+
|
|
65
|
+
# Formats minor syntax issues and writes the rest into a TODO file
|
|
35
66
|
format:
|
|
36
67
|
@bundle exec rubocop -a
|
|
37
68
|
@bundle exec rubocop --auto-gen-config
|
|
38
69
|
|
|
70
|
+
# Run all linters, in this case just rubocop
|
|
39
71
|
lint:
|
|
40
72
|
@bundle exec rubocop
|
|
41
73
|
|
|
74
|
+
# Generates YARD documentation into the ./doc folder, and opens ./doc/index.html
|
|
75
|
+
doc:
|
|
76
|
+
@rake doc
|
|
77
|
+
@open doc/index.html
|
|
78
|
+
|
|
42
79
|
check-all: lint test
|
|
43
80
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
module FlowEngine
|
|
4
4
|
module CLI
|
|
5
5
|
module Commands
|
|
6
|
+
# Exports a flow definition as a Mermaid flowchart (stdout or file).
|
|
6
7
|
class Graph < Dry::CLI::Command
|
|
7
8
|
desc "Export a flow definition as a Mermaid diagram"
|
|
8
9
|
|
|
@@ -10,6 +11,9 @@ module FlowEngine
|
|
|
10
11
|
option :output, aliases: ["-o"], desc: "Output file (default: stdout)"
|
|
11
12
|
option :format, default: "mermaid", desc: "Output format (mermaid)"
|
|
12
13
|
|
|
14
|
+
# @param flow_file [String] path to the flow definition .rb file
|
|
15
|
+
# @param options [Hash] :output => path to write diagram, :format => "mermaid"
|
|
16
|
+
# @return [void]
|
|
13
17
|
def call(flow_file:, **options)
|
|
14
18
|
definition = FlowLoader.load(flow_file)
|
|
15
19
|
|
|
@@ -7,45 +7,56 @@ require "tty-screen"
|
|
|
7
7
|
module FlowEngine
|
|
8
8
|
module CLI
|
|
9
9
|
module Commands
|
|
10
|
+
# Runs a flow definition interactively via TTY prompts and writes the
|
|
11
|
+
# collected answers (and metadata) as JSON to stdout or a file.
|
|
10
12
|
class Run < Dry::CLI::Command
|
|
11
13
|
desc "Run a flow definition interactively"
|
|
12
14
|
|
|
13
15
|
argument :flow_file, required: true, desc: "Path to flow definition (.rb file)"
|
|
14
16
|
option :output, aliases: ["-o"], desc: "Output file for JSON results"
|
|
15
17
|
|
|
18
|
+
# @param flow_file [String] path to the flow definition .rb file
|
|
19
|
+
# @param options [Hash] :output => path to write JSON (optional)
|
|
20
|
+
# @return [void]
|
|
16
21
|
def call(flow_file:, **options)
|
|
17
22
|
engine = run_flow(flow_file)
|
|
18
23
|
json_output = JSON.pretty_generate(build_result(flow_file, engine))
|
|
19
24
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
if options[:output]
|
|
26
|
+
write_output(options[:output], json_output)
|
|
27
|
+
else
|
|
28
|
+
$stderr.puts json_output # rubocop:disable Style/StderrPuts
|
|
29
|
+
end
|
|
24
30
|
rescue FlowEngine::CLI::Error => e
|
|
25
|
-
|
|
31
|
+
error(e.message)
|
|
26
32
|
exit 1
|
|
27
33
|
rescue FlowEngine::Error => e
|
|
28
|
-
|
|
34
|
+
error("Engine error: #{e.message}")
|
|
29
35
|
exit 1
|
|
30
36
|
end
|
|
31
37
|
|
|
32
38
|
private
|
|
33
39
|
|
|
40
|
+
# @param flow_file [String] path to flow definition
|
|
41
|
+
# @return [FlowEngine::Engine] engine after completion
|
|
34
42
|
def run_flow(flow_file)
|
|
35
43
|
definition = FlowLoader.load(flow_file)
|
|
36
44
|
engine = FlowEngine::Engine.new(definition)
|
|
37
45
|
renderer = Renderer.new
|
|
38
46
|
|
|
39
|
-
|
|
47
|
+
box("FlowEngine Interactive Wizard")
|
|
40
48
|
|
|
41
49
|
until engine.finished?
|
|
42
|
-
|
|
50
|
+
next_step(engine.current_step_id, engine.history.length)
|
|
43
51
|
engine.answer(renderer.render(engine.current_step))
|
|
44
52
|
end
|
|
45
53
|
|
|
46
54
|
engine
|
|
47
55
|
end
|
|
48
56
|
|
|
57
|
+
# @param flow_file [String] path used to load the flow
|
|
58
|
+
# @param engine [FlowEngine::Engine] completed engine
|
|
59
|
+
# @return [Hash] result hash with flow_file, path_taken, answers, etc.
|
|
49
60
|
def build_result(flow_file, engine)
|
|
50
61
|
{
|
|
51
62
|
flow_file: flow_file,
|
|
@@ -56,30 +67,13 @@ module FlowEngine
|
|
|
56
67
|
}
|
|
57
68
|
end
|
|
58
69
|
|
|
70
|
+
# @param path [String] output file path
|
|
71
|
+
# @param json_output [String] JSON string to write
|
|
72
|
+
# @return [void]
|
|
59
73
|
def write_output(path, json_output)
|
|
60
74
|
File.write(path, json_output)
|
|
61
75
|
puts "\nResults saved to #{path}"
|
|
62
76
|
end
|
|
63
|
-
|
|
64
|
-
def display_header(text)
|
|
65
|
-
width = [TTY::Screen.width, 80].min
|
|
66
|
-
puts TTY::Box.frame(text, width: width, padding: 1, align: :center, border: :thick)
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def display_step_indicator(step_id, step_number)
|
|
70
|
-
puts "\n Step #{step_number}: #{step_id}"
|
|
71
|
-
puts " #{"─" * 40}"
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def display_success(text)
|
|
75
|
-
width = [TTY::Screen.width, 80].min
|
|
76
|
-
puts TTY::Box.frame(text, width: width, padding: 1, align: :center, title: { top_left: " SUCCESS " })
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def display_error(text)
|
|
80
|
-
width = [TTY::Screen.width, 80].min
|
|
81
|
-
puts TTY::Box.frame(text, width: width, padding: 1, align: :center, title: { top_left: " ERROR " })
|
|
82
|
-
end
|
|
83
77
|
end
|
|
84
78
|
end
|
|
85
79
|
end
|
|
@@ -3,11 +3,16 @@
|
|
|
3
3
|
module FlowEngine
|
|
4
4
|
module CLI
|
|
5
5
|
module Commands
|
|
6
|
+
# Validates a flow definition file: start step exists, transitions point to
|
|
7
|
+
# known steps, and all steps are reachable from the start step.
|
|
6
8
|
class ValidateFlow < Dry::CLI::Command
|
|
7
9
|
desc "Validate a flow definition file"
|
|
8
10
|
|
|
9
11
|
argument :flow_file, required: true, desc: "Path to flow definition (.rb file)"
|
|
10
12
|
|
|
13
|
+
# @param flow_file [String] path to the flow definition .rb file
|
|
14
|
+
# @param **_ [Hash] ignored options
|
|
15
|
+
# @return [void]
|
|
11
16
|
def call(flow_file:, **)
|
|
12
17
|
definition = FlowLoader.load(flow_file)
|
|
13
18
|
errors = validate_definition(definition)
|
|
@@ -28,6 +33,8 @@ module FlowEngine
|
|
|
28
33
|
|
|
29
34
|
private
|
|
30
35
|
|
|
36
|
+
# @param definition [FlowEngine::Definition]
|
|
37
|
+
# @return [void]
|
|
31
38
|
def print_success(definition)
|
|
32
39
|
puts "Flow definition is valid!"
|
|
33
40
|
puts " Start step: #{definition.start_step_id}"
|
|
@@ -35,11 +42,15 @@ module FlowEngine
|
|
|
35
42
|
puts " Steps: #{definition.step_ids.join(", ")}"
|
|
36
43
|
end
|
|
37
44
|
|
|
45
|
+
# @param errors [Array<String>]
|
|
46
|
+
# @return [void]
|
|
38
47
|
def print_errors(errors)
|
|
39
48
|
warn "Flow definition has errors:"
|
|
40
49
|
errors.each { |e| warn " - #{e}" }
|
|
41
50
|
end
|
|
42
51
|
|
|
52
|
+
# @param definition [FlowEngine::Definition]
|
|
53
|
+
# @return [Array<String>] list of error messages
|
|
43
54
|
def validate_definition(definition)
|
|
44
55
|
errors = []
|
|
45
56
|
|
|
@@ -50,12 +61,18 @@ module FlowEngine
|
|
|
50
61
|
errors
|
|
51
62
|
end
|
|
52
63
|
|
|
64
|
+
# @param definition [FlowEngine::Definition]
|
|
65
|
+
# @param errors [Array<String>] mutated with new errors
|
|
66
|
+
# @return [void]
|
|
53
67
|
def validate_start_step(definition, errors)
|
|
54
68
|
return if definition.step_ids.include?(definition.start_step_id)
|
|
55
69
|
|
|
56
70
|
errors << "Start step :#{definition.start_step_id} not found in steps"
|
|
57
71
|
end
|
|
58
72
|
|
|
73
|
+
# @param definition [FlowEngine::Definition]
|
|
74
|
+
# @param errors [Array<String>] mutated with new errors
|
|
75
|
+
# @return [void]
|
|
59
76
|
def validate_transition_targets(definition, errors)
|
|
60
77
|
definition.step_ids.each do |step_id|
|
|
61
78
|
step = definition.step(step_id)
|
|
@@ -67,6 +84,9 @@ module FlowEngine
|
|
|
67
84
|
end
|
|
68
85
|
end
|
|
69
86
|
|
|
87
|
+
# @param definition [FlowEngine::Definition]
|
|
88
|
+
# @param errors [Array<String>] mutated with new errors
|
|
89
|
+
# @return [void]
|
|
70
90
|
def validate_reachability(definition, errors)
|
|
71
91
|
reachable = find_reachable_steps(definition)
|
|
72
92
|
orphans = definition.step_ids - reachable
|
|
@@ -76,6 +96,8 @@ module FlowEngine
|
|
|
76
96
|
end
|
|
77
97
|
end
|
|
78
98
|
|
|
99
|
+
# @param definition [FlowEngine::Definition]
|
|
100
|
+
# @return [Array<Symbol>] step ids reachable from start
|
|
79
101
|
def find_reachable_steps(definition)
|
|
80
102
|
visited = Set.new
|
|
81
103
|
queue = [definition.start_step_id]
|
|
@@ -94,6 +116,10 @@ module FlowEngine
|
|
|
94
116
|
visited.to_a
|
|
95
117
|
end
|
|
96
118
|
|
|
119
|
+
# @param step [FlowEngine::Node]
|
|
120
|
+
# @param known_ids [Array<Symbol>]
|
|
121
|
+
# @param queue [Array] mutated with transition targets
|
|
122
|
+
# @return [void]
|
|
97
123
|
def enqueue_transitions(step, known_ids, queue)
|
|
98
124
|
step.transitions.each do |t|
|
|
99
125
|
queue << t.target if known_ids.include?(t.target)
|
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
module FlowEngine
|
|
4
4
|
module CLI
|
|
5
5
|
module Commands
|
|
6
|
+
# Prints flowengine-cli and flowengine gem versions to stdout.
|
|
6
7
|
class Version < Dry::CLI::Command
|
|
7
8
|
desc "Print version information"
|
|
8
9
|
|
|
10
|
+
# @param **_ [Hash] ignored options
|
|
11
|
+
# @return [void]
|
|
9
12
|
def call(**)
|
|
10
13
|
puts "flowengine-cli #{FlowEngine::CLI::VERSION}"
|
|
11
14
|
puts "flowengine #{FlowEngine::VERSION}"
|
|
@@ -5,12 +5,16 @@ require_relative "commands/run"
|
|
|
5
5
|
require_relative "commands/graph"
|
|
6
6
|
require_relative "commands/validate_flow"
|
|
7
7
|
require_relative "commands/version"
|
|
8
|
+
require_relative "ui_helper"
|
|
8
9
|
|
|
9
10
|
module FlowEngine
|
|
10
11
|
module CLI
|
|
12
|
+
# Dry::CLI registry for flowengine-cli subcommands: run, graph, validate, version.
|
|
11
13
|
module Commands
|
|
12
14
|
extend Dry::CLI::Registry
|
|
13
15
|
|
|
16
|
+
::Dry::CLI::Command.include(UIHelper)
|
|
17
|
+
|
|
14
18
|
register "run", Run
|
|
15
19
|
register "graph", Graph
|
|
16
20
|
register "validate", ValidateFlow
|
|
@@ -2,16 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
module FlowEngine
|
|
4
4
|
module CLI
|
|
5
|
+
# Loads a flow definition from a Ruby file by evaluating it in a top-level
|
|
6
|
+
# binding. Expects the file to define a flow via FlowEngine.define and
|
|
7
|
+
# return a FlowEngine::Definition.
|
|
5
8
|
class FlowLoader
|
|
9
|
+
# Load a flow definition from a file.
|
|
10
|
+
# @param path [String] path to a .rb flow definition file
|
|
11
|
+
# @return [FlowEngine::Definition] the evaluated definition
|
|
12
|
+
# @raise [FlowEngine::CLI::Error] if file is missing, not .rb, or has syntax errors
|
|
6
13
|
def self.load(path)
|
|
7
14
|
new(path).load
|
|
8
15
|
end
|
|
9
16
|
|
|
17
|
+
# @param path [String] path to the flow definition file (will be expanded)
|
|
10
18
|
def initialize(path)
|
|
11
19
|
@path = File.expand_path(path)
|
|
12
20
|
validate_path!
|
|
13
21
|
end
|
|
14
22
|
|
|
23
|
+
# Evaluates the file and returns the resulting definition.
|
|
24
|
+
# @return [FlowEngine::Definition]
|
|
25
|
+
# @raise [FlowEngine::CLI::Error] on syntax error
|
|
15
26
|
def load
|
|
16
27
|
content = File.read(@path)
|
|
17
28
|
# Evaluate in a clean binding that has FlowEngine available
|
|
@@ -24,6 +35,7 @@ module FlowEngine
|
|
|
24
35
|
|
|
25
36
|
private
|
|
26
37
|
|
|
38
|
+
# @raise [FlowEngine::CLI::Error] if path does not exist or does not end with .rb
|
|
27
39
|
def validate_path!
|
|
28
40
|
raise FlowEngine::CLI::Error, "File not found: #{@path}" unless File.exist?(@path)
|
|
29
41
|
raise FlowEngine::CLI::Error, "Not a .rb file: #{@path}" unless @path.end_with?(".rb")
|
|
@@ -1,16 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "tty-prompt"
|
|
4
|
+
require_relative "ui_helper"
|
|
4
5
|
|
|
5
6
|
module FlowEngine
|
|
6
7
|
module CLI
|
|
8
|
+
# Renders flow steps as TTY prompts. Dispatches to a type-specific renderer
|
|
9
|
+
# (e.g. +render_multi_select+, +render_single_select+) or falls back to
|
|
10
|
+
# +render_text+ for unknown node types.
|
|
7
11
|
class Renderer
|
|
12
|
+
# @return [TTY::Prompt] the TTY prompt instance used for input
|
|
8
13
|
attr_reader :prompt
|
|
9
14
|
|
|
15
|
+
include ::FlowEngine::CLI::UIHelper
|
|
16
|
+
|
|
17
|
+
# @param prompt [TTY::Prompt] prompt instance (default: new TTY::Prompt)
|
|
10
18
|
def initialize(prompt: TTY::Prompt.new)
|
|
11
19
|
@prompt = prompt
|
|
12
20
|
end
|
|
13
21
|
|
|
22
|
+
# Renders a single step node and returns the user's answer.
|
|
23
|
+
# @param node [FlowEngine::Node] the current step node from the flow definition
|
|
24
|
+
# @return [Object] answer value (String, Integer, Boolean, Array, Hash, or nil)
|
|
14
25
|
def render(node)
|
|
15
26
|
method_name = :"render_#{node.type}"
|
|
16
27
|
|
|
@@ -23,14 +34,20 @@ module FlowEngine
|
|
|
23
34
|
|
|
24
35
|
private
|
|
25
36
|
|
|
37
|
+
# @param node [FlowEngine::Node] multi_select step
|
|
38
|
+
# @return [Array<String>]
|
|
26
39
|
def render_multi_select(node)
|
|
27
40
|
prompt.multi_select(node.question, node.options, min: 1)
|
|
28
41
|
end
|
|
29
42
|
|
|
43
|
+
# @param node [FlowEngine::Node] single_select step
|
|
44
|
+
# @return [String]
|
|
30
45
|
def render_single_select(node)
|
|
31
46
|
prompt.select(node.question, node.options)
|
|
32
47
|
end
|
|
33
48
|
|
|
49
|
+
# @param node [FlowEngine::Node] number_matrix step
|
|
50
|
+
# @return [Hash<String, Integer>]
|
|
34
51
|
def render_number_matrix(node)
|
|
35
52
|
puts "\n#{node.question}\n\n"
|
|
36
53
|
result = {}
|
|
@@ -40,21 +57,44 @@ module FlowEngine
|
|
|
40
57
|
result
|
|
41
58
|
end
|
|
42
59
|
|
|
60
|
+
# @param node [FlowEngine::Node] text step
|
|
61
|
+
# @return [String, nil]
|
|
43
62
|
def render_text(node)
|
|
44
63
|
prompt.ask(node.question)
|
|
45
64
|
end
|
|
46
65
|
|
|
66
|
+
# @param node [FlowEngine::Node] number step
|
|
67
|
+
# @return [Integer, nil]
|
|
47
68
|
def render_number(node)
|
|
48
69
|
prompt.ask(node.question, convert: :int)
|
|
49
70
|
end
|
|
50
71
|
|
|
72
|
+
# @param node [FlowEngine::Node] boolean step
|
|
73
|
+
# @return [Boolean]
|
|
51
74
|
def render_boolean(node) # rubocop:disable Naming/PredicateMethod
|
|
52
75
|
prompt.yes?(node.question)
|
|
53
76
|
end
|
|
54
77
|
|
|
78
|
+
# @param node [FlowEngine::Node] display step (informational, no answer)
|
|
79
|
+
# @return [nil]
|
|
55
80
|
def render_display(node)
|
|
56
81
|
puts "\n#{node.question}\n"
|
|
57
82
|
prompt.keypress("Press any key to continue...")
|
|
83
|
+
sep(:green, "━")
|
|
84
|
+
nil
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Renders a header node with a title and a separator.
|
|
88
|
+
# @param node [FlowEngine::Node] header step (optional decorations)
|
|
89
|
+
# @return [nil]
|
|
90
|
+
def render_header(node)
|
|
91
|
+
title = node.respond_to?(:decorations) ? node.decorations : nil
|
|
92
|
+
opts = {}
|
|
93
|
+
opts[:title] = { style: { top_left: title } } if title
|
|
94
|
+
puts box(node.question, bg: :blue, fg: :white, **opts)
|
|
95
|
+
puts node.question
|
|
96
|
+
prompt.keypress("Press any key to continue...")
|
|
97
|
+
sep(:green, "━")
|
|
58
98
|
nil
|
|
59
99
|
end
|
|
60
100
|
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
|
4
|
+
|
|
5
|
+
require "forwardable"
|
|
6
|
+
require "tty-box"
|
|
7
|
+
require "tty-screen"
|
|
8
|
+
require "pastel"
|
|
9
|
+
|
|
10
|
+
module FlowEngine
|
|
11
|
+
module CLI
|
|
12
|
+
# Mixin that provides TTY::Box, Pastel, and screen helpers to CLI commands
|
|
13
|
+
# and the Renderer. Defines +box+, +sep+, +next_step+, +frame+, +info+,
|
|
14
|
+
# +success+, +error+, +warning+, and +width+ on the including class.
|
|
15
|
+
module UIHelper
|
|
16
|
+
class << self
|
|
17
|
+
# @return [Pastel] shared Pastel instance for colored output
|
|
18
|
+
def pastel
|
|
19
|
+
@pastel ||= Pastel.new
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def included(base)
|
|
23
|
+
base.extend(Forwardable)
|
|
24
|
+
# Forwardable needs an accessor (method returning the target).
|
|
25
|
+
# :TTY::Box / :TTY::Screen are not valid method names on the instance.
|
|
26
|
+
base.define_method(:tty_box) { ::TTY::Box }
|
|
27
|
+
base.define_method(:tty_screen) { ::TTY::Screen }
|
|
28
|
+
base.define_method(:pastel) { ::FlowEngine::CLI::UIHelper.pastel }
|
|
29
|
+
|
|
30
|
+
# TTY::Box uses :warn, not :warning
|
|
31
|
+
%i[frame info success error].each do |method|
|
|
32
|
+
base.define_method(method) { |*args, **kwargs| puts ::TTY::Box.send(method, *args, **kwargs) }
|
|
33
|
+
end
|
|
34
|
+
base.define_method(:warning) { |*args, **kwargs| puts ::TTY::Box.send(:warn, *args, **kwargs) }
|
|
35
|
+
|
|
36
|
+
base.def_delegators :tty_screen, :width
|
|
37
|
+
|
|
38
|
+
base.class_eval do
|
|
39
|
+
# Draw a bordered box with optional title.
|
|
40
|
+
# @param text [String] content to display
|
|
41
|
+
# @param title [String, nil] optional top-left title
|
|
42
|
+
# @param bg [Symbol] background color (e.g. :green, :blue)
|
|
43
|
+
# @param fg [Symbol] foreground color (e.g. :white)
|
|
44
|
+
# @return [void]
|
|
45
|
+
def box(text, title: nil, bg: :green, fg: :white) # rubocop:disable Naming/MethodParameterName
|
|
46
|
+
width = [width(), 80].min
|
|
47
|
+
args = {
|
|
48
|
+
width: width,
|
|
49
|
+
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
50
|
+
align: :center,
|
|
51
|
+
style: { fg: fg,
|
|
52
|
+
bg: bg,
|
|
53
|
+
border: { type: :thin, fg: fg, bg: bg } }
|
|
54
|
+
}
|
|
55
|
+
args[:title] = { top_left: title } if title
|
|
56
|
+
frame(text, **args)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Print step progress line and separator.
|
|
60
|
+
# @param step_id [Symbol, String] current step identifier
|
|
61
|
+
# @param step_number [Integer] 1-based step index
|
|
62
|
+
# @return [void]
|
|
63
|
+
def next_step(step_id, step_number)
|
|
64
|
+
puts pastel.yellow("Step #{step_number}: #{step_id}")
|
|
65
|
+
sep(:yellow, "━")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Print a horizontal separator line in the given color.
|
|
69
|
+
# @param color [Symbol] Pastel color (e.g. :yellow, :green)
|
|
70
|
+
# @param char [String] character to repeat (default "▪")
|
|
71
|
+
# @return [void]
|
|
72
|
+
def sep(color = :yellow, char = "▪")
|
|
73
|
+
puts pastel.send(color, (char * 80).to_s)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
data/lib/flowengine/cli.rb
CHANGED
|
@@ -7,7 +7,10 @@ require_relative "cli/renderer"
|
|
|
7
7
|
require_relative "cli/commands"
|
|
8
8
|
|
|
9
9
|
module FlowEngine
|
|
10
|
+
# Terminal UI adapter for flowengine: runs flows interactively via Dry::CLI
|
|
11
|
+
# and TTY components, and supports graph export and flow validation.
|
|
10
12
|
module CLI
|
|
13
|
+
# Raised when flowengine-cli encounters a load, validation, or runtime error.
|
|
11
14
|
class Error < StandardError; end
|
|
12
15
|
end
|
|
13
16
|
end
|