wisco 0.1.7
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 +7 -0
- data/bin/wisco +7 -0
- data/lib/wisco/commands/exec.rb +126 -0
- data/lib/wisco/commands/fixtures.rb +149 -0
- data/lib/wisco/commands/init.rb +63 -0
- data/lib/wisco/commands/list.rb +192 -0
- data/lib/wisco/commands/pull.rb +176 -0
- data/lib/wisco/commands/push.rb +129 -0
- data/lib/wisco/config.rb +43 -0
- data/lib/wisco/connector.rb +91 -0
- data/lib/wisco/path_utils.rb +57 -0
- data/lib/wisco/version.rb +3 -0
- data/lib/wisco/workato_api.rb +75 -0
- data/lib/wisco.rb +162 -0
- metadata +98 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 832da4703274d86e1bc2f5e1684e499b9c4f2de2628c3f0dfb362e23e34213b9
|
|
4
|
+
data.tar.gz: 369be3677fdc543c2a0d1a5344e195bf97e05a61d1a9816b6f1993bc521c63af
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 3c5e906f59c4f5db77e30a8c583e09cea24e02074c89dfb1b9f985a7c3078bf789f17e5723d49d303789f8b0328d1c5a2c8bf80b735ea1706c4834700eef8dcb
|
|
7
|
+
data.tar.gz: e474a3f6f3b761f3165366fb335c276f7d73ed2725d50a7dc2bea9fde92decf13b10991a809bc86b2737dbbc558284e8617c4301f321c67dadfe00e127e68eee
|
data/bin/wisco
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
require 'fileutils'
|
|
2
|
+
require 'workato/connector/sdk'
|
|
3
|
+
require 'workato/cli/exec_command'
|
|
4
|
+
require_relative '../config'
|
|
5
|
+
require_relative '../connector'
|
|
6
|
+
require_relative '../path_utils'
|
|
7
|
+
|
|
8
|
+
module Wisco
|
|
9
|
+
module Commands
|
|
10
|
+
module Exec
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def run(path_arg, target_dir, input: nil, debug: false)
|
|
14
|
+
target_dir = File.expand_path(target_dir)
|
|
15
|
+
config_path = Wisco.config_path(target_dir)
|
|
16
|
+
|
|
17
|
+
unless File.exist?(config_path)
|
|
18
|
+
warn "Error: No #{Wisco::WISCO_DIR}/#{Wisco::CONFIG_FILENAME} found in #{target_dir}."
|
|
19
|
+
warn " Run '#{Wisco::CLI_NAME} init' first."
|
|
20
|
+
exit 1
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
config = Wisco::Config.load_config(config_path)
|
|
24
|
+
connector_path = config.dig('connector', 'path')
|
|
25
|
+
connector_file = config.dig('connector', 'file')
|
|
26
|
+
|
|
27
|
+
if connector_path.nil? || connector_file.nil?
|
|
28
|
+
warn "Error: #{Wisco::WISCO_DIR}/#{Wisco::CONFIG_FILENAME} is missing connector path/file. Run '#{Wisco::CLI_NAME} init' again."
|
|
29
|
+
exit 1
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
connector_full_path = File.join(connector_path, connector_file)
|
|
33
|
+
connection = config['connection']
|
|
34
|
+
|
|
35
|
+
connector = Wisco::Connector.load_connector_from_config(target_dir)
|
|
36
|
+
pairs = Wisco::PathUtils.parse_path(path_arg, connector)
|
|
37
|
+
|
|
38
|
+
pairs.each do |section, key|
|
|
39
|
+
puts "Executing #{section}.#{key}"
|
|
40
|
+
fixtures_dir = File.join(target_dir, 'fixtures', section, key)
|
|
41
|
+
fixture_dir_output = fixtures_dir.sub(connector_path, '.')
|
|
42
|
+
|
|
43
|
+
unless File.directory?(fixtures_dir)
|
|
44
|
+
warn "Error: fixtures directory not found: #{fixture_dir_output}"
|
|
45
|
+
warn " Run '#{Wisco::CLI_NAME} fixtures #{section}.#{key}' first."
|
|
46
|
+
next
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
input_files = resolve_input_files(input, fixtures_dir)
|
|
50
|
+
|
|
51
|
+
if input_files.empty?
|
|
52
|
+
warn "\tWarning: No ready input files found in #{fixture_dir_output}"
|
|
53
|
+
next
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
input_files.each do |input_file|
|
|
57
|
+
execute_one(section, key, input_file, fixtures_dir,
|
|
58
|
+
connector_full_path, connection, debug: debug)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Resolve the list of input files to execute.
|
|
64
|
+
# If an explicit input filename/path is given, use that (relative to fixtures_dir).
|
|
65
|
+
# Otherwise glob execute_* in fixtures_dir and exclude files still containing the sentinel.
|
|
66
|
+
def resolve_input_files(input, fixtures_dir)
|
|
67
|
+
if input
|
|
68
|
+
path = File.absolute_path?(input) ? input : File.join(fixtures_dir, input)
|
|
69
|
+
unless File.exist?(path)
|
|
70
|
+
warn "Error: Input file not found: #{path}"
|
|
71
|
+
exit 1
|
|
72
|
+
end
|
|
73
|
+
[path]
|
|
74
|
+
else
|
|
75
|
+
Dir.glob(File.join(fixtures_dir, 'execute_*')).select do |f|
|
|
76
|
+
File.file?(f) && !file_has_sentinel?(f)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def file_has_sentinel?(path)
|
|
82
|
+
first_line = begin
|
|
83
|
+
File.open(path, &:readline).chomp
|
|
84
|
+
rescue StandardError
|
|
85
|
+
''
|
|
86
|
+
end
|
|
87
|
+
first_line == Wisco::Commands::Fixtures::SENTINEL
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def execute_one(section, key, input_file, fixtures_dir, connector_full_path, connection, debug: false)
|
|
91
|
+
stem = File.basename(input_file, '.*')
|
|
92
|
+
output_file = File.join(fixtures_dir, "output_#{stem}.json")
|
|
93
|
+
error_file = File.join(fixtures_dir, "error_#{stem}.txt")
|
|
94
|
+
|
|
95
|
+
options = { connector: connector_full_path, input: input_file, output: output_file }
|
|
96
|
+
options[:connection] = connection if connection
|
|
97
|
+
|
|
98
|
+
if debug
|
|
99
|
+
warn "[exec] path: #{section}.#{key}.execute"
|
|
100
|
+
warn "[exec] connector: #{connector_full_path}"
|
|
101
|
+
warn "[exec] connection: #{connection.inspect}"
|
|
102
|
+
warn "[exec] input: #{input_file}"
|
|
103
|
+
warn "[exec] output: #{output_file}"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
begin
|
|
107
|
+
cmd = Workato::CLI::ExecCommand.new(path: "#{section}.#{key}.execute", options: options)
|
|
108
|
+
cmd.call
|
|
109
|
+
rescue StandardError => e
|
|
110
|
+
File.write(error_file, "#{e.class}: #{e.message}\n\n#{e.backtrace.join("\n")}\n")
|
|
111
|
+
warn "Error executing #{section}.#{key} with #{File.basename(input_file)}: #{e.message}"
|
|
112
|
+
warn " Details written to: #{error_file}"
|
|
113
|
+
return
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
FileUtils.rm_f(error_file)
|
|
117
|
+
|
|
118
|
+
return unless File.exist?(output_file)
|
|
119
|
+
|
|
120
|
+
pretty = JSON.pretty_generate(JSON.parse(File.read(output_file)))
|
|
121
|
+
File.write(output_file, pretty + "\n")
|
|
122
|
+
puts " Written: #{output_file}"
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
require 'fileutils'
|
|
2
|
+
require 'workato/connector/sdk'
|
|
3
|
+
require 'workato/cli/exec_command'
|
|
4
|
+
require_relative '../config'
|
|
5
|
+
require_relative '../connector'
|
|
6
|
+
require_relative '../path_utils'
|
|
7
|
+
|
|
8
|
+
module Wisco
|
|
9
|
+
module Commands
|
|
10
|
+
module Fixtures
|
|
11
|
+
SENTINEL = '# Remove this comment before updating. Files that include this line will be overwritten.'
|
|
12
|
+
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
def run(path_arg, target_dir, overwrite: false, debug: false)
|
|
16
|
+
target_dir = File.expand_path(target_dir)
|
|
17
|
+
config_path = Wisco.config_path(target_dir)
|
|
18
|
+
|
|
19
|
+
unless File.exist?(config_path)
|
|
20
|
+
warn "Error: No #{Wisco::WISCO_DIR}/#{Wisco::CONFIG_FILENAME} found in #{target_dir}."
|
|
21
|
+
warn " Run '#{Wisco::CLI_NAME} init' first."
|
|
22
|
+
exit 1
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
config = Wisco::Config.load_config(config_path)
|
|
26
|
+
connector_path = config.dig('connector', 'path')
|
|
27
|
+
connector_file = config.dig('connector', 'file')
|
|
28
|
+
|
|
29
|
+
if connector_path.nil? || connector_file.nil?
|
|
30
|
+
warn "Error: #{Wisco::WISCO_DIR}/#{Wisco::CONFIG_FILENAME} is missing connector path/file. Run '#{Wisco::CLI_NAME} init' again."
|
|
31
|
+
exit 1
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
connector_full_path = File.join(connector_path, connector_file)
|
|
35
|
+
connection = config['connection']
|
|
36
|
+
|
|
37
|
+
connector = Wisco::Connector.load_connector_from_config(target_dir)
|
|
38
|
+
pairs = Wisco::PathUtils.parse_path(path_arg, connector)
|
|
39
|
+
|
|
40
|
+
pairs.each do |section, key|
|
|
41
|
+
puts "Processing #{section}.#{key}"
|
|
42
|
+
fixtures_dir = File.join(target_dir, 'fixtures', section, key)
|
|
43
|
+
FileUtils.mkdir_p(fixtures_dir)
|
|
44
|
+
|
|
45
|
+
input_fields_file = File.join(fixtures_dir, 'input_fields.json')
|
|
46
|
+
call_exec(
|
|
47
|
+
path: "#{section}.#{key}.input_fields",
|
|
48
|
+
connector: connector_full_path,
|
|
49
|
+
connection: connection,
|
|
50
|
+
output: input_fields_file,
|
|
51
|
+
debug: debug
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
generate_execute_input(input_fields_file, fixtures_dir, overwrite: overwrite, debug: debug)
|
|
55
|
+
|
|
56
|
+
output_fields_file = File.join(fixtures_dir, 'output_fields.json')
|
|
57
|
+
call_exec(
|
|
58
|
+
path: "#{section}.#{key}.output_fields",
|
|
59
|
+
connector: connector_full_path,
|
|
60
|
+
connection: connection,
|
|
61
|
+
output: output_fields_file,
|
|
62
|
+
debug: debug
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Reads input_fields.json, builds a template hash, writes execute_input.json.
|
|
68
|
+
# The file is prefixed with SENTINEL so it is identifiable as an unedited template.
|
|
69
|
+
# Overwrite rules:
|
|
70
|
+
# - File absent -> write
|
|
71
|
+
# - File present, sentinel L1 -> overwrite (still a template)
|
|
72
|
+
# - File present, no sentinel -> skip (user-edited); force with --overwrite
|
|
73
|
+
def generate_execute_input(input_fields_file, fixtures_dir, overwrite: false, debug: false)
|
|
74
|
+
return unless File.exist?(input_fields_file)
|
|
75
|
+
|
|
76
|
+
fields = JSON.parse(File.read(input_fields_file))
|
|
77
|
+
return if fields.empty?
|
|
78
|
+
|
|
79
|
+
output_file = File.join(fixtures_dir, 'execute_input.json')
|
|
80
|
+
|
|
81
|
+
if File.exist?(output_file)
|
|
82
|
+
first_line = begin
|
|
83
|
+
File.open(output_file, &:readline).chomp
|
|
84
|
+
rescue StandardError
|
|
85
|
+
''
|
|
86
|
+
end
|
|
87
|
+
has_sentinel = (first_line == SENTINEL)
|
|
88
|
+
|
|
89
|
+
unless has_sentinel || overwrite
|
|
90
|
+
puts " Skipped (user-edited): #{output_file}" if debug
|
|
91
|
+
return
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
template = schema_to_template(fields)
|
|
96
|
+
content = "#{SENTINEL}\n#{JSON.pretty_generate(template)}\n"
|
|
97
|
+
File.write(output_file, content)
|
|
98
|
+
puts " Written: #{output_file}"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Recursively converts a Workato schema array into a template hash.
|
|
102
|
+
# Scalars become "<type_value_required|optional>" placeholder strings.
|
|
103
|
+
# Objects expand into a nested hash via their properties.
|
|
104
|
+
# Arrays expand into a single-element array via their properties.
|
|
105
|
+
def schema_to_template(fields)
|
|
106
|
+
fields.each_with_object({}) do |field, hash|
|
|
107
|
+
name = field['name']
|
|
108
|
+
type = field['type'] || 'string'
|
|
109
|
+
optional = field.fetch('optional', true)
|
|
110
|
+
req_str = optional ? 'optional' : 'required'
|
|
111
|
+
|
|
112
|
+
hash[name] = case type
|
|
113
|
+
when 'object'
|
|
114
|
+
schema_to_template(field['properties'] || [])
|
|
115
|
+
when 'array'
|
|
116
|
+
[schema_to_template(field['properties'] || [])]
|
|
117
|
+
else
|
|
118
|
+
"<#{type}_value_#{req_str}>"
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def call_exec(path:, connector:, connection:, output:, debug: false)
|
|
124
|
+
options = { connector: connector, output: output }
|
|
125
|
+
options[:connection] = connection if connection
|
|
126
|
+
|
|
127
|
+
if debug
|
|
128
|
+
warn "[fixtures] path: #{path}"
|
|
129
|
+
warn "[fixtures] connector: #{connector}"
|
|
130
|
+
warn "[fixtures] connection: #{connection.inspect}"
|
|
131
|
+
warn "[fixtures] output: #{output}"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
cmd = Workato::CLI::ExecCommand.new(path: path, options: options)
|
|
135
|
+
begin
|
|
136
|
+
cmd.call
|
|
137
|
+
rescue StandardError => e
|
|
138
|
+
warn " Warning: #{path} failed — #{e.message}"
|
|
139
|
+
return
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
return unless File.exist?(output)
|
|
143
|
+
|
|
144
|
+
pretty = JSON.pretty_generate(JSON.parse(File.read(output)))
|
|
145
|
+
File.write(output, pretty + "\n")
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
require 'fileutils'
|
|
2
|
+
require_relative '../config'
|
|
3
|
+
require_relative '../connector'
|
|
4
|
+
|
|
5
|
+
module Wisco
|
|
6
|
+
module Commands
|
|
7
|
+
module Init
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def run(target_dir)
|
|
11
|
+
target_dir = File.expand_path(target_dir)
|
|
12
|
+
|
|
13
|
+
unless Dir.exist?(target_dir)
|
|
14
|
+
warn "Error: Directory not found: #{target_dir}"
|
|
15
|
+
exit 1
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
puts "Searching for connector in #{target_dir}..."
|
|
19
|
+
|
|
20
|
+
connector_file = Wisco::Connector.detect_connector(target_dir)
|
|
21
|
+
|
|
22
|
+
if connector_file.nil?
|
|
23
|
+
warn "Error: No valid Workato connector file found in #{target_dir}"
|
|
24
|
+
exit 1
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
puts "Found connector: #{connector_file}"
|
|
28
|
+
|
|
29
|
+
wisco_dir = File.join(target_dir, Wisco::WISCO_DIR)
|
|
30
|
+
FileUtils.mkdir_p(wisco_dir)
|
|
31
|
+
|
|
32
|
+
config_path = Wisco.config_path(target_dir)
|
|
33
|
+
config = Wisco::Config.load_config(config_path)
|
|
34
|
+
|
|
35
|
+
config['connector'] ||= {}
|
|
36
|
+
config['connector']['path'] = target_dir
|
|
37
|
+
config['connector']['file'] = connector_file
|
|
38
|
+
|
|
39
|
+
Wisco::Config.save_config(config_path, config)
|
|
40
|
+
puts "Config written to #{config_path}"
|
|
41
|
+
|
|
42
|
+
update_gitignore(target_dir)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def update_gitignore(target_dir)
|
|
46
|
+
gitignore_path = File.join(target_dir, '.gitignore')
|
|
47
|
+
entry = "#{Wisco::WISCO_DIR}/"
|
|
48
|
+
|
|
49
|
+
if File.exist?(gitignore_path)
|
|
50
|
+
content = File.read(gitignore_path)
|
|
51
|
+
if content.include?(entry)
|
|
52
|
+
puts ".gitignore already contains '#{entry}' — no changes made."
|
|
53
|
+
else
|
|
54
|
+
File.open(gitignore_path, 'a') { |f| f.puts entry }
|
|
55
|
+
puts "Added '#{entry}' to .gitignore"
|
|
56
|
+
end
|
|
57
|
+
else
|
|
58
|
+
puts "Note: No .gitignore found — consider adding '#{entry}' manually."
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
require_relative '../connector'
|
|
2
|
+
|
|
3
|
+
module Wisco
|
|
4
|
+
module Commands
|
|
5
|
+
module List
|
|
6
|
+
TREE_FORK = '├── '
|
|
7
|
+
TREE_LAST = '└── '
|
|
8
|
+
TREE_PIPE = '│ '
|
|
9
|
+
TREE_BLANK = ' '
|
|
10
|
+
EXPANDABLE_KEYS = %i[actions triggers object_definitions methods pick_lists].freeze
|
|
11
|
+
SORT_FIELDS = %w[key title].freeze
|
|
12
|
+
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
def run(subcommand, target, sort: nil)
|
|
16
|
+
validate_sort!(sort)
|
|
17
|
+
|
|
18
|
+
case subcommand
|
|
19
|
+
when nil then run_tree(target)
|
|
20
|
+
when 'actions' then run_actions(target, sort: sort)
|
|
21
|
+
when 'triggers' then run_triggers(target, sort: sort)
|
|
22
|
+
when 'all' then run_all(target, sort: sort)
|
|
23
|
+
else
|
|
24
|
+
warn "Error: Unknown list subcommand '#{subcommand}'"
|
|
25
|
+
warn "Run '#{Wisco::CLI_NAME} --help' for usage."
|
|
26
|
+
exit 1
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def run_tree(target_dir)
|
|
31
|
+
connector = Wisco::Connector.load_connector_from_config(target_dir)
|
|
32
|
+
puts connector[:title]
|
|
33
|
+
|
|
34
|
+
keys = connector.keys
|
|
35
|
+
keys.each_with_index do |key, idx|
|
|
36
|
+
last = idx == keys.size - 1
|
|
37
|
+
connector_str = last ? TREE_LAST : TREE_FORK
|
|
38
|
+
child_prefix = last ? TREE_BLANK : TREE_PIPE
|
|
39
|
+
value = connector[key]
|
|
40
|
+
|
|
41
|
+
if key == :connection && value.is_a?(Hash)
|
|
42
|
+
puts "#{connector_str}#{key}"
|
|
43
|
+
render_connection_tree(value, child_prefix)
|
|
44
|
+
elsif EXPANDABLE_KEYS.include?(key) && value.is_a?(Hash) && !value.empty?
|
|
45
|
+
puts "#{connector_str}#{key} [#{value.size}]"
|
|
46
|
+
render_children_tree(value, child_prefix)
|
|
47
|
+
elsif value.is_a?(Hash)
|
|
48
|
+
puts "#{connector_str}#{key} [#{value.size}]"
|
|
49
|
+
elsif value.is_a?(Array)
|
|
50
|
+
puts "#{connector_str}#{key} [#{value.size}]"
|
|
51
|
+
else
|
|
52
|
+
puts "#{connector_str}#{key}"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def run_actions(target_dir, sort: nil)
|
|
58
|
+
connector = Wisco::Connector.load_connector_from_config(target_dir)
|
|
59
|
+
actions = connector[:actions]
|
|
60
|
+
|
|
61
|
+
if actions.nil? || actions.empty?
|
|
62
|
+
puts 'No actions defined.'
|
|
63
|
+
return
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
rows = actions.map { |key, item| [key, title_for(key, item), subtitle_for(item)] }
|
|
67
|
+
rows = sort_rows(rows, sort)
|
|
68
|
+
render_markdown_table(%w[Key Title Subtitle], rows)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def run_triggers(target_dir, sort: nil)
|
|
72
|
+
connector = Wisco::Connector.load_connector_from_config(target_dir)
|
|
73
|
+
triggers = connector[:triggers]
|
|
74
|
+
|
|
75
|
+
if triggers.nil? || triggers.empty?
|
|
76
|
+
puts 'No triggers defined.'
|
|
77
|
+
return
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
rows = triggers.map { |key, item| [key, title_for(key, item), subtitle_for(item)] }
|
|
81
|
+
rows = sort_rows(rows, sort)
|
|
82
|
+
render_markdown_table(%w[Key Title Subtitle], rows)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def run_all(target_dir, sort: nil)
|
|
86
|
+
puts '## Overview'
|
|
87
|
+
run_tree(target_dir)
|
|
88
|
+
puts
|
|
89
|
+
puts '## Actions'
|
|
90
|
+
run_actions(target_dir, sort: sort)
|
|
91
|
+
puts
|
|
92
|
+
puts '## Triggers'
|
|
93
|
+
run_triggers(target_dir, sort: sort)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
# List utilities
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
def strip_html(str)
|
|
101
|
+
str.to_s.gsub(/<[^>]+>/, '').squeeze(' ').strip
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def humanise_key(key)
|
|
105
|
+
key.to_s.split('_').map(&:capitalize).join(' ')
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def title_for(key, item)
|
|
109
|
+
item.is_a?(Hash) && item[:title] ? item[:title] : humanise_key(key)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def subtitle_for(item)
|
|
113
|
+
return '' unless item.is_a?(Hash)
|
|
114
|
+
return item[:subtitle] if item[:subtitle]
|
|
115
|
+
|
|
116
|
+
strip_html(item[:description]) if item[:description]
|
|
117
|
+
item[:subtitle] || strip_html(item[:description].to_s)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def render_markdown_table(headers, rows)
|
|
121
|
+
all_rows = [headers] + rows
|
|
122
|
+
widths = headers.length.times.map do |i|
|
|
123
|
+
all_rows.map { |r| r[i].to_s.length }.max
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
fmt = widths.map { |w| "%-#{w}s" }.join(' | ')
|
|
127
|
+
sep = widths.map { |w| '-' * w }.join('-|-')
|
|
128
|
+
|
|
129
|
+
puts "| #{format(fmt, *headers)} |"
|
|
130
|
+
puts "|-#{sep}-|"
|
|
131
|
+
rows.each { |r| puts "| #{format(fmt, *r)} |" }
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def validate_sort!(sort)
|
|
135
|
+
return if sort.nil? || SORT_FIELDS.include?(sort)
|
|
136
|
+
|
|
137
|
+
warn "Error: Unsupported sort field '#{sort}'. Valid values: #{SORT_FIELDS.join(', ')}."
|
|
138
|
+
exit 1
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def sort_rows(rows, sort)
|
|
142
|
+
return rows if sort.nil?
|
|
143
|
+
|
|
144
|
+
index = SORT_FIELDS.index(sort)
|
|
145
|
+
rows.sort_by { |row| [row[index].to_s.downcase, row[0].to_s.downcase] }
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# ---------------------------------------------------------------------------
|
|
149
|
+
# Tree rendering
|
|
150
|
+
# ---------------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
def value_label(value)
|
|
153
|
+
case value
|
|
154
|
+
when Hash then "[#{value.size}]"
|
|
155
|
+
when Array then "[#{value.size}]"
|
|
156
|
+
when Proc then '(lambda)'
|
|
157
|
+
else value.to_s
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def render_connection_tree(conn, prefix)
|
|
162
|
+
keys = conn.keys
|
|
163
|
+
keys.each_with_index do |key, idx|
|
|
164
|
+
last = idx == keys.size - 1
|
|
165
|
+
connector = last ? TREE_LAST : TREE_FORK
|
|
166
|
+
child_prefix = prefix + (last ? TREE_BLANK : TREE_PIPE)
|
|
167
|
+
value = conn[key]
|
|
168
|
+
|
|
169
|
+
if value.is_a?(Hash)
|
|
170
|
+
puts "#{prefix}#{connector}#{key}"
|
|
171
|
+
sub_keys = value.keys
|
|
172
|
+
sub_keys.each_with_index do |sk, si|
|
|
173
|
+
sl = si == sub_keys.size - 1
|
|
174
|
+
sc = sl ? TREE_LAST : TREE_FORK
|
|
175
|
+
puts "#{child_prefix}#{sc}#{sk}"
|
|
176
|
+
end
|
|
177
|
+
else
|
|
178
|
+
puts "#{prefix}#{connector}#{key} #{value_label(value)}"
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def render_children_tree(value, prefix)
|
|
184
|
+
child_keys = value.keys.sort
|
|
185
|
+
child_keys.each_with_index do |ck, ci|
|
|
186
|
+
cl = ci == child_keys.size - 1
|
|
187
|
+
puts "#{prefix}#{cl ? TREE_LAST : TREE_FORK}#{ck}"
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|