taski 0.3.0 → 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.
Files changed (119) hide show
  1. checksums.yaml +4 -4
  2. data/.gem_rbs_collection/ast/2.4/.rbs_meta.yaml +9 -0
  3. data/.gem_rbs_collection/ast/2.4/ast.rbs +73 -0
  4. data/.gem_rbs_collection/minitest/5.25/.rbs_meta.yaml +9 -0
  5. data/.gem_rbs_collection/minitest/5.25/minitest/abstract_reporter.rbs +52 -0
  6. data/.gem_rbs_collection/minitest/5.25/minitest/assertion.rbs +17 -0
  7. data/.gem_rbs_collection/minitest/5.25/minitest/assertions.rbs +590 -0
  8. data/.gem_rbs_collection/minitest/5.25/minitest/backtrace_filter.rbs +23 -0
  9. data/.gem_rbs_collection/minitest/5.25/minitest/bench_spec.rbs +102 -0
  10. data/.gem_rbs_collection/minitest/5.25/minitest/benchmark.rbs +259 -0
  11. data/.gem_rbs_collection/minitest/5.25/minitest/composite_reporter.rbs +25 -0
  12. data/.gem_rbs_collection/minitest/5.25/minitest/compress.rbs +13 -0
  13. data/.gem_rbs_collection/minitest/5.25/minitest/error_on_warning.rbs +3 -0
  14. data/.gem_rbs_collection/minitest/5.25/minitest/expectation.rbs +2 -0
  15. data/.gem_rbs_collection/minitest/5.25/minitest/expectations.rbs +21 -0
  16. data/.gem_rbs_collection/minitest/5.25/minitest/guard.rbs +64 -0
  17. data/.gem_rbs_collection/minitest/5.25/minitest/mock.rbs +64 -0
  18. data/.gem_rbs_collection/minitest/5.25/minitest/parallel/executor.rbs +46 -0
  19. data/.gem_rbs_collection/minitest/5.25/minitest/parallel/test/class_methods.rbs +5 -0
  20. data/.gem_rbs_collection/minitest/5.25/minitest/parallel/test.rbs +3 -0
  21. data/.gem_rbs_collection/minitest/5.25/minitest/parallel.rbs +2 -0
  22. data/.gem_rbs_collection/minitest/5.25/minitest/pride_io.rbs +62 -0
  23. data/.gem_rbs_collection/minitest/5.25/minitest/pride_lol.rbs +19 -0
  24. data/.gem_rbs_collection/minitest/5.25/minitest/progress_reporter.rbs +11 -0
  25. data/.gem_rbs_collection/minitest/5.25/minitest/reportable.rbs +53 -0
  26. data/.gem_rbs_collection/minitest/5.25/minitest/reporter.rbs +5 -0
  27. data/.gem_rbs_collection/minitest/5.25/minitest/result.rbs +28 -0
  28. data/.gem_rbs_collection/minitest/5.25/minitest/runnable.rbs +163 -0
  29. data/.gem_rbs_collection/minitest/5.25/minitest/skip.rbs +6 -0
  30. data/.gem_rbs_collection/minitest/5.25/minitest/spec/dsl/instance_methods.rbs +48 -0
  31. data/.gem_rbs_collection/minitest/5.25/minitest/spec/dsl.rbs +129 -0
  32. data/.gem_rbs_collection/minitest/5.25/minitest/spec.rbs +11 -0
  33. data/.gem_rbs_collection/minitest/5.25/minitest/statistics_reporter.rbs +81 -0
  34. data/.gem_rbs_collection/minitest/5.25/minitest/summary_reporter.rbs +18 -0
  35. data/.gem_rbs_collection/minitest/5.25/minitest/test/lifecycle_hooks.rbs +92 -0
  36. data/.gem_rbs_collection/minitest/5.25/minitest/test.rbs +69 -0
  37. data/.gem_rbs_collection/minitest/5.25/minitest/unexpected_error.rbs +12 -0
  38. data/.gem_rbs_collection/minitest/5.25/minitest/unexpected_warning.rbs +6 -0
  39. data/.gem_rbs_collection/minitest/5.25/minitest/unit/test_case.rbs +3 -0
  40. data/.gem_rbs_collection/minitest/5.25/minitest/unit.rbs +4 -0
  41. data/.gem_rbs_collection/minitest/5.25/minitest.rbs +115 -0
  42. data/.gem_rbs_collection/parallel/1.20/.rbs_meta.yaml +9 -0
  43. data/.gem_rbs_collection/parallel/1.20/parallel.rbs +86 -0
  44. data/.gem_rbs_collection/parser/3.2/.rbs_meta.yaml +9 -0
  45. data/.gem_rbs_collection/parser/3.2/manifest.yaml +7 -0
  46. data/.gem_rbs_collection/parser/3.2/parser.rbs +193 -0
  47. data/.gem_rbs_collection/parser/3.2/polyfill.rbs +4 -0
  48. data/.gem_rbs_collection/rainbow/3.0/.rbs_meta.yaml +9 -0
  49. data/.gem_rbs_collection/rainbow/3.0/global.rbs +7 -0
  50. data/.gem_rbs_collection/rainbow/3.0/presenter.rbs +209 -0
  51. data/.gem_rbs_collection/rainbow/3.0/rainbow.rbs +5 -0
  52. data/.gem_rbs_collection/rake/13.0/.rbs_meta.yaml +9 -0
  53. data/.gem_rbs_collection/rake/13.0/manifest.yaml +2 -0
  54. data/.gem_rbs_collection/rake/13.0/rake.rbs +39 -0
  55. data/.gem_rbs_collection/regexp_parser/2.8/.rbs_meta.yaml +9 -0
  56. data/.gem_rbs_collection/regexp_parser/2.8/regexp_parser.rbs +17 -0
  57. data/.gem_rbs_collection/rubocop/1.57/.rbs_meta.yaml +9 -0
  58. data/.gem_rbs_collection/rubocop/1.57/rubocop.rbs +129 -0
  59. data/.gem_rbs_collection/rubocop-ast/1.30/.rbs_meta.yaml +9 -0
  60. data/.gem_rbs_collection/rubocop-ast/1.30/rubocop-ast.rbs +771 -0
  61. data/.gem_rbs_collection/simplecov/0.22/.rbs_meta.yaml +9 -0
  62. data/.gem_rbs_collection/simplecov/0.22/simplecov.rbs +54 -0
  63. data/README.md +137 -248
  64. data/Steepfile +19 -0
  65. data/docs/advanced-features.md +625 -0
  66. data/docs/api-guide.md +509 -0
  67. data/docs/error-handling.md +684 -0
  68. data/examples/README.md +95 -42
  69. data/examples/context_demo.rb +112 -0
  70. data/examples/data_pipeline_demo.rb +231 -0
  71. data/examples/parallel_progress_demo.rb +72 -0
  72. data/examples/quick_start.rb +4 -4
  73. data/examples/reexecution_demo.rb +127 -0
  74. data/examples/{section_configuration.rb → section_demo.rb} +49 -66
  75. data/lib/taski/context.rb +52 -0
  76. data/lib/taski/execution/coordinator.rb +63 -0
  77. data/lib/taski/execution/parallel_progress_display.rb +201 -0
  78. data/lib/taski/execution/registry.rb +72 -0
  79. data/lib/taski/execution/task_wrapper.rb +255 -0
  80. data/lib/taski/section.rb +26 -250
  81. data/lib/taski/static_analysis/analyzer.rb +34 -0
  82. data/lib/taski/static_analysis/dependency_graph.rb +90 -0
  83. data/lib/taski/static_analysis/visitor.rb +114 -0
  84. data/lib/taski/task.rb +173 -0
  85. data/lib/taski/version.rb +1 -1
  86. data/lib/taski.rb +45 -39
  87. data/rbs_collection.lock.yaml +116 -0
  88. data/rbs_collection.yaml +19 -0
  89. data/sig/taski.rbs +269 -62
  90. metadata +97 -32
  91. data/examples/advanced_patterns.rb +0 -119
  92. data/examples/progress_demo.rb +0 -166
  93. data/examples/tree_demo.rb +0 -205
  94. data/lib/taski/dependency_analyzer.rb +0 -231
  95. data/lib/taski/exceptions.rb +0 -17
  96. data/lib/taski/logger.rb +0 -158
  97. data/lib/taski/logging/formatter_factory.rb +0 -34
  98. data/lib/taski/logging/formatter_interface.rb +0 -19
  99. data/lib/taski/logging/json_formatter.rb +0 -26
  100. data/lib/taski/logging/simple_formatter.rb +0 -16
  101. data/lib/taski/logging/structured_formatter.rb +0 -44
  102. data/lib/taski/progress/display_colors.rb +0 -17
  103. data/lib/taski/progress/display_manager.rb +0 -115
  104. data/lib/taski/progress/output_capture.rb +0 -105
  105. data/lib/taski/progress/spinner_animation.rb +0 -46
  106. data/lib/taski/progress/task_formatter.rb +0 -25
  107. data/lib/taski/progress/task_status.rb +0 -38
  108. data/lib/taski/progress/terminal_controller.rb +0 -35
  109. data/lib/taski/progress_display.rb +0 -59
  110. data/lib/taski/reference.rb +0 -40
  111. data/lib/taski/task/base.rb +0 -90
  112. data/lib/taski/task/define_api.rb +0 -154
  113. data/lib/taski/task/dependency_resolver.rb +0 -73
  114. data/lib/taski/task/exports_api.rb +0 -31
  115. data/lib/taski/task/instance_management.rb +0 -203
  116. data/lib/taski/tree_colors.rb +0 -91
  117. data/lib/taski/utils/dependency_resolver_helper.rb +0 -85
  118. data/lib/taski/utils/tree_display_helper.rb +0 -71
  119. data/lib/taski/utils.rb +0 -107
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Taski Re-execution Demo
5
+ #
6
+ # This example demonstrates cache control and re-execution:
7
+ # - Default caching behavior
8
+ # - Task.new for fresh instances
9
+ # - Task.reset! for clearing all caches
10
+ #
11
+ # Run: ruby examples/reexecution_demo.rb
12
+
13
+ require_relative "../lib/taski"
14
+
15
+ puts "Taski Re-execution Demo"
16
+ puts "=" * 40
17
+
18
+ # Task that generates random values (to demonstrate caching)
19
+ class RandomGenerator < Taski::Task
20
+ exports :value, :timestamp
21
+
22
+ def run
23
+ @value = rand(1000)
24
+ @timestamp = Time.now.strftime("%H:%M:%S.%L")
25
+ puts " RandomGenerator.run called: value=#{@value}, time=#{@timestamp}"
26
+ end
27
+ end
28
+
29
+ # Task that depends on RandomGenerator
30
+ class Consumer < Taski::Task
31
+ exports :result
32
+
33
+ def run
34
+ random_value = RandomGenerator.value
35
+ @result = "Consumed value: #{random_value}"
36
+ puts " Consumer.run called: #{@result}"
37
+ end
38
+ end
39
+
40
+ puts "\n1. Default Caching Behavior"
41
+ puts "-" * 40
42
+ puts "First call to RandomGenerator.value:"
43
+ value1 = RandomGenerator.value
44
+ puts " => #{value1}"
45
+
46
+ puts "\nSecond call to RandomGenerator.value (cached, no run):"
47
+ value2 = RandomGenerator.value
48
+ puts " => #{value2}"
49
+
50
+ puts "\nValues are identical: #{value1 == value2}"
51
+
52
+ puts "\n" + "=" * 40
53
+ puts "\n2. Using Task.new for Fresh Instance"
54
+ puts "-" * 40
55
+ puts "Creating new instance with RandomGenerator.new:"
56
+
57
+ instance1 = RandomGenerator.new
58
+ instance1.run
59
+ puts " instance1.value = #{instance1.value}"
60
+
61
+ instance2 = RandomGenerator.new
62
+ instance2.run
63
+ puts " instance2.value = #{instance2.value}"
64
+
65
+ puts "\nNote: Each .new creates independent instance"
66
+ puts "Class-level cache unchanged: RandomGenerator.value = #{RandomGenerator.value}"
67
+
68
+ puts "\n" + "=" * 40
69
+ puts "\n3. Using reset! to Clear Cache"
70
+ puts "-" * 40
71
+ puts "Before reset!:"
72
+ puts " RandomGenerator.value = #{RandomGenerator.value}"
73
+
74
+ puts "\nCalling RandomGenerator.reset!..."
75
+ RandomGenerator.reset!
76
+
77
+ puts "\nAfter reset! (fresh execution):"
78
+ new_value = RandomGenerator.value
79
+ puts " RandomGenerator.value = #{new_value}"
80
+
81
+ puts "\n" + "=" * 40
82
+ puts "\n4. Dependency Chain with Re-execution"
83
+ puts "-" * 40
84
+
85
+ # Reset both tasks
86
+ RandomGenerator.reset!
87
+ Consumer.reset!
88
+
89
+ puts "First Consumer execution:"
90
+ result1 = Consumer.result
91
+ puts " => #{result1}"
92
+
93
+ puts "\nSecond Consumer execution (cached):"
94
+ result2 = Consumer.result
95
+ puts " => #{result2}"
96
+
97
+ puts "\nReset Consumer and re-execute:"
98
+ Consumer.reset!
99
+ result3 = Consumer.result
100
+ puts " => #{result3}"
101
+ puts " (Dependencies are re-resolved when task is reset)"
102
+
103
+ puts "\nReset both tasks:"
104
+ RandomGenerator.reset!
105
+ Consumer.reset!
106
+ result4 = Consumer.result
107
+ puts " => #{result4}"
108
+ puts " (New random value because both were reset)"
109
+
110
+ puts "\n" + "=" * 40
111
+ puts "\n5. Use Cases Summary"
112
+ puts "-" * 40
113
+ puts <<~SUMMARY
114
+ TaskClass.run / TaskClass.value
115
+ => Normal execution with caching (recommended for dependency graphs)
116
+
117
+ TaskClass.new.run
118
+ => Re-execute only this task (dependencies still use cache)
119
+ => Useful for: testing, one-off executions
120
+
121
+ TaskClass.reset!
122
+ => Clear this task's cache, next call will re-execute
123
+ => Useful for: environment changes, refreshing data
124
+ SUMMARY
125
+
126
+ puts "\n" + "=" * 40
127
+ puts "Re-execution demonstration complete!"
@@ -1,17 +1,12 @@
1
1
  #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- # Section Configuration Example
5
- # This example demonstrates how to use Taski::Section for dynamic implementation
6
- # selection and dependency resolution with configuration management.
7
- #
8
- # Key Features Demonstrated:
9
- # 1. DRY Principle: No need to duplicate 'exports' declarations in nested Task classes
10
- # - interface declaration automatically adds exports to nested Task classes
11
- # 2. Consistent API: impl must return Task classes - .build is called automatically
12
- # 3. Dynamic Implementation Selection: Different implementations based on environment
13
- # 4. Dependency Resolution: Sections are properly detected in dependency analysis
14
- # 5. Tree Visualization: Sections appear in dependency trees
2
+
3
+ # Section API Basics Example
4
+ # This example demonstrates runtime implementation selection with the Section API
5
+
6
+ # Section API is perfect for:
7
+ # - Environment-specific implementations (dev/staging/prod)
8
+ # - Different service adapters (AWS/GCP/local)
9
+ # - Clean abstraction with guaranteed interfaces
15
10
 
16
11
  require_relative "../lib/taski"
17
12
 
@@ -20,10 +15,10 @@ require_relative "../lib/taski"
20
15
  # for development and production environments
21
16
  class DatabaseSection < Taski::Section
22
17
  # Define the interface that implementations must provide
23
- interface :host, :port, :username, :password, :database_name, :pool_size
18
+ interfaces :host, :port, :username, :password, :database_name, :pool_size
24
19
 
25
20
  # Select implementation based on environment
26
- # Note: Must return a Task class - .build is automatically called
21
+ # Note: Must return a Task class - .run is automatically called
27
22
  # No 'self' needed - just define as instance method!
28
23
  def impl
29
24
  if ENV["RAILS_ENV"] == "production"
@@ -34,9 +29,9 @@ class DatabaseSection < Taski::Section
34
29
  end
35
30
 
36
31
  # Production implementation with secure settings
37
- # Note: exports are automatically inherited from interface declaration
32
+ # No exports needed - automatically inherited from interfaces
38
33
  class Production < Taski::Task
39
- def build
34
+ def run
40
35
  @host = "prod-db.example.com"
41
36
  @port = 5432
42
37
  @username = "app_user"
@@ -47,9 +42,8 @@ class DatabaseSection < Taski::Section
47
42
  end
48
43
 
49
44
  # Development implementation with local settings
50
- # Note: exports are automatically inherited from interface declaration
51
45
  class Development < Taski::Task
52
- def build
46
+ def run
53
47
  @host = "localhost"
54
48
  @port = 5432
55
49
  @username = "dev_user"
@@ -58,20 +52,17 @@ class DatabaseSection < Taski::Section
58
52
  @pool_size = 5
59
53
  end
60
54
  end
61
-
62
- # Apply auto-exports after all nested Task classes are defined
63
- apply_auto_exports
64
55
  end
65
56
 
66
57
  # Example 2: API Configuration Section
67
58
  # This section provides API endpoints and credentials
68
59
  class ApiSection < Taski::Section
69
- interface :base_url, :api_key, :timeout, :retry_count
60
+ interfaces :base_url, :api_key, :timeout, :retry_count
70
61
 
71
62
  # No 'self' needed - just define as instance method!
72
63
  def impl
73
64
  # Select based on feature flag
74
- # Note: Must return a Task class - .build is automatically called
65
+ # Note: Must return a Task class - .run is automatically called
75
66
  if ENV["USE_STAGING_API"] == "true"
76
67
  Staging
77
68
  else
@@ -79,9 +70,8 @@ class ApiSection < Taski::Section
79
70
  end
80
71
  end
81
72
 
82
- # Note: exports are automatically inherited from interface declaration - DRY principle!
83
73
  class Production < Taski::Task
84
- def build
74
+ def run
85
75
  @base_url = "https://api.example.com/v1"
86
76
  @api_key = ENV["PROD_API_KEY"] || "prod-key-123"
87
77
  @timeout = 30
@@ -89,25 +79,21 @@ class ApiSection < Taski::Section
89
79
  end
90
80
  end
91
81
 
92
- # Note: exports are automatically inherited from interface declaration - DRY principle!
93
82
  class Staging < Taski::Task
94
- def build
83
+ def run
95
84
  @base_url = "https://staging-api.example.com/v1"
96
85
  @api_key = ENV["STAGING_API_KEY"] || "staging-key-456"
97
86
  @timeout = 60
98
87
  @retry_count = 1
99
88
  end
100
89
  end
101
-
102
- # Apply auto-exports after all nested Task classes are defined
103
- apply_auto_exports
104
90
  end
105
91
 
106
92
  # Example 3: Task that depends on multiple sections
107
93
  class ApplicationSetup < Taski::Task
108
94
  exports :config_summary
109
95
 
110
- def build
96
+ def run
111
97
  puts "Setting up application with configuration:"
112
98
  puts "Database: #{DatabaseSection.host}:#{DatabaseSection.port}/#{DatabaseSection.database_name}"
113
99
  puts "API: #{ApiSection.base_url}"
@@ -134,7 +120,7 @@ end
134
120
  class DatabaseConnection < Taski::Task
135
121
  exports :connection
136
122
 
137
- def build
123
+ def run
138
124
  puts "Connecting to database..."
139
125
  # Use section configuration to create connection
140
126
  connection_string = "postgresql://#{DatabaseSection.username}:#{DatabaseSection.password}@#{DatabaseSection.host}:#{DatabaseSection.port}/#{DatabaseSection.database_name}"
@@ -146,7 +132,7 @@ end
146
132
  class ApiClient < Taski::Task
147
133
  exports :client
148
134
 
149
- def build
135
+ def run
150
136
  puts "Initializing API client..."
151
137
  @client = "API Client: #{ApiSection.base_url} (timeout: #{ApiSection.timeout}s, retries: #{ApiSection.retry_count})"
152
138
  puts @client
@@ -154,12 +140,12 @@ class ApiClient < Taski::Task
154
140
  end
155
141
 
156
142
  class Application < Taski::Task
157
- def build
143
+ def run
158
144
  puts "\n=== Starting Application ==="
159
145
 
160
146
  # Dependencies are automatically resolved
161
- # DatabaseConnection and ApiClient will be built first
162
- # which triggers building of their respective sections
147
+ # DatabaseConnection and ApiClient will be executed first
148
+ # which triggers execution of their respective sections
163
149
 
164
150
  puts "\nDatabase ready: #{DatabaseConnection.connection}"
165
151
  puts "API ready: #{ApiClient.client}"
@@ -171,42 +157,39 @@ class Application < Taski::Task
171
157
  end
172
158
  end
173
159
 
174
- # Demo script
175
- if __FILE__ == $0
176
- puts "Taski Section Configuration Example"
177
- puts "=" * 50
160
+ puts "Taski Section Configuration Example"
161
+ puts "=" * 50
178
162
 
179
- puts "\n1. Development Environment (default)"
180
- ENV["RAILS_ENV"] = "development"
181
- ENV["USE_STAGING_API"] = "false"
163
+ puts "\n1. Development Environment (default)"
164
+ ENV["RAILS_ENV"] = "development"
165
+ ENV["USE_STAGING_API"] = "false"
182
166
 
183
- # Reset all tasks to ensure fresh build
184
- [DatabaseSection, ApiSection, ApplicationSetup, DatabaseConnection, ApiClient, Application].each(&:reset!)
167
+ # Reset all tasks to ensure fresh build
168
+ [DatabaseSection, ApiSection, ApplicationSetup, DatabaseConnection, ApiClient, Application].each(&:reset!)
185
169
 
186
- Application.build
170
+ Application.run
187
171
 
188
- puts "\n" + "=" * 50
189
- puts "\n2. Production Environment with Staging API"
190
- ENV["RAILS_ENV"] = "production"
191
- ENV["USE_STAGING_API"] = "true"
172
+ puts "\n" + "=" * 50
173
+ puts "\n2. Production Environment with Staging API"
174
+ ENV["RAILS_ENV"] = "production"
175
+ ENV["USE_STAGING_API"] = "true"
192
176
 
193
- # Reset all tasks to see different configuration
194
- [DatabaseSection, ApiSection, ApplicationSetup, DatabaseConnection, ApiClient, Application].each(&:reset!)
177
+ # Reset all tasks to see different configuration
178
+ [DatabaseSection, ApiSection, ApplicationSetup, DatabaseConnection, ApiClient, Application].each(&:reset!)
195
179
 
196
- Application.build
180
+ Application.run
197
181
 
198
- puts "\n" + "=" * 50
199
- puts "\n3. Dependency Tree Visualization"
200
- puts "\nApplication dependency tree:"
201
- puts Application.tree
182
+ puts "\n" + "=" * 50
183
+ puts "\n3. Dependency Tree Visualization"
184
+ puts "\nApplication dependency tree:"
185
+ puts Application.tree
202
186
 
203
- puts "\nDatabaseConnection dependency tree:"
204
- puts DatabaseConnection.tree
187
+ puts "\nDatabaseConnection dependency tree:"
188
+ puts DatabaseConnection.tree
205
189
 
206
- puts "\nApiClient dependency tree:"
207
- puts ApiClient.tree
190
+ puts "\nApiClient dependency tree:"
191
+ puts ApiClient.tree
208
192
 
209
- puts "\n" + "=" * 50
210
- puts "\nSection dependency resolution successfully demonstrated!"
211
- puts "Notice how sections appear in the dependency trees and logs."
212
- end
193
+ puts "\n" + "=" * 50
194
+ puts "\nSection dependency resolution successfully demonstrated!"
195
+ puts "Notice how sections appear in the dependency trees and logs."
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "monitor"
4
+
5
+ module Taski
6
+ # Runtime context accessible from any task (not included in dependency analysis).
7
+ class Context
8
+ @monitor = Monitor.new
9
+
10
+ class << self
11
+ # @return [String] The working directory path
12
+ def working_directory
13
+ @monitor.synchronize do
14
+ @working_directory ||= Dir.pwd
15
+ end
16
+ end
17
+
18
+ # @return [Time] The start time
19
+ def started_at
20
+ @monitor.synchronize do
21
+ @started_at ||= Time.now
22
+ end
23
+ end
24
+
25
+ # @return [Class, nil] The root task class or nil if not set
26
+ def root_task
27
+ @monitor.synchronize do
28
+ @root_task
29
+ end
30
+ end
31
+
32
+ # Called internally when a task is first invoked. Only the first call has effect.
33
+ # @param task_class [Class] The task class to set as root
34
+ def set_root_task(task_class)
35
+ @monitor.synchronize do
36
+ return if @root_task
37
+ @root_task = task_class
38
+ @started_at ||= Time.now
39
+ @working_directory ||= Dir.pwd
40
+ end
41
+ end
42
+
43
+ def reset!
44
+ @monitor.synchronize do
45
+ @working_directory = nil
46
+ @started_at = nil
47
+ @root_task = nil
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Taski
4
+ module Execution
5
+ class Coordinator
6
+ def initialize(registry:, analyzer:)
7
+ @registry = registry
8
+ @analyzer = analyzer
9
+ end
10
+
11
+ # @param task_class [Class] The task class whose dependencies should be started
12
+ def start_dependencies(task_class)
13
+ dependencies = get_dependencies(task_class)
14
+ return if dependencies.empty?
15
+
16
+ dependencies.each do |dep_class|
17
+ start_dependency_execution(dep_class)
18
+ end
19
+ end
20
+
21
+ # @param task_class [Class] The task class whose dependencies should be cleaned
22
+ def start_clean_dependencies(task_class)
23
+ dependencies = get_dependencies(task_class)
24
+ return if dependencies.empty?
25
+
26
+ dependencies.each do |dep_class|
27
+ start_dependency_clean(dep_class)
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def get_dependencies(task_class)
34
+ if task_class.respond_to?(:cached_dependencies)
35
+ task_class.cached_dependencies
36
+ else
37
+ @analyzer.analyze(task_class)
38
+ end
39
+ end
40
+
41
+ def start_thread_with(&block)
42
+ thread = Thread.new(&block)
43
+ @registry.register_thread(thread)
44
+ end
45
+
46
+ def start_dependency_execution(dep_class)
47
+ exported_methods = dep_class.exported_methods
48
+
49
+ exported_methods.each do |method|
50
+ start_thread_with do
51
+ dep_class.public_send(method)
52
+ end
53
+ end
54
+ end
55
+
56
+ def start_dependency_clean(dep_class)
57
+ start_thread_with do
58
+ dep_class.public_send(:clean)
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,201 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "monitor"
4
+
5
+ module Taski
6
+ module Execution
7
+ class ParallelProgressDisplay
8
+ SPINNER_FRAMES = %w[⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏].freeze
9
+
10
+ class TaskProgress
11
+ attr_accessor :state, :start_time, :end_time, :error, :duration
12
+
13
+ def initialize
14
+ @state = :pending
15
+ @start_time = nil
16
+ @end_time = nil
17
+ @error = nil
18
+ @duration = nil
19
+ end
20
+ end
21
+
22
+ def initialize(output: $stdout)
23
+ @output = output
24
+ @tasks = {}
25
+ @monitor = Monitor.new
26
+ @spinner_index = 0
27
+ @renderer_thread = nil
28
+ @running = false
29
+ end
30
+
31
+ # @param task_class [Class] The task class to register
32
+ def register_task(task_class)
33
+ @monitor.synchronize do
34
+ @tasks[task_class] = TaskProgress.new
35
+ end
36
+ end
37
+
38
+ # @param task_class [Class] The task class to check
39
+ # @return [Boolean] true if the task is registered
40
+ def task_registered?(task_class)
41
+ @monitor.synchronize do
42
+ @tasks.key?(task_class)
43
+ end
44
+ end
45
+
46
+ # @param task_class [Class] The task class to update
47
+ # @param state [Symbol] The new state (:pending, :running, :completed, :failed)
48
+ # @param duration [Float] Duration in milliseconds (for completed tasks)
49
+ # @param error [Exception] Error object (for failed tasks)
50
+ def update_task(task_class, state:, duration: nil, error: nil)
51
+ @monitor.synchronize do
52
+ progress = @tasks[task_class]
53
+ return unless progress
54
+
55
+ progress.state = state
56
+ progress.duration = duration if duration
57
+ progress.error = error if error
58
+
59
+ case state
60
+ when :running
61
+ progress.start_time = Time.now
62
+ when :completed, :failed
63
+ progress.end_time = Time.now
64
+ end
65
+ end
66
+ end
67
+
68
+ # @param task_class [Class] The task class
69
+ # @return [Symbol] The task state
70
+ def task_state(task_class)
71
+ @monitor.synchronize do
72
+ @tasks[task_class]&.state
73
+ end
74
+ end
75
+
76
+ def render
77
+ @monitor.synchronize do
78
+ @tasks.each do |task_class, progress|
79
+ line = format_task_line(task_class, progress)
80
+ @output.puts line
81
+ end
82
+ end
83
+ end
84
+
85
+ def start
86
+ return if @running
87
+
88
+ @running = true
89
+ @renderer_thread = Thread.new do
90
+ loop do
91
+ break unless @running
92
+ render_live
93
+ sleep 0.1
94
+ end
95
+ end
96
+ end
97
+
98
+ def stop
99
+ return unless @running
100
+
101
+ @running = false
102
+ @renderer_thread&.join
103
+ render_final
104
+ end
105
+
106
+ private
107
+
108
+ # @return [Array<String>] Array of formatted task lines
109
+ def collect_task_lines
110
+ @tasks.map do |task_class, progress|
111
+ format_task_line(task_class, progress)
112
+ end
113
+ end
114
+
115
+ def render_live
116
+ return unless @output.tty?
117
+
118
+ @monitor.synchronize do
119
+ @spinner_index += 1
120
+
121
+ lines = collect_task_lines
122
+
123
+ lines.each_with_index do |line, index|
124
+ @output.print "\r\e[K#{line}"
125
+ @output.print "\n" unless index == lines.length - 1
126
+ end
127
+
128
+ @output.print "\e[#{lines.length - 1}A" if lines.length > 1
129
+ end
130
+ end
131
+
132
+ def render_final
133
+ @monitor.synchronize do
134
+ lines = collect_task_lines
135
+
136
+ if @output.tty? && lines.length > 0
137
+ lines.each_with_index do |_, index|
138
+ @output.print "\r\e[K"
139
+ @output.print "\e[1B" unless index == lines.length - 1
140
+ end
141
+ @output.print "\e[#{lines.length - 1}A" if lines.length > 1
142
+ end
143
+
144
+ lines.each do |line|
145
+ @output.puts line
146
+ end
147
+ end
148
+ end
149
+
150
+ # @param task_class [Class] The task class
151
+ # @param progress [TaskProgress] The task progress
152
+ # @return [String] Formatted line
153
+ def format_task_line(task_class, progress)
154
+ icon = task_icon(progress.state)
155
+ name = task_class.name || "AnonymousTask"
156
+ details = task_details(progress)
157
+
158
+ "#{icon} #{name}#{details}"
159
+ end
160
+
161
+ # @param state [Symbol] The task state
162
+ # @return [String] The icon character
163
+ def task_icon(state)
164
+ case state
165
+ when :completed
166
+ "✅"
167
+ when :failed
168
+ "❌"
169
+ when :running
170
+ spinner_char
171
+ when :pending
172
+ "⏳"
173
+ else
174
+ "❓"
175
+ end
176
+ end
177
+
178
+ # @return [String] Current spinner frame
179
+ def spinner_char
180
+ SPINNER_FRAMES[@spinner_index % SPINNER_FRAMES.length]
181
+ end
182
+
183
+ # @param progress [TaskProgress] The task progress
184
+ # @return [String] Details string
185
+ def task_details(progress)
186
+ case progress.state
187
+ when :completed
188
+ " (#{progress.duration}ms)"
189
+ when :failed
190
+ " (failed)"
191
+ when :running
192
+ " (running)"
193
+ when :pending
194
+ " (pending)"
195
+ else
196
+ ""
197
+ end
198
+ end
199
+ end
200
+ end
201
+ end