howzit 2.1.29 → 2.1.31

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/lib/howzit/topic.rb CHANGED
@@ -7,7 +7,7 @@ module Howzit
7
7
 
8
8
  attr_accessor :content
9
9
 
10
- attr_reader :title, :tasks, :prereqs, :postreqs, :results, :named_args
10
+ attr_reader :title, :tasks, :prereqs, :postreqs, :results, :named_args, :directives
11
11
 
12
12
  ##
13
13
  ## Initialize a topic object
@@ -25,6 +25,7 @@ module Howzit
25
25
  @metadata = metadata
26
26
  arguments
27
27
 
28
+ @directives = parse_directives_with_conditionals
28
29
  @tasks = gather_tasks
29
30
  @results = { total: 0, success: 0, errors: 0, message: ''.c }
30
31
  end
@@ -81,6 +82,15 @@ module Howzit
81
82
 
82
83
  cols = check_cols
83
84
 
85
+ # Use sequential processing if we have directives with conditionals
86
+ if @directives && @directives.any?(&:conditional?)
87
+ return run_sequential(nested: nested, output: output, cols: cols)
88
+ end
89
+
90
+ # Fall back to old behavior for backward compatibility
91
+ # Note: @set_var directives are already processed in gather_tasks for non-sequential path
92
+ # This section is kept for backward compatibility but shouldn't be needed
93
+
84
94
  if @tasks.count.positive?
85
95
  unless @prereqs.empty?
86
96
  begin
@@ -178,7 +188,8 @@ module Howzit
178
188
  output = []
179
189
 
180
190
  if keys[:action] =~ / *\[(.*?)\] *$/
181
- Howzit.named_arguments = @named_args
191
+ Howzit.named_arguments ||= {}
192
+ Howzit.named_arguments.merge!(@named_args) if @named_args
182
193
  Howzit.arguments = Regexp.last_match(1).split(/ *, */).map!(&:render_arguments)
183
194
  end
184
195
 
@@ -318,7 +329,9 @@ module Howzit
318
329
  action: obj,
319
330
  parent: self }
320
331
  # Set named_arguments before processing titles for variable substitution
321
- Howzit.named_arguments = @named_args
332
+ # Merge with existing named_arguments to preserve @set_var variables
333
+ Howzit.named_arguments ||= {}
334
+ Howzit.named_arguments.merge!(@named_args) if @named_args
322
335
  case cmd
323
336
  when /include/i
324
337
  if title =~ /\[(.*?)\] *$/
@@ -335,6 +348,14 @@ module Howzit
335
348
  when /run/i
336
349
  task_args[:type] = :run
337
350
  task_args[:title] = title.render_arguments
351
+ # Parse log_level from action if present (format: script, log_level=level)
352
+ if obj =~ /,\s*log_level\s*=\s*(\w+)/i
353
+ log_level = Regexp.last_match(1).downcase
354
+ task_args[:log_level] = log_level
355
+ # Remove log_level parameter from action
356
+ obj = obj.sub(/,\s*log_level\s*=\s*\w+/i, '').strip
357
+ end
358
+ task_args[:action] = obj
338
359
  when /copy/i
339
360
  task_args[:type] = :copy
340
361
  task_args[:action] = Shellwords.escape(obj)
@@ -379,7 +400,39 @@ module Howzit
379
400
  runnable = []
380
401
  # Process conditional blocks first
381
402
  # Set named_arguments before processing so conditions can access them
382
- Howzit.named_arguments = @named_args
403
+ Howzit.named_arguments ||= {}
404
+ Howzit.named_arguments.merge!(@named_args) if @named_args
405
+
406
+ # Process @set_var directives before gathering tasks (for non-sequential path)
407
+ # This ensures variables are available when task actions are rendered
408
+ if @directives && !@directives.any?(&:conditional?)
409
+ @directives.each do |dir|
410
+ next unless dir.set_var?
411
+
412
+ value = dir.var_value
413
+ if value
414
+ # Check for command substitution: backticks or $()
415
+ if value =~ /^`(.+)`$/ || value =~ /^\$\((.+)\)$/
416
+ command = Regexp.last_match(1).strip
417
+ # Apply variable substitution to command before execution
418
+ command = command.render_arguments
419
+ # Execute command and capture output
420
+ begin
421
+ value = `#{command}`.strip
422
+ rescue StandardError => e
423
+ Howzit.console.warn("Error executing command in @set_var: #{e.message}")
424
+ value = ''
425
+ end
426
+ else
427
+ # Apply variable substitution to the value (in case it references other variables)
428
+ value = value.render_arguments
429
+ end
430
+ end
431
+
432
+ Howzit.named_arguments[dir.var_name] = value
433
+ end
434
+ end
435
+
383
436
  metadata = @metadata || Howzit.buildnote&.metadata
384
437
  processed_content = ConditionalContent.process(@content, { metadata: metadata })
385
438
 
@@ -394,7 +447,8 @@ module Howzit
394
447
  processed_content.scan(rx) { matches << Regexp.last_match }
395
448
  matches.each do |m|
396
449
  c = m.named_captures.symbolize_keys
397
- Howzit.named_arguments = @named_args
450
+ Howzit.named_arguments ||= {}
451
+ Howzit.named_arguments.merge!(@named_args) if @named_args
398
452
 
399
453
  if c[:cmd].nil?
400
454
  optional, default = define_optional(c[:optional2])
@@ -418,5 +472,494 @@ module Howzit
418
472
 
419
473
  runnable
420
474
  end
475
+
476
+ ##
477
+ ## Parse directives with conditional context for sequential evaluation
478
+ ##
479
+ ## @return [Array] Array of Directive objects
480
+ ##
481
+ def parse_directives_with_conditionals
482
+ directives = []
483
+ lines = @content.split(/\n/)
484
+ conditional_stack = [] # Array of directive indices for @if/@unless directives
485
+ current_branch_index = nil # Track current @if/@elsif/@else branch index
486
+ line_num = 0
487
+ in_code_block = false
488
+ code_block_lines = []
489
+ code_block_fence = nil
490
+ code_block_title = nil
491
+ code_block_optional = nil
492
+
493
+ # Extract prereqs and postreqs from raw content
494
+ @prereqs = @content.scan(/(?<=@before\n).*?(?=\n@end)/im).map(&:strip)
495
+ @postreqs = @content.scan(/(?<=@after\n).*?(?=\n@end)/im).map(&:strip)
496
+
497
+ lines.each do |line|
498
+ line_num += 1
499
+
500
+ # Handle code blocks (fenced code)
501
+ if line =~ /^(`{3,})run([?!]*)\s*(.*?)$/i && !in_code_block
502
+ in_code_block = true
503
+ code_block_fence = Regexp.last_match(1)
504
+ code_block_optional = Regexp.last_match(2)
505
+ code_block_title = Regexp.last_match(3).strip
506
+ code_block_lines = []
507
+ next
508
+ elsif in_code_block
509
+ if line =~ /^#{Regexp.escape(code_block_fence)}\s*$/
510
+ # End of code block
511
+ block_content = code_block_lines.join("\n")
512
+ optional, default = define_optional(code_block_optional)
513
+ conditional_path = conditional_stack.dup
514
+ # If we're inside an @elsif/@else branch, include it in the path
515
+ conditional_path << current_branch_index if current_branch_index
516
+ directives << Howzit::Directive.new(
517
+ type: :task,
518
+ content: {
519
+ type: :block,
520
+ title: code_block_title,
521
+ action: block_content,
522
+ arguments: nil
523
+ },
524
+ optional: optional,
525
+ default: default,
526
+ line_number: line_num,
527
+ conditional_path: conditional_path
528
+ )
529
+ in_code_block = false
530
+ code_block_lines = []
531
+ code_block_fence = nil
532
+ else
533
+ code_block_lines << line
534
+ end
535
+ next
536
+ end
537
+
538
+ # Handle conditional directives
539
+ if line =~ /^@(if|unless)\s+(.+)$/i
540
+ directive_type = Regexp.last_match(1).downcase
541
+ condition = Regexp.last_match(2).strip
542
+ directive_index = directives.length
543
+ conditional_stack << directive_index
544
+ current_branch_index = directive_index
545
+ directives << Howzit::Directive.new(
546
+ type: directive_type.to_sym,
547
+ condition: condition,
548
+ directive_type: directive_type,
549
+ line_number: line_num,
550
+ conditional_path: conditional_stack[0..-2].dup
551
+ )
552
+ next
553
+ elsif line =~ /^@elsif\s+(.+)$/i
554
+ condition = Regexp.last_match(1).strip
555
+ directive_index = directives.length
556
+ current_branch_index = directive_index
557
+ directives << Howzit::Directive.new(
558
+ type: :elsif,
559
+ condition: condition,
560
+ directive_type: 'elsif',
561
+ line_number: line_num,
562
+ conditional_path: conditional_stack[0..-2].dup
563
+ )
564
+ next
565
+ elsif line =~ /^@else\s*$/i
566
+ directive_index = directives.length
567
+ current_branch_index = directive_index
568
+ directives << Howzit::Directive.new(
569
+ type: :else,
570
+ directive_type: 'else',
571
+ line_number: line_num,
572
+ conditional_path: conditional_stack[0..-2].dup
573
+ )
574
+ next
575
+ elsif line =~ /^@end\s*$/i && !conditional_stack.empty?
576
+ # Closing a conditional block
577
+ conditional_stack.pop
578
+ current_branch_index = nil
579
+ directives << Howzit::Directive.new(
580
+ type: :end,
581
+ directive_type: 'end',
582
+ line_number: line_num,
583
+ conditional_path: conditional_stack.dup
584
+ )
585
+ next
586
+ end
587
+
588
+ # Handle @log_level directive
589
+ if line =~ /^@log_level\s*\(([^)]+)\)\s*$/i
590
+ log_level = Regexp.last_match(1).strip
591
+ conditional_path = conditional_stack.dup
592
+ directives << Howzit::Directive.new(
593
+ type: :log_level,
594
+ log_level_value: log_level,
595
+ line_number: line_num,
596
+ conditional_path: conditional_path
597
+ )
598
+ next
599
+ end
600
+
601
+ # Handle @set_var directive
602
+ if line =~ /^@set_var\s*\(/i
603
+ # Extract content between parentheses, handling nested parentheses
604
+ paren_content = line.sub(/^@set_var\s*\(/i, '').sub(/\)\s*$/, '')
605
+ # Split by first comma only - everything after first comma is the value
606
+ if paren_content =~ /^([^,]+),\s*(.+)$/
607
+ var_name = Regexp.last_match(1).strip
608
+ var_value = Regexp.last_match(2).strip
609
+ # Validate variable name: alphanumeric, dashes, underscores only
610
+ if var_name =~ /^[A-Za-z0-9_-]+$/
611
+ # Remove quotes from value if present (handles both single and double quotes)
612
+ var_value = Regexp.last_match(1) if var_value =~ /^["'](.+)["']$/
613
+ conditional_path = conditional_stack.dup
614
+ directives << Howzit::Directive.new(
615
+ type: :set_var,
616
+ var_name: var_name,
617
+ var_value: var_value,
618
+ line_number: line_num,
619
+ conditional_path: conditional_path
620
+ )
621
+ end
622
+ end
623
+ next
624
+ end
625
+
626
+ # Handle task directives (@run, @include, etc.)
627
+ unless line =~ /^@(?<cmd>include|run|copy|open|url)(?<optional>[!?]{1,2})?\((?<action>[^)]*?)\)(?<title>.*?)$/
628
+ next
629
+ end
630
+
631
+ cmd = Regexp.last_match(:cmd)
632
+ optional_str = Regexp.last_match(:optional) || ''
633
+ action = Regexp.last_match(:action)
634
+ title = Regexp.last_match(:title).strip
635
+
636
+ optional, default = define_optional(optional_str)
637
+ conditional_path = conditional_stack.dup
638
+ # If we're inside an @elsif/@else branch, include it in the path
639
+ conditional_path << current_branch_index if current_branch_index
640
+ directives << Howzit::Directive.new(
641
+ type: :task,
642
+ content: {
643
+ type: cmd.downcase.to_sym,
644
+ action: action,
645
+ title: title,
646
+ arguments: nil
647
+ },
648
+ optional: optional,
649
+ default: default,
650
+ line_number: line_num,
651
+ conditional_path: conditional_path
652
+ )
653
+ end
654
+
655
+ directives
656
+ end
657
+
658
+ ##
659
+ ## Run directives sequentially with conditional re-evaluation
660
+ ##
661
+ def run_sequential(nested: false, output: [], cols: 80)
662
+ # Initialize conditional state
663
+ conditional_state = {} # { index => { evaluated: bool, result: bool, matched_chain: bool } }
664
+ directive_index = 0
665
+ current_log_level = nil # Track current log level set by @log_level directives
666
+
667
+ # Initialize named_arguments with topic's named args (don't overwrite on each iteration)
668
+ Howzit.named_arguments ||= {}
669
+ Howzit.named_arguments.merge!(@named_args) if @named_args
670
+
671
+ unless @prereqs.empty?
672
+ begin
673
+ puts TTY::Box.frame("{by}#{@prereqs.join("\n\n").wrap(cols - 4)}{x}".c, width: cols)
674
+ rescue Errno::EPIPE
675
+ # Pipe closed, ignore
676
+ end
677
+ res = Prompt.yn('Have the above prerequisites been met?', default: true)
678
+ Process.exit 1 unless res
679
+ end
680
+
681
+ # Process directives sequentially
682
+ while directive_index < @directives.length
683
+ directive = @directives[directive_index]
684
+ directive_index += 1
685
+
686
+ # Update context for condition evaluation
687
+ metadata = @metadata || Howzit.buildnote&.metadata
688
+ context = { metadata: metadata }
689
+
690
+ # Handle conditional directives
691
+ if directive.conditional?
692
+ case directive.type
693
+ when :if, :unless
694
+ # Evaluate condition
695
+ result = ConditionEvaluator.evaluate(directive.condition, context)
696
+ result = !result if directive.directive_type == 'unless'
697
+
698
+ conditional_state[directive_index - 1] = {
699
+ evaluated: true,
700
+ result: result,
701
+ matched_chain: result,
702
+ condition: directive.condition,
703
+ directive_type: directive.directive_type
704
+ }
705
+
706
+ when :elsif
707
+ # Find the matching @if/@unless
708
+ matching_if_index = find_matching_if_index(directive_index - 1)
709
+ if matching_if_index && conditional_state[matching_if_index]
710
+ # If previous branch matched, this is false
711
+ if conditional_state[matching_if_index][:matched_chain]
712
+ conditional_state[directive_index - 1] = {
713
+ evaluated: true,
714
+ result: false,
715
+ matched_chain: false,
716
+ condition: directive.condition,
717
+ directive_type: 'elsif',
718
+ parent_index: matching_if_index
719
+ }
720
+ else
721
+ # Evaluate condition
722
+ result = ConditionEvaluator.evaluate(directive.condition, context)
723
+ conditional_state[directive_index - 1] = {
724
+ evaluated: true,
725
+ result: result,
726
+ matched_chain: result,
727
+ condition: directive.condition,
728
+ directive_type: 'elsif',
729
+ parent_index: matching_if_index
730
+ }
731
+ conditional_state[matching_if_index][:matched_chain] = true if result
732
+ end
733
+ end
734
+
735
+ when :else
736
+ # Find the matching @if/@unless
737
+ matching_if_index = find_matching_if_index(directive_index - 1)
738
+ if matching_if_index && conditional_state[matching_if_index]
739
+ # If any previous branch matched, else is false
740
+ if conditional_state[matching_if_index][:matched_chain]
741
+ conditional_state[directive_index - 1] = {
742
+ evaluated: true,
743
+ result: false,
744
+ matched_chain: false,
745
+ directive_type: 'else',
746
+ parent_index: matching_if_index
747
+ }
748
+ else
749
+ conditional_state[directive_index - 1] = {
750
+ evaluated: true,
751
+ result: true,
752
+ matched_chain: true,
753
+ directive_type: 'else',
754
+ parent_index: matching_if_index
755
+ }
756
+ conditional_state[matching_if_index][:matched_chain] = true
757
+ end
758
+ end
759
+
760
+ when :end
761
+ # End of conditional block - no action needed, state is managed by stack
762
+ end
763
+ next
764
+ end
765
+
766
+ # Handle @log_level directive (before task check)
767
+ if directive.log_level?
768
+ current_log_level = directive.log_level_value
769
+ next
770
+ end
771
+
772
+ # Handle @set_var directive (before task check)
773
+ if directive.set_var?
774
+ # Set the variable in named_arguments
775
+ Howzit.named_arguments ||= {}
776
+ value = directive.var_value
777
+
778
+ if value
779
+ # Check for command substitution: backticks or $()
780
+ if value =~ /^`(.+)`$/ || value =~ /^\$\((.+)\)$/
781
+ command = Regexp.last_match(1).strip
782
+ # Apply variable substitution to command before execution
783
+ command = command.render_arguments
784
+ # Execute command and capture output
785
+ begin
786
+ value = `#{command}`.strip
787
+ rescue StandardError => e
788
+ Howzit.console.warn("Error executing command in @set_var: #{e.message}")
789
+ value = ''
790
+ end
791
+ else
792
+ # Apply variable substitution to the value (in case it references other variables)
793
+ value = value.render_arguments
794
+ end
795
+ end
796
+
797
+ Howzit.named_arguments[directive.var_name] = value
798
+ # Re-evaluate conditionals after setting variable
799
+ re_evaluate_conditionals(conditional_state, directive_index - 1, context)
800
+ next
801
+ end
802
+
803
+ # Handle task directives
804
+ next unless directive.task?
805
+
806
+ # Check if all parent conditionals are true
807
+ should_execute = true
808
+
809
+ # If path ends with an @elsif/@else, skip the parent @if index
810
+ # (the index right before the elsif/else in the path)
811
+ path_to_check = directive.conditional_path.dup
812
+ if path_to_check.length >= 2
813
+ last_idx = path_to_check.last
814
+ last_state = conditional_state[last_idx]
815
+ if last_state && %w[elsif else].include?(last_state[:directive_type])
816
+ # Skip the parent @if index (the one before the elsif/else)
817
+ parent_if_idx = path_to_check[path_to_check.length - 2]
818
+ parent_if_state = conditional_state[parent_if_idx]
819
+ if parent_if_state && %w[if unless].include?(parent_if_state[:directive_type])
820
+ path_to_check.delete(parent_if_idx)
821
+ end
822
+ end
823
+ end
824
+
825
+ path_to_check.each do |cond_idx|
826
+ cond_state = conditional_state[cond_idx]
827
+ if cond_state.nil? || !cond_state[:evaluated] || !cond_state[:result]
828
+ should_execute = false
829
+ break
830
+ end
831
+ end
832
+
833
+ next unless should_execute
834
+
835
+ # Convert directive to task
836
+ task = directive.to_task(self, current_log_level: current_log_level)
837
+ next unless task
838
+
839
+ next if (task.optional || Howzit.options[:ask]) && !ask_task(task)
840
+
841
+ run_output, total, success = task.run
842
+
843
+ output.concat(run_output)
844
+ @results[:total] += total
845
+
846
+ if success
847
+ @results[:success] += total
848
+ else
849
+ Howzit.console.warn %({bw}\u{2297} {br}Error running task {bw}"#{task.title}"{x}).c
850
+
851
+ @results[:errors] += total
852
+
853
+ break unless Howzit.options[:force]
854
+ end
855
+
856
+ log_task_result(task, success)
857
+
858
+ # Re-evaluate all open conditionals after task execution
859
+ re_evaluate_conditionals(conditional_state, directive_index - 1, context)
860
+ end
861
+
862
+ total = "{bw}#{@results[:total]}{by} #{@results[:total] == 1 ? 'task' : 'tasks'}".c
863
+ errors = "{bw}#{@results[:errors]}{by} #{@results[:errors] == 1 ? 'error' : 'errors'}".c
864
+ @results[:message] += if @results[:errors].zero?
865
+ "{bg}\u{2713} {by}Ran #{total}{x}".c
866
+ elsif Howzit.options[:force]
867
+ "{br}\u{2715} {by}Completed #{total} with #{errors}{x}".c
868
+ else
869
+ "{br}\u{2715} {by}Ran #{total}, terminated due to error{x}".c
870
+ end
871
+
872
+ output.push(@results[:message]) if Howzit.options[:log_level] < 2 && !nested && !Howzit.options[:run]
873
+
874
+ unless @postreqs.empty?
875
+ begin
876
+ puts TTY::Box.frame("{bw}#{@postreqs.join("\n\n").wrap(cols - 4)}{x}".c, width: cols)
877
+ rescue Errno::EPIPE
878
+ # Pipe closed, ignore
879
+ end
880
+ end
881
+
882
+ output
883
+ end
884
+
885
+ ##
886
+ ## Find the index of the matching @if/@unless for an @elsif/@else/@end
887
+ ##
888
+ def find_matching_if_index(current_index)
889
+ stack_depth = 0
890
+ (current_index - 1).downto(0) do |i|
891
+ dir = @directives[i]
892
+ next unless dir.conditional?
893
+
894
+ case dir.type
895
+ when :end
896
+ stack_depth += 1
897
+ when :if, :unless
898
+ return i if stack_depth.zero?
899
+
900
+ stack_depth -= 1
901
+
902
+ when :elsif, :else
903
+ stack_depth -= 1 if stack_depth.positive?
904
+ end
905
+ end
906
+ nil
907
+ end
908
+
909
+ ##
910
+ ## Re-evaluate conditionals after a task runs (variables may have changed)
911
+ ##
912
+ def re_evaluate_conditionals(conditional_state, current_index, context)
913
+ # Re-evaluate all conditionals that come after the current task
914
+ # and before the next task
915
+ (current_index + 1).upto(@directives.length - 1) do |i|
916
+ dir = @directives[i]
917
+ break if dir.task? # Stop at next task
918
+
919
+ next unless dir.conditional?
920
+
921
+ case dir.type
922
+ when :if, :unless
923
+ if conditional_state[i]
924
+ # Re-evaluate
925
+ result = ConditionEvaluator.evaluate(dir.condition, context)
926
+ result = !result if dir.directive_type == 'unless'
927
+ conditional_state[i][:result] = result
928
+ conditional_state[i][:matched_chain] = result
929
+ end
930
+ when :elsif
931
+ matching_if_index = find_matching_if_index(i)
932
+ if matching_if_index && conditional_state[matching_if_index]
933
+ parent_state = conditional_state[matching_if_index]
934
+ if conditional_state[i]
935
+ if parent_state[:matched_chain] && !conditional_state[i][:matched_chain]
936
+ conditional_state[i][:result] = false
937
+ else
938
+ result = ConditionEvaluator.evaluate(dir.condition, context)
939
+ conditional_state[i][:result] = result
940
+ conditional_state[i][:matched_chain] = result
941
+ parent_state[:matched_chain] = true if result
942
+ end
943
+ end
944
+ end
945
+ when :else
946
+ matching_if_index = find_matching_if_index(i)
947
+ if matching_if_index && conditional_state[matching_if_index]
948
+ parent_state = conditional_state[matching_if_index]
949
+ if conditional_state[i]
950
+ if parent_state[:matched_chain]
951
+ conditional_state[i][:result] = false
952
+ else
953
+ conditional_state[i][:result] = true
954
+ conditional_state[i][:matched_chain] = true
955
+ parent_state[:matched_chain] = true
956
+ end
957
+ end
958
+ end
959
+ when :end
960
+ # No re-evaluation needed
961
+ end
962
+ end
963
+ end
421
964
  end
422
965
  end
@@ -3,5 +3,5 @@
3
3
  # Primary module for this gem.
4
4
  module Howzit
5
5
  # Current Howzit version.
6
- VERSION = '2.1.29'
6
+ VERSION = '2.1.31'
7
7
  end
data/lib/howzit.rb CHANGED
@@ -1,7 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Main config dir
4
- CONFIG_DIR = '~/.config/howzit'
4
+ # Prefer XDG_CONFIG_HOME if set, otherwise fall back to ~/.config/howzit
5
+ CONFIG_DIR = if ENV['XDG_CONFIG_HOME'] && !ENV['XDG_CONFIG_HOME'].empty?
6
+ File.join(ENV['XDG_CONFIG_HOME'], 'howzit')
7
+ else
8
+ File.join(Dir.home, '.config', 'howzit')
9
+ end
5
10
 
6
11
  # Config file name
7
12
  CONFIG_FILE = 'howzit.yaml'
@@ -39,8 +44,10 @@ require_relative 'howzit/stringutils'
39
44
  require_relative 'howzit/console_logger'
40
45
  require_relative 'howzit/config'
41
46
  require_relative 'howzit/script_comm'
47
+ require_relative 'howzit/script_support'
42
48
  require_relative 'howzit/condition_evaluator'
43
49
  require_relative 'howzit/conditional_content'
50
+ require_relative 'howzit/directive'
44
51
  require_relative 'howzit/task'
45
52
  require_relative 'howzit/topic'
46
53
  require_relative 'howzit/buildnote'