howzit 2.1.28 → 2.1.30
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 +121 -0
- data/README.md +5 -1
- data/Rakefile +11 -4
- data/bin/howzit +65 -65
- data/howzit.gemspec +1 -1
- data/lib/howzit/buildnote.rb +4 -8
- data/lib/howzit/colors.rb +50 -22
- data/lib/howzit/condition_evaluator.rb +307 -0
- data/lib/howzit/conditional_content.rb +96 -0
- data/lib/howzit/config.rb +28 -3
- data/lib/howzit/console_logger.rb +74 -2
- data/lib/howzit/directive.rb +137 -0
- data/lib/howzit/prompt.rb +20 -12
- data/lib/howzit/run_report.rb +1 -1
- data/lib/howzit/script_comm.rb +105 -0
- data/lib/howzit/script_support.rb +479 -0
- data/lib/howzit/stringutils.rb +4 -4
- data/lib/howzit/task.rb +68 -6
- data/lib/howzit/topic.rb +576 -13
- data/lib/howzit/util.rb +11 -3
- data/lib/howzit/version.rb +1 -1
- data/lib/howzit.rb +5 -0
- data/spec/condition_evaluator_spec.rb +261 -0
- data/spec/conditional_blocks_integration_spec.rb +159 -0
- data/spec/conditional_content_spec.rb +296 -0
- data/spec/log_level_spec.rb +247 -0
- data/spec/script_comm_spec.rb +303 -0
- data/spec/sequential_conditional_spec.rb +319 -0
- data/spec/set_var_spec.rb +603 -0
- data/spec/spec_helper.rb +3 -1
- data/spec/topic_spec.rb +8 -6
- data/src/_README.md +5 -1
- metadata +23 -4
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'shellwords'
|
|
4
|
+
|
|
5
|
+
module Howzit
|
|
6
|
+
# Directive class
|
|
7
|
+
# Represents a parsed directive from topic content (tasks, conditionals, etc.)
|
|
8
|
+
class Directive
|
|
9
|
+
attr_reader :type, :content, :condition, :directive_type, :optional, :default, :line_number, :conditional_path,
|
|
10
|
+
:log_level_value, :var_name, :var_value
|
|
11
|
+
|
|
12
|
+
##
|
|
13
|
+
## Initialize a Directive
|
|
14
|
+
##
|
|
15
|
+
## @param type [Symbol] :task, :if, :unless, :elsif, :else, :end, :log_level
|
|
16
|
+
## @param content [String] The directive content (action, block, etc.)
|
|
17
|
+
## @param condition [String, nil] Condition string for conditionals
|
|
18
|
+
## @param directive_type [String, nil] 'if', 'unless', 'elsif', 'else' for conditionals
|
|
19
|
+
## @param optional [Boolean] Whether task requires confirmation
|
|
20
|
+
## @param default [Boolean] Default response for confirmation
|
|
21
|
+
## @param line_number [Integer] Line number in original content
|
|
22
|
+
## @param conditional_path [Array] Array of conditional indices this directive is nested in
|
|
23
|
+
## @param log_level_value [String, nil] Log level value for @log_level directives
|
|
24
|
+
## @param var_name [String, nil] Variable name for @set_var directives
|
|
25
|
+
## @param var_value [String, nil] Variable value for @set_var directives
|
|
26
|
+
##
|
|
27
|
+
def initialize(type:, content: nil, condition: nil, directive_type: nil, optional: false, default: true,
|
|
28
|
+
line_number: nil, conditional_path: [], log_level_value: nil, var_name: nil, var_value: nil)
|
|
29
|
+
@type = type
|
|
30
|
+
@content = content
|
|
31
|
+
@condition = condition
|
|
32
|
+
@directive_type = directive_type
|
|
33
|
+
@optional = optional
|
|
34
|
+
@default = default
|
|
35
|
+
@line_number = line_number
|
|
36
|
+
@conditional_path = conditional_path || []
|
|
37
|
+
@log_level_value = log_level_value
|
|
38
|
+
@var_name = var_name
|
|
39
|
+
@var_value = var_value
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
##
|
|
43
|
+
## Is this a conditional directive?
|
|
44
|
+
##
|
|
45
|
+
def conditional?
|
|
46
|
+
%i[if unless elsif else end].include?(@type)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
##
|
|
50
|
+
## Is this a task directive?
|
|
51
|
+
##
|
|
52
|
+
def task?
|
|
53
|
+
@type == :task
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
##
|
|
57
|
+
## Is this a log_level directive?
|
|
58
|
+
##
|
|
59
|
+
def log_level?
|
|
60
|
+
@type == :log_level
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
##
|
|
64
|
+
## Is this a set_var directive?
|
|
65
|
+
##
|
|
66
|
+
def set_var?
|
|
67
|
+
@type == :set_var
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
##
|
|
71
|
+
## Convert directive to a Task object (only works for task directives)
|
|
72
|
+
##
|
|
73
|
+
## @param parent [Topic] The parent topic
|
|
74
|
+
## @param current_log_level [String, nil] Current log level to apply to task
|
|
75
|
+
##
|
|
76
|
+
## @return [Task] Task object
|
|
77
|
+
##
|
|
78
|
+
def to_task(parent, current_log_level: nil)
|
|
79
|
+
return nil unless task?
|
|
80
|
+
|
|
81
|
+
task_data = @content.dup
|
|
82
|
+
task_type = task_data[:type]
|
|
83
|
+
|
|
84
|
+
# Apply current log level if set and task doesn't have its own
|
|
85
|
+
task_data[:log_level] = current_log_level if current_log_level && !task_data[:log_level]
|
|
86
|
+
|
|
87
|
+
# Set named_arguments before processing titles for variable substitution
|
|
88
|
+
Howzit.named_arguments = parent.named_args
|
|
89
|
+
|
|
90
|
+
case task_type
|
|
91
|
+
when :block
|
|
92
|
+
# Block tasks are already properly formatted
|
|
93
|
+
task_data[:parent] = parent
|
|
94
|
+
Howzit::Task.new(task_data, optional: @optional, default: @default)
|
|
95
|
+
when :run
|
|
96
|
+
# Run tasks need title rendering (similar to define_task_args)
|
|
97
|
+
title = task_data[:title]
|
|
98
|
+
title = title.render_arguments if title && !title.empty?
|
|
99
|
+
task_data[:title] = title
|
|
100
|
+
task_data[:parent] = parent
|
|
101
|
+
Howzit::Task.new(task_data, optional: @optional, default: @default)
|
|
102
|
+
when :copy
|
|
103
|
+
# Copy tasks need title rendering and action escaping
|
|
104
|
+
title = task_data[:title]
|
|
105
|
+
title = title.render_arguments if title && !title.empty?
|
|
106
|
+
task_data[:title] = title
|
|
107
|
+
task_data[:action] = Shellwords.escape(task_data[:action])
|
|
108
|
+
task_data[:parent] = parent
|
|
109
|
+
Howzit::Task.new(task_data, optional: @optional, default: @default)
|
|
110
|
+
when :open
|
|
111
|
+
# Open tasks need title rendering
|
|
112
|
+
title = task_data[:title]
|
|
113
|
+
title = title.render_arguments if title && !title.empty?
|
|
114
|
+
task_data[:title] = title
|
|
115
|
+
task_data[:parent] = parent
|
|
116
|
+
Howzit::Task.new(task_data, optional: @optional, default: @default)
|
|
117
|
+
when :include
|
|
118
|
+
# Include tasks need special handling (title processing, arguments, etc.)
|
|
119
|
+
title = task_data[:title]
|
|
120
|
+
if title =~ /\[(.*?)\] *$/
|
|
121
|
+
args = Regexp.last_match(1).split(/ *, */).map(&:render_arguments)
|
|
122
|
+
Howzit.arguments = args
|
|
123
|
+
parent.arguments
|
|
124
|
+
title.sub!(/ *\[.*?\] *$/, '')
|
|
125
|
+
end
|
|
126
|
+
title = title.render_arguments if title && !title.empty?
|
|
127
|
+
task_data[:title] = title
|
|
128
|
+
task_data[:parent] = parent
|
|
129
|
+
task_data[:arguments] = Howzit.named_arguments
|
|
130
|
+
Howzit::Task.new(task_data, optional: @optional, default: @default)
|
|
131
|
+
else
|
|
132
|
+
task_data[:parent] = parent
|
|
133
|
+
Howzit::Task.new(task_data, optional: @optional, default: @default)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
data/lib/howzit/prompt.rb
CHANGED
|
@@ -100,9 +100,7 @@ module Howzit
|
|
|
100
100
|
return fzf_result(res)
|
|
101
101
|
end
|
|
102
102
|
|
|
103
|
-
if Util.command_exist?('gum')
|
|
104
|
-
return gum_choose(matches, query: query, multi: true)
|
|
105
|
-
end
|
|
103
|
+
return gum_choose(matches, query: query, multi: true) if Util.command_exist?('gum')
|
|
106
104
|
|
|
107
105
|
tty_menu(matches, query: query)
|
|
108
106
|
end
|
|
@@ -150,7 +148,11 @@ module Howzit
|
|
|
150
148
|
end
|
|
151
149
|
|
|
152
150
|
if query
|
|
153
|
-
|
|
151
|
+
begin
|
|
152
|
+
puts "\nSelect a topic for `#{query}`:"
|
|
153
|
+
rescue Errno::EPIPE
|
|
154
|
+
# Pipe closed, ignore
|
|
155
|
+
end
|
|
154
156
|
end
|
|
155
157
|
options_list(matches)
|
|
156
158
|
read_selection(matches)
|
|
@@ -168,7 +170,11 @@ module Howzit
|
|
|
168
170
|
|
|
169
171
|
return [matches[line - 1]] if line.positive? && line <= matches.length
|
|
170
172
|
|
|
171
|
-
|
|
173
|
+
begin
|
|
174
|
+
puts 'Out of range'
|
|
175
|
+
rescue Errno::EPIPE
|
|
176
|
+
# Pipe closed, ignore
|
|
177
|
+
end
|
|
172
178
|
read_selection(matches)
|
|
173
179
|
end
|
|
174
180
|
ensure
|
|
@@ -229,9 +235,7 @@ module Howzit
|
|
|
229
235
|
return res.empty? ? [] : res.split(/\n/)
|
|
230
236
|
end
|
|
231
237
|
|
|
232
|
-
if Util.command_exist?('gum')
|
|
233
|
-
return gum_choose(matches, prompt: prompt_text, multi: true, required: false)
|
|
234
|
-
end
|
|
238
|
+
return gum_choose(matches, prompt: prompt_text, multi: true, required: false) if Util.command_exist?('gum')
|
|
235
239
|
|
|
236
240
|
text_template_input(matches)
|
|
237
241
|
end
|
|
@@ -265,7 +269,11 @@ module Howzit
|
|
|
265
269
|
exit
|
|
266
270
|
end
|
|
267
271
|
|
|
268
|
-
|
|
272
|
+
begin
|
|
273
|
+
puts "\n{bw}Available templates:{x} #{available.join(', ')}".c
|
|
274
|
+
rescue Errno::EPIPE
|
|
275
|
+
# Pipe closed, ignore
|
|
276
|
+
end
|
|
269
277
|
printf '{bw}Enter templates to include, comma-separated (return to skip):{x} '.c
|
|
270
278
|
input = Readline.readline('', true).strip
|
|
271
279
|
|
|
@@ -321,7 +329,7 @@ module Howzit
|
|
|
321
329
|
## @return [String] the entered value
|
|
322
330
|
##
|
|
323
331
|
def get_line(prompt_text, default: nil)
|
|
324
|
-
return
|
|
332
|
+
return default || '' unless $stdout.isatty
|
|
325
333
|
|
|
326
334
|
if Util.command_exist?('gum')
|
|
327
335
|
result = gum_input(prompt_text, placeholder: default || '')
|
|
@@ -346,7 +354,7 @@ module Howzit
|
|
|
346
354
|
##
|
|
347
355
|
def gum_choose(matches, prompt: nil, multi: false, required: true, query: nil)
|
|
348
356
|
prompt_text = prompt || (query ? "Select for '#{query}'" : 'Select')
|
|
349
|
-
args = [
|
|
357
|
+
args = %w[gum choose]
|
|
350
358
|
args << '--no-limit' if multi
|
|
351
359
|
args << "--header=#{Shellwords.escape(prompt_text)}"
|
|
352
360
|
args << '--cursor.foreground=6'
|
|
@@ -376,7 +384,7 @@ module Howzit
|
|
|
376
384
|
## @return [String] The entered value
|
|
377
385
|
##
|
|
378
386
|
def gum_input(prompt_text, placeholder: '')
|
|
379
|
-
args = [
|
|
387
|
+
args = %w[gum input]
|
|
380
388
|
args << "--header=#{Shellwords.escape(prompt_text)}"
|
|
381
389
|
args << "--placeholder=#{Shellwords.escape(placeholder)}" unless placeholder.empty?
|
|
382
390
|
args << '--cursor.foreground=6'
|
data/lib/howzit/run_report.rb
CHANGED
|
@@ -57,7 +57,7 @@ module Howzit
|
|
|
57
57
|
|
|
58
58
|
# Build the table with emoji header - center emoji in 6-char column
|
|
59
59
|
header = "| 🚥 | #{'Task'.ljust(task_width)} |"
|
|
60
|
-
separator = "| :--: | #{'
|
|
60
|
+
separator = "| :--: | #{":#{'-' * (task_width - 1)}"} |"
|
|
61
61
|
|
|
62
62
|
table_lines = [header, separator]
|
|
63
63
|
rows.each do |row|
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Howzit
|
|
4
|
+
# Script Communication module
|
|
5
|
+
# Handles communication from scripts to Howzit via a communication file
|
|
6
|
+
module ScriptComm
|
|
7
|
+
class << self
|
|
8
|
+
##
|
|
9
|
+
## Create a communication file for scripts to write to
|
|
10
|
+
##
|
|
11
|
+
## @return [String] Path to the communication file
|
|
12
|
+
##
|
|
13
|
+
def create_comm_file
|
|
14
|
+
file = Tempfile.new('howzit_comm')
|
|
15
|
+
file.close
|
|
16
|
+
file.path
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
##
|
|
20
|
+
## Set up the communication file and environment variable
|
|
21
|
+
##
|
|
22
|
+
## @return [String] Path to the communication file
|
|
23
|
+
##
|
|
24
|
+
def setup
|
|
25
|
+
comm_file = create_comm_file
|
|
26
|
+
ENV['HOWZIT_COMM_FILE'] = comm_file
|
|
27
|
+
comm_file
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
##
|
|
31
|
+
## Read and process the communication file after script execution
|
|
32
|
+
##
|
|
33
|
+
## @param comm_file [String] Path to the communication file
|
|
34
|
+
##
|
|
35
|
+
## @return [Hash] Hash with :logs and :vars keys
|
|
36
|
+
##
|
|
37
|
+
def process(comm_file)
|
|
38
|
+
return { logs: [], vars: {} } unless File.exist?(comm_file)
|
|
39
|
+
|
|
40
|
+
logs = []
|
|
41
|
+
vars = {}
|
|
42
|
+
|
|
43
|
+
begin
|
|
44
|
+
content = File.read(comm_file)
|
|
45
|
+
content.each_line do |line|
|
|
46
|
+
line = line.strip
|
|
47
|
+
next if line.empty?
|
|
48
|
+
|
|
49
|
+
case line
|
|
50
|
+
when /^LOG:(info|warn|error|debug):(.+)$/i
|
|
51
|
+
level = Regexp.last_match(1).downcase.to_sym
|
|
52
|
+
message = Regexp.last_match(2)
|
|
53
|
+
logs << { level: level, message: message }
|
|
54
|
+
when /^VAR:([A-Z0-9_]+)=(.*)$/i
|
|
55
|
+
key = Regexp.last_match(1)
|
|
56
|
+
value = Regexp.last_match(2)
|
|
57
|
+
vars[key] = value
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
rescue StandardError => e
|
|
61
|
+
Howzit.console&.warn("Error reading communication file: #{e.message}")
|
|
62
|
+
ensure
|
|
63
|
+
# Clean up the file
|
|
64
|
+
File.unlink(comm_file) if File.exist?(comm_file)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
{ logs: logs, vars: vars }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
##
|
|
71
|
+
## Process communication and apply logs/variables
|
|
72
|
+
##
|
|
73
|
+
## @param comm_file [String] Path to the communication file
|
|
74
|
+
##
|
|
75
|
+
def apply(comm_file)
|
|
76
|
+
result = process(comm_file)
|
|
77
|
+
return if result[:logs].empty? && result[:vars].empty?
|
|
78
|
+
|
|
79
|
+
# Apply log messages
|
|
80
|
+
result[:logs].each do |log_entry|
|
|
81
|
+
level = log_entry[:level]
|
|
82
|
+
message = log_entry[:message]
|
|
83
|
+
next unless Howzit.console
|
|
84
|
+
|
|
85
|
+
case level
|
|
86
|
+
when :info
|
|
87
|
+
Howzit.console.info(message)
|
|
88
|
+
when :warn
|
|
89
|
+
Howzit.console.warn(message)
|
|
90
|
+
when :error
|
|
91
|
+
Howzit.console.error(message)
|
|
92
|
+
when :debug
|
|
93
|
+
Howzit.console.debug(message)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Apply variables to named_arguments
|
|
98
|
+
return if result[:vars].empty?
|
|
99
|
+
|
|
100
|
+
Howzit.named_arguments ||= {}
|
|
101
|
+
Howzit.named_arguments.merge!(result[:vars])
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|