taski 0.2.1 → 0.2.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/README.md +26 -1
- data/examples/README.md +57 -0
- data/examples/{complex_example.rb → advanced_patterns.rb} +15 -3
- data/examples/progress_demo.rb +166 -0
- data/examples/{readme_example.rb → quick_start.rb} +14 -3
- data/examples/tree_demo.rb +80 -0
- data/lib/taski/logger.rb +14 -3
- data/lib/taski/progress_display.rb +356 -0
- data/lib/taski/task/base.rb +51 -0
- data/lib/taski/task/define_api.rb +18 -2
- data/lib/taski/task/dependency_resolver.rb +6 -11
- data/lib/taski/task/instance_management.rb +46 -32
- data/lib/taski/utils.rb +107 -0
- data/lib/taski/version.rb +1 -1
- data/lib/taski.rb +2 -0
- metadata +9 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 28f2e52366511da4606df64a169c804712fcf4398b55a7b7d6430a1367cc183d
|
4
|
+
data.tar.gz: 97cc63c767ceaf7dda2fbb4345876c47d1fb1bf7e0395c8a2a0369bc8585e817
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4558c7e5ab3ae48301da573a0d2dffa935ecf2bdf7f04ccce1b92add5ea45b3aea1b562dcce0c6356d28b7eacd58fcdca030a9a2748fbafdda9d9046b5a09213
|
7
|
+
data.tar.gz: f440abca3014be0bffdd34cb8440f197b55403fc938660a8eb311e1bb6c64127bf9d033335d7e52f71cefec534ae4b6e41e2b83164d76b893d3a3c8f692bc221
|
data/README.md
CHANGED
@@ -99,7 +99,7 @@ EnvironmentConfig.build
|
|
99
99
|
|
100
100
|
### When to Use Each API
|
101
101
|
|
102
|
-
- **Define API**: Best for dynamic runtime dependencies. Cannot contain side effects in definition blocks.
|
102
|
+
- **Define API**: Best for dynamic runtime dependencies. Cannot contain side effects in definition blocks. Dependencies are analyzed at class definition time, not runtime.
|
103
103
|
- **Exports API**: Ideal for static dependencies. Supports side effects in build methods.
|
104
104
|
|
105
105
|
| Use Case | API | Example |
|
@@ -109,6 +109,8 @@ EnvironmentConfig.build
|
|
109
109
|
| Side effects | Exports | Database connections, I/O |
|
110
110
|
| Conditional processing | Define | Algorithm selection |
|
111
111
|
|
112
|
+
**Note**: Define API analyzes dependencies when the class is defined. Conditional dependencies like `ENV['USE_NEW'] ? TaskA : TaskB` will only include the task selected at class definition time, not runtime.
|
113
|
+
|
112
114
|
## ✨ Key Features
|
113
115
|
|
114
116
|
- **Automatic Dependency Resolution**: Dependencies detected through static analysis
|
@@ -172,6 +174,29 @@ WebServer.clean
|
|
172
174
|
# => Database disconnected
|
173
175
|
```
|
174
176
|
|
177
|
+
### Clean Method Idempotency
|
178
|
+
|
179
|
+
**Important**: The `clean` method must be idempotent - safe to call multiple times without errors.
|
180
|
+
|
181
|
+
```ruby
|
182
|
+
class FileTask < Taski::Task
|
183
|
+
exports :output_file
|
184
|
+
|
185
|
+
def build
|
186
|
+
@output_file = '/tmp/data.csv'
|
187
|
+
File.write(@output_file, process_data)
|
188
|
+
end
|
189
|
+
|
190
|
+
def clean
|
191
|
+
# ❌ Bad: Raises error if file doesn't exist
|
192
|
+
# File.delete(@output_file)
|
193
|
+
|
194
|
+
# ✅ Good: Check before delete
|
195
|
+
File.delete(@output_file) if File.exist?(@output_file)
|
196
|
+
end
|
197
|
+
end
|
198
|
+
```
|
199
|
+
|
175
200
|
### Error Handling
|
176
201
|
|
177
202
|
```ruby
|
data/examples/README.md
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
# Taski Examples
|
2
|
+
|
3
|
+
Learn Taski through practical examples, from basic concepts to advanced patterns.
|
4
|
+
|
5
|
+
## Getting Started
|
6
|
+
|
7
|
+
Start with these examples in order:
|
8
|
+
|
9
|
+
### 1. **[quick_start.rb](quick_start.rb)** - Your First Taski Program
|
10
|
+
- Basic task definition with Exports API
|
11
|
+
- Automatic dependency resolution
|
12
|
+
- Simple task execution
|
13
|
+
|
14
|
+
```bash
|
15
|
+
ruby examples/quick_start.rb
|
16
|
+
```
|
17
|
+
|
18
|
+
### 2. **[progress_demo.rb](progress_demo.rb)** - Rich CLI Progress Display
|
19
|
+
- Animated spinner with ANSI colors
|
20
|
+
- Real-time output capture and 5-line tail
|
21
|
+
- Production build scenarios
|
22
|
+
- TTY detection for clean file output
|
23
|
+
|
24
|
+
```bash
|
25
|
+
# Interactive mode with rich spinner
|
26
|
+
ruby examples/progress_demo.rb
|
27
|
+
|
28
|
+
# Clean output mode (no spinner)
|
29
|
+
ruby examples/progress_demo.rb > build.log 2>&1
|
30
|
+
cat build.log
|
31
|
+
```
|
32
|
+
|
33
|
+
### 3. **[advanced_patterns.rb](advanced_patterns.rb)** - Complex Dependency Patterns
|
34
|
+
- Mixed Exports API and Define API usage
|
35
|
+
- Environment-specific dependencies
|
36
|
+
- Feature flags and conditional logic
|
37
|
+
- Task reset and rebuild scenarios
|
38
|
+
|
39
|
+
```bash
|
40
|
+
ruby examples/advanced_patterns.rb
|
41
|
+
```
|
42
|
+
|
43
|
+
## Key Concepts Demonstrated
|
44
|
+
|
45
|
+
- **Exports API**: Static dependencies with `exports :property`
|
46
|
+
- **Define API**: Dynamic dependencies with `define :property, -> { ... }`
|
47
|
+
- **Progress Display**: Rich terminal output with spinners and colors
|
48
|
+
- **Output Capture**: Tail-style display of task output
|
49
|
+
- **Environment Configuration**: Different behavior based on runtime settings
|
50
|
+
- **Error Handling**: Graceful failure with progress indicators
|
51
|
+
|
52
|
+
## Next Steps
|
53
|
+
|
54
|
+
After exploring these examples:
|
55
|
+
- Read the main documentation
|
56
|
+
- Examine the test files for more usage patterns
|
57
|
+
- Check out the source code in `lib/taski/`
|
@@ -1,8 +1,22 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
-
#
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# Taski Advanced Patterns
|
5
|
+
#
|
6
|
+
# This example demonstrates advanced Taski patterns:
|
7
|
+
# - Mixed usage of Exports API and Define API
|
8
|
+
# - Environment-specific dependency resolution
|
9
|
+
# - Feature flag integration with dynamic dependencies
|
10
|
+
# - Task reset and rebuild scenarios
|
11
|
+
# - Conditional dependency evaluation
|
12
|
+
#
|
13
|
+
# Run: ruby examples/advanced_patterns.rb
|
3
14
|
|
4
15
|
require_relative "../lib/taski"
|
5
16
|
|
17
|
+
puts "⚡ Advanced Taski Patterns"
|
18
|
+
puts "=" * 40
|
19
|
+
|
6
20
|
# Mock classes for the example
|
7
21
|
class ProductionDB < Taski::Task
|
8
22
|
exports :connection_string
|
@@ -81,8 +95,6 @@ class Application < Taski::Task
|
|
81
95
|
end
|
82
96
|
|
83
97
|
# Test different environments
|
84
|
-
puts "=== Complex Example ==="
|
85
|
-
|
86
98
|
puts "\n1. Development Environment (default):"
|
87
99
|
ENV.delete("RAILS_ENV")
|
88
100
|
ENV.delete("FEATURE_REDIS_CACHE")
|
@@ -0,0 +1,166 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# Taski Progress Display Demo
|
5
|
+
#
|
6
|
+
# This comprehensive example demonstrates all progress display features:
|
7
|
+
# 1. Basic spinner animation with success/failure indicators
|
8
|
+
# 2. Output capture with 5-line tail display
|
9
|
+
# 3. Real-world build scenario with external command simulation
|
10
|
+
# 4. TTY detection for clean file output
|
11
|
+
#
|
12
|
+
# Run: ruby examples/progress_demo.rb
|
13
|
+
# Try: ruby examples/progress_demo.rb > build.log 2>&1 && cat build.log
|
14
|
+
|
15
|
+
require_relative "../lib/taski"
|
16
|
+
|
17
|
+
puts "🎯 Taski Progress Display Demo"
|
18
|
+
puts "=" * 50
|
19
|
+
|
20
|
+
# SECTION 1: Basic Spinner Animation
|
21
|
+
puts "\n📍 SECTION 1: Basic Spinner & Success/Failure Indicators"
|
22
|
+
puts "-" * 50
|
23
|
+
|
24
|
+
class ConfigTask < Taski::Task
|
25
|
+
exports :database_url, :cache_url
|
26
|
+
|
27
|
+
def build
|
28
|
+
sleep 0.8
|
29
|
+
@database_url = "postgres://localhost/myapp"
|
30
|
+
@cache_url = "redis://localhost:6379"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class DatabaseTask < Taski::Task
|
35
|
+
exports :connection
|
36
|
+
|
37
|
+
def build
|
38
|
+
sleep 1.2
|
39
|
+
@connection = "Connected to #{ConfigTask.database_url}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class ApplicationTask < Taski::Task
|
44
|
+
exports :status
|
45
|
+
|
46
|
+
def build
|
47
|
+
sleep 1.0
|
48
|
+
db = DatabaseTask.connection
|
49
|
+
@status = "App ready! #{db}"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
ApplicationTask.build
|
54
|
+
puts "🎉 Application Status: #{ApplicationTask.status}"
|
55
|
+
|
56
|
+
# SECTION 2: Output Capture Demo
|
57
|
+
puts "\n📍 SECTION 2: Output Capture with 5-Line Tail"
|
58
|
+
puts "-" * 50
|
59
|
+
|
60
|
+
class VerboseTask < Taski::Task
|
61
|
+
exports :result
|
62
|
+
|
63
|
+
def build
|
64
|
+
puts "Starting task initialization..."
|
65
|
+
sleep 0.3
|
66
|
+
|
67
|
+
puts "Loading configuration files..."
|
68
|
+
puts "Connecting to database..."
|
69
|
+
puts "Connection established: localhost:5432"
|
70
|
+
sleep 0.3
|
71
|
+
|
72
|
+
puts "Running initial checks..."
|
73
|
+
puts "Checking schema version..."
|
74
|
+
puts "Schema is up to date"
|
75
|
+
puts "Performing data validation..."
|
76
|
+
puts "Validating user records..."
|
77
|
+
puts "Validating product records..."
|
78
|
+
puts "All validations passed"
|
79
|
+
sleep 0.4
|
80
|
+
|
81
|
+
puts "Task completed successfully!"
|
82
|
+
@result = "All operations completed"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
VerboseTask.build
|
87
|
+
puts "📊 Verbose Task Result: #{VerboseTask.result}"
|
88
|
+
|
89
|
+
# SECTION 3: Production Build Scenario
|
90
|
+
puts "\n📍 SECTION 3: Production Build Scenario"
|
91
|
+
puts "-" * 50
|
92
|
+
|
93
|
+
class CompileTask < Taski::Task
|
94
|
+
exports :result
|
95
|
+
|
96
|
+
def build
|
97
|
+
puts "Starting compilation process..."
|
98
|
+
sleep 0.8
|
99
|
+
|
100
|
+
puts "Checking source files..."
|
101
|
+
puts "Found: main.c, utils.c, config.h"
|
102
|
+
sleep 0.6
|
103
|
+
|
104
|
+
puts "Running gcc compilation..."
|
105
|
+
puts "gcc -Wall -O2 -c main.c"
|
106
|
+
puts "gcc -Wall -O2 -c utils.c"
|
107
|
+
puts "main.c: In function 'main':"
|
108
|
+
puts "main.c:42: warning: unused variable 'temp'"
|
109
|
+
puts "utils.c: In function 'parse_config':"
|
110
|
+
puts "utils.c:15: warning: implicit declaration of function 'strcpy'"
|
111
|
+
sleep 0.8
|
112
|
+
|
113
|
+
puts "Linking objects..."
|
114
|
+
puts "gcc -o myapp main.o utils.o"
|
115
|
+
puts "Compilation successful!"
|
116
|
+
|
117
|
+
@result = "myapp binary created"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
class TestTask < Taski::Task
|
122
|
+
exports :test_result
|
123
|
+
|
124
|
+
def build
|
125
|
+
puts "Running test suite..."
|
126
|
+
sleep 0.2
|
127
|
+
|
128
|
+
(1..8).each do |i|
|
129
|
+
puts "Test #{i}/8: #{["PASS", "PASS", "FAIL", "PASS", "PASS", "PASS", "PASS", "PASS"][i - 1]}"
|
130
|
+
sleep 0.4
|
131
|
+
end
|
132
|
+
|
133
|
+
puts "Test summary: 7/8 passed, 1 failed"
|
134
|
+
@test_result = "Tests completed with 1 failure"
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
CompileTask.build
|
139
|
+
puts "📦 Compilation: #{CompileTask.result}"
|
140
|
+
|
141
|
+
TestTask.build
|
142
|
+
puts "🧪 Test Result: #{TestTask.test_result}"
|
143
|
+
|
144
|
+
# SECTION 4: Error Handling Demo
|
145
|
+
puts "\n📍 SECTION 4: Error Handling Demo"
|
146
|
+
puts "-" * 50
|
147
|
+
|
148
|
+
class FailingTask < Taski::Task
|
149
|
+
def build
|
150
|
+
puts "Attempting network connection..."
|
151
|
+
sleep 1.0 # Watch it spin before failing
|
152
|
+
puts "Connection timeout after 30 seconds"
|
153
|
+
puts "Retrying connection..."
|
154
|
+
sleep 0.5
|
155
|
+
raise StandardError, "Network connection failed!"
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
begin
|
160
|
+
FailingTask.build
|
161
|
+
rescue Taski::TaskBuildError => e
|
162
|
+
puts "🛡️ Error handled gracefully: #{e.message}"
|
163
|
+
end
|
164
|
+
|
165
|
+
puts "\n✨ Demo Complete!"
|
166
|
+
puts "Note: Rich spinner display only appears in terminals, not when output is redirected."
|
@@ -1,8 +1,20 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
-
#
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# Taski Quick Start Guide
|
5
|
+
#
|
6
|
+
# This example demonstrates the fundamentals of Taski:
|
7
|
+
# - Task definition with the Exports API
|
8
|
+
# - Automatic dependency resolution
|
9
|
+
# - Simple task execution
|
10
|
+
#
|
11
|
+
# Run: ruby examples/quick_start.rb
|
3
12
|
|
4
13
|
require_relative "../lib/taski"
|
5
14
|
|
15
|
+
puts "🚀 Taski Quick Start"
|
16
|
+
puts "=" * 30
|
17
|
+
|
6
18
|
# Simple static dependency using Exports API
|
7
19
|
class DatabaseSetup < Taski::Task
|
8
20
|
exports :connection_string
|
@@ -24,7 +36,6 @@ class APIServer < Taski::Task
|
|
24
36
|
end
|
25
37
|
|
26
38
|
# Execute - dependencies are resolved automatically
|
27
|
-
puts "=== Quick Start Example ==="
|
28
39
|
APIServer.build
|
29
40
|
|
30
|
-
puts "\
|
41
|
+
puts "\n✅ Result: APIServer running on port #{APIServer.port}"
|
@@ -0,0 +1,80 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# Tree Display Demo
|
5
|
+
#
|
6
|
+
# This example demonstrates the tree display functionality that shows
|
7
|
+
# task dependency relationships in a visual tree format.
|
8
|
+
#
|
9
|
+
# Run: ruby examples/tree_demo.rb
|
10
|
+
|
11
|
+
require_relative "../lib/taski"
|
12
|
+
|
13
|
+
puts "🌲 Taski Tree Display Demo"
|
14
|
+
puts "=" * 40
|
15
|
+
|
16
|
+
# Create a dependency chain for demonstration
|
17
|
+
class Database < Taski::Task
|
18
|
+
exports :connection_string
|
19
|
+
|
20
|
+
def build
|
21
|
+
@connection_string = "postgres://localhost/myapp"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class Cache < Taski::Task
|
26
|
+
exports :redis_url
|
27
|
+
|
28
|
+
def build
|
29
|
+
@redis_url = "redis://localhost:6379"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class Config < Taski::Task
|
34
|
+
exports :settings
|
35
|
+
|
36
|
+
def build
|
37
|
+
@settings = {
|
38
|
+
database: Database.connection_string,
|
39
|
+
cache: Cache.redis_url,
|
40
|
+
port: 3000
|
41
|
+
}
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class Logger < Taski::Task
|
46
|
+
exports :log_level
|
47
|
+
|
48
|
+
def build
|
49
|
+
@log_level = "info"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class WebServer < Taski::Task
|
54
|
+
exports :server_instance
|
55
|
+
|
56
|
+
def build
|
57
|
+
@server_instance = "WebServer configured with #{Config.settings[:database]} and #{Logger.log_level}"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
class Application < Taski::Task
|
62
|
+
def build
|
63
|
+
puts "Starting application..."
|
64
|
+
puts "Web server: #{WebServer.server_instance}"
|
65
|
+
puts "Config: #{Config.settings}"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
puts "\n📊 Application Dependency Tree:"
|
70
|
+
puts Application.tree
|
71
|
+
|
72
|
+
puts "\n🔍 Individual Component Trees:"
|
73
|
+
puts "\nWebServer dependencies:"
|
74
|
+
puts WebServer.tree
|
75
|
+
|
76
|
+
puts "\nConfig dependencies:"
|
77
|
+
puts Config.tree
|
78
|
+
|
79
|
+
puts "\n▶️ Building Application (to verify dependencies work):"
|
80
|
+
Application.build
|
data/lib/taski/logger.rb
CHANGED
@@ -48,11 +48,16 @@ module Taski
|
|
48
48
|
# Log task build start event
|
49
49
|
# @param task_name [String] Name of the task being built
|
50
50
|
# @param dependencies [Array] List of task dependencies
|
51
|
-
|
52
|
-
|
51
|
+
# @param args [Hash] Build arguments for parametrized builds
|
52
|
+
def task_build_start(task_name, dependencies: [], args: nil)
|
53
|
+
context = {
|
53
54
|
task: task_name,
|
54
55
|
dependencies: dependencies.size,
|
55
|
-
dependency_names: dependencies.map { |dep| dep.is_a?(Hash) ? dep[:klass].inspect : dep.inspect }
|
56
|
+
dependency_names: dependencies.map { |dep| dep.is_a?(Hash) ? dep[:klass].inspect : dep.inspect }
|
57
|
+
}
|
58
|
+
context[:args] = args if args && !args.empty?
|
59
|
+
|
60
|
+
info("Task build started", **context)
|
56
61
|
end
|
57
62
|
|
58
63
|
# Log task build completion event
|
@@ -181,6 +186,12 @@ module Taski
|
|
181
186
|
@logger ||= Logger.new
|
182
187
|
end
|
183
188
|
|
189
|
+
# Get the current progress display instance (always enabled)
|
190
|
+
# @return [ProgressDisplay] Current progress display instance
|
191
|
+
def progress_display
|
192
|
+
@progress_display ||= ProgressDisplay.new(force_enable: ENV["TASKI_FORCE_PROGRESS"] == "1")
|
193
|
+
end
|
194
|
+
|
184
195
|
# Configure the logger with new settings
|
185
196
|
# @param level [Symbol] Log level (:debug, :info, :warn, :error)
|
186
197
|
# @param output [IO] Output destination
|
@@ -0,0 +1,356 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "stringio"
|
4
|
+
|
5
|
+
module Taski
|
6
|
+
# Terminal control operations with ANSI escape sequences
|
7
|
+
class TerminalController
|
8
|
+
# ANSI escape sequences
|
9
|
+
MOVE_UP = "\033[A"
|
10
|
+
CLEAR_LINE = "\033[K"
|
11
|
+
MOVE_UP_AND_CLEAR = "#{MOVE_UP}#{CLEAR_LINE}"
|
12
|
+
|
13
|
+
def initialize(output)
|
14
|
+
@output = output
|
15
|
+
end
|
16
|
+
|
17
|
+
def clear_lines(count)
|
18
|
+
return if count == 0
|
19
|
+
|
20
|
+
count.times { @output.print MOVE_UP_AND_CLEAR }
|
21
|
+
end
|
22
|
+
|
23
|
+
def puts(text)
|
24
|
+
@output.puts text
|
25
|
+
end
|
26
|
+
|
27
|
+
def print(text)
|
28
|
+
@output.print text
|
29
|
+
end
|
30
|
+
|
31
|
+
def flush
|
32
|
+
@output.flush
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Spinner animation with dots-style characters
|
37
|
+
class SpinnerAnimation
|
38
|
+
SPINNER_CHARS = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"].freeze
|
39
|
+
FRAME_DELAY = 0.1
|
40
|
+
|
41
|
+
def initialize
|
42
|
+
@frame = 0
|
43
|
+
@running = false
|
44
|
+
@thread = nil
|
45
|
+
end
|
46
|
+
|
47
|
+
def start(terminal, task_name, &display_callback)
|
48
|
+
return if @running
|
49
|
+
|
50
|
+
@running = true
|
51
|
+
@frame = 0
|
52
|
+
|
53
|
+
@thread = Thread.new do
|
54
|
+
while @running
|
55
|
+
current_char = SPINNER_CHARS[@frame % SPINNER_CHARS.length]
|
56
|
+
display_callback&.call(current_char, task_name)
|
57
|
+
|
58
|
+
@frame += 1
|
59
|
+
sleep FRAME_DELAY
|
60
|
+
end
|
61
|
+
rescue
|
62
|
+
# Silently handle thread errors
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def stop
|
67
|
+
@running = false
|
68
|
+
@thread&.join(0.2)
|
69
|
+
@thread = nil
|
70
|
+
end
|
71
|
+
|
72
|
+
def running?
|
73
|
+
@running
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Captures stdout and maintains last N lines like tail -f
|
78
|
+
class OutputCapture
|
79
|
+
MAX_LINES = 10
|
80
|
+
DISPLAY_LINES = 5
|
81
|
+
|
82
|
+
def initialize(main_output)
|
83
|
+
@main_output = main_output
|
84
|
+
@buffer = []
|
85
|
+
@capturing = false
|
86
|
+
@original_stdout = nil
|
87
|
+
@pipe_reader = nil
|
88
|
+
@pipe_writer = nil
|
89
|
+
@capture_thread = nil
|
90
|
+
end
|
91
|
+
|
92
|
+
def start
|
93
|
+
return if @capturing
|
94
|
+
|
95
|
+
@buffer.clear
|
96
|
+
setup_stdout_redirection
|
97
|
+
@capturing = true
|
98
|
+
|
99
|
+
start_capture_thread
|
100
|
+
end
|
101
|
+
|
102
|
+
def stop
|
103
|
+
return unless @capturing
|
104
|
+
|
105
|
+
@capturing = false
|
106
|
+
|
107
|
+
# Restore stdout
|
108
|
+
restore_stdout
|
109
|
+
|
110
|
+
# Clean up pipes and thread
|
111
|
+
cleanup_capture_thread
|
112
|
+
cleanup_pipes
|
113
|
+
end
|
114
|
+
|
115
|
+
def last_lines
|
116
|
+
@buffer.last(DISPLAY_LINES)
|
117
|
+
end
|
118
|
+
|
119
|
+
def capturing?
|
120
|
+
@capturing
|
121
|
+
end
|
122
|
+
|
123
|
+
private
|
124
|
+
|
125
|
+
def setup_stdout_redirection
|
126
|
+
@original_stdout = $stdout
|
127
|
+
@pipe_reader, @pipe_writer = IO.pipe
|
128
|
+
$stdout = @pipe_writer
|
129
|
+
end
|
130
|
+
|
131
|
+
def restore_stdout
|
132
|
+
return unless @original_stdout
|
133
|
+
|
134
|
+
$stdout = @original_stdout
|
135
|
+
@original_stdout = nil
|
136
|
+
end
|
137
|
+
|
138
|
+
def start_capture_thread
|
139
|
+
@capture_thread = Thread.new do
|
140
|
+
while (line = @pipe_reader.gets)
|
141
|
+
line = line.chomp
|
142
|
+
next if line.empty?
|
143
|
+
next if skip_line?(line)
|
144
|
+
|
145
|
+
add_line_to_buffer(line)
|
146
|
+
end
|
147
|
+
rescue IOError
|
148
|
+
# Pipe closed, normal termination
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def skip_line?(line)
|
153
|
+
# Skip logger lines (they appear separately)
|
154
|
+
line.match?(/^\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}\]/)
|
155
|
+
end
|
156
|
+
|
157
|
+
def add_line_to_buffer(line)
|
158
|
+
@buffer << line
|
159
|
+
@buffer.shift while @buffer.length > MAX_LINES
|
160
|
+
end
|
161
|
+
|
162
|
+
def cleanup_capture_thread
|
163
|
+
@capture_thread&.join(0.1)
|
164
|
+
@capture_thread = nil
|
165
|
+
end
|
166
|
+
|
167
|
+
def cleanup_pipes
|
168
|
+
[@pipe_writer, @pipe_reader].each do |pipe|
|
169
|
+
pipe&.close
|
170
|
+
rescue IOError
|
171
|
+
# Already closed, ignore
|
172
|
+
end
|
173
|
+
@pipe_writer = @pipe_reader = nil
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
# Represents task execution status
|
178
|
+
class TaskStatus
|
179
|
+
attr_reader :name, :duration, :error
|
180
|
+
|
181
|
+
def initialize(name:, duration: nil, error: nil)
|
182
|
+
@name = name
|
183
|
+
@duration = duration
|
184
|
+
@error = error
|
185
|
+
end
|
186
|
+
|
187
|
+
def success?
|
188
|
+
@error.nil?
|
189
|
+
end
|
190
|
+
|
191
|
+
def failure?
|
192
|
+
!success?
|
193
|
+
end
|
194
|
+
|
195
|
+
def duration_ms
|
196
|
+
return nil unless @duration
|
197
|
+
(@duration * 1000).round(1)
|
198
|
+
end
|
199
|
+
|
200
|
+
def icon
|
201
|
+
success? ? "✅" : "❌"
|
202
|
+
end
|
203
|
+
|
204
|
+
def format_duration
|
205
|
+
return "" unless duration_ms
|
206
|
+
"(#{duration_ms}ms)"
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
# Main progress display controller
|
211
|
+
class ProgressDisplay
|
212
|
+
# ANSI colors
|
213
|
+
COLORS = {
|
214
|
+
reset: "\033[0m",
|
215
|
+
bold: "\033[1m",
|
216
|
+
dim: "\033[2m",
|
217
|
+
cyan: "\033[36m",
|
218
|
+
green: "\033[32m",
|
219
|
+
red: "\033[31m"
|
220
|
+
}.freeze
|
221
|
+
|
222
|
+
def initialize(output: $stdout, force_enable: nil)
|
223
|
+
@output = output
|
224
|
+
@terminal = TerminalController.new(output)
|
225
|
+
@spinner = SpinnerAnimation.new
|
226
|
+
@output_capture = OutputCapture.new(output)
|
227
|
+
|
228
|
+
# Enable if TTY or force enabled or environment variable set
|
229
|
+
@enabled = force_enable.nil? ? (output.tty? || ENV["TASKI_FORCE_PROGRESS"] == "1") : force_enable
|
230
|
+
|
231
|
+
@completed_tasks = []
|
232
|
+
@current_display_lines = 0
|
233
|
+
end
|
234
|
+
|
235
|
+
def start_task(task_name, dependencies: [])
|
236
|
+
puts "DEBUG: start_task called for #{task_name}, enabled: #{@enabled}" if ENV["TASKI_DEBUG"]
|
237
|
+
return unless @enabled
|
238
|
+
|
239
|
+
clear_current_display
|
240
|
+
@output_capture.start
|
241
|
+
|
242
|
+
start_spinner_display(task_name)
|
243
|
+
end
|
244
|
+
|
245
|
+
def complete_task(task_name, duration:)
|
246
|
+
return unless @enabled
|
247
|
+
|
248
|
+
status = TaskStatus.new(name: task_name, duration: duration)
|
249
|
+
finish_task(status)
|
250
|
+
end
|
251
|
+
|
252
|
+
def fail_task(task_name, error:, duration:)
|
253
|
+
return unless @enabled
|
254
|
+
|
255
|
+
status = TaskStatus.new(name: task_name, duration: duration, error: error)
|
256
|
+
finish_task(status)
|
257
|
+
end
|
258
|
+
|
259
|
+
def clear
|
260
|
+
return unless @enabled
|
261
|
+
|
262
|
+
@spinner.stop
|
263
|
+
@output_capture.stop
|
264
|
+
clear_current_display
|
265
|
+
|
266
|
+
# Display final summary of all completed tasks
|
267
|
+
if @completed_tasks.any?
|
268
|
+
@completed_tasks.each do |status|
|
269
|
+
@terminal.puts format_completed_task(status)
|
270
|
+
end
|
271
|
+
@terminal.flush
|
272
|
+
end
|
273
|
+
|
274
|
+
@completed_tasks.clear
|
275
|
+
@current_display_lines = 0
|
276
|
+
end
|
277
|
+
|
278
|
+
def enabled?
|
279
|
+
@enabled
|
280
|
+
end
|
281
|
+
|
282
|
+
private
|
283
|
+
|
284
|
+
def start_spinner_display(task_name)
|
285
|
+
@spinner.start(@terminal, task_name) do |spinner_char, name|
|
286
|
+
display_current_state(spinner_char, name)
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
def display_current_state(spinner_char, task_name)
|
291
|
+
clear_current_display
|
292
|
+
|
293
|
+
lines_count = 0
|
294
|
+
|
295
|
+
# Only display current task with spinner (no past completed tasks during execution)
|
296
|
+
@terminal.puts format_current_task(spinner_char, task_name)
|
297
|
+
lines_count += 1
|
298
|
+
|
299
|
+
# Display output lines
|
300
|
+
@output_capture.last_lines.each do |line|
|
301
|
+
@terminal.puts format_output_line(line)
|
302
|
+
lines_count += 1
|
303
|
+
end
|
304
|
+
|
305
|
+
@current_display_lines = lines_count
|
306
|
+
@terminal.flush
|
307
|
+
end
|
308
|
+
|
309
|
+
def finish_task(status)
|
310
|
+
@spinner.stop
|
311
|
+
|
312
|
+
# Capture output before stopping
|
313
|
+
captured_output = @output_capture.last_lines
|
314
|
+
@output_capture.stop
|
315
|
+
clear_current_display
|
316
|
+
|
317
|
+
# In test environments (when terminal is StringIO), include captured output
|
318
|
+
if @terminal.is_a?(StringIO) && captured_output.any?
|
319
|
+
captured_output.each do |line|
|
320
|
+
@terminal.puts line.chomp
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
@completed_tasks << status
|
325
|
+
display_final_state
|
326
|
+
end
|
327
|
+
|
328
|
+
def display_final_state
|
329
|
+
# Only display the newly completed task (last one)
|
330
|
+
if @completed_tasks.any?
|
331
|
+
latest_task = @completed_tasks.last
|
332
|
+
@terminal.puts format_completed_task(latest_task)
|
333
|
+
end
|
334
|
+
@terminal.flush
|
335
|
+
@current_display_lines = 1 # Only one line for the latest task
|
336
|
+
end
|
337
|
+
|
338
|
+
def format_completed_task(status)
|
339
|
+
color = status.success? ? COLORS[:green] : COLORS[:red]
|
340
|
+
"#{color}#{COLORS[:bold]}#{status.icon} #{status.name}#{COLORS[:reset]} #{COLORS[:dim]}#{status.format_duration}#{COLORS[:reset]}"
|
341
|
+
end
|
342
|
+
|
343
|
+
def format_current_task(spinner_char, task_name)
|
344
|
+
"#{COLORS[:cyan]}#{spinner_char}#{COLORS[:reset]} #{COLORS[:bold]}#{task_name}#{COLORS[:reset]}"
|
345
|
+
end
|
346
|
+
|
347
|
+
def format_output_line(line)
|
348
|
+
" #{COLORS[:dim]}#{line}#{COLORS[:reset]}"
|
349
|
+
end
|
350
|
+
|
351
|
+
def clear_current_display
|
352
|
+
@terminal.clear_lines(@current_display_lines)
|
353
|
+
@current_display_lines = 0
|
354
|
+
end
|
355
|
+
end
|
356
|
+
end
|
data/lib/taski/task/base.rb
CHANGED
@@ -41,6 +41,51 @@ module Taski
|
|
41
41
|
def __resolve__
|
42
42
|
@__resolve__ ||= {}
|
43
43
|
end
|
44
|
+
|
45
|
+
# Display dependency tree for this task
|
46
|
+
# @param prefix [String] Current indentation prefix
|
47
|
+
# @param visited [Set] Set of visited classes to prevent infinite loops
|
48
|
+
# @return [String] Formatted dependency tree
|
49
|
+
def tree(prefix = "", visited = Set.new)
|
50
|
+
return "#{prefix}#{name} (circular)\n" if visited.include?(self)
|
51
|
+
|
52
|
+
visited = visited.dup
|
53
|
+
visited << self
|
54
|
+
|
55
|
+
result = "#{prefix}#{name}\n"
|
56
|
+
|
57
|
+
dependencies = (@dependencies || []).uniq { |dep| extract_class(dep) }
|
58
|
+
dependencies.each_with_index do |dep, index|
|
59
|
+
dep_class = extract_class(dep)
|
60
|
+
is_last = index == dependencies.length - 1
|
61
|
+
|
62
|
+
connector = is_last ? "└── " : "├── "
|
63
|
+
child_prefix = prefix + (is_last ? " " : "│ ")
|
64
|
+
|
65
|
+
# For the dependency itself, we want to use the connector
|
66
|
+
# For its children, we want to use the child_prefix
|
67
|
+
dep_tree = dep_class.tree(child_prefix, visited)
|
68
|
+
# Replace the first line (which has child_prefix) with the proper connector
|
69
|
+
dep_lines = dep_tree.lines
|
70
|
+
if dep_lines.any?
|
71
|
+
# Replace the first line prefix with connector
|
72
|
+
first_line = dep_lines[0]
|
73
|
+
fixed_first_line = first_line.sub(/^#{Regexp.escape(child_prefix)}/, prefix + connector)
|
74
|
+
result += fixed_first_line
|
75
|
+
# Add the rest of the lines as-is
|
76
|
+
result += dep_lines[1..].join if dep_lines.length > 1
|
77
|
+
else
|
78
|
+
result += "#{prefix}#{connector}#{dep_class.name}\n"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
result
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
include Utils::DependencyUtils
|
88
|
+
private :extract_class
|
44
89
|
end
|
45
90
|
|
46
91
|
# === Instance Methods ===
|
@@ -51,6 +96,12 @@ module Taski
|
|
51
96
|
raise NotImplementedError, "You must implement the build method in your task class"
|
52
97
|
end
|
53
98
|
|
99
|
+
# Access build arguments passed to parametrized builds
|
100
|
+
# @return [Hash] Build arguments or empty hash if none provided
|
101
|
+
def build_args
|
102
|
+
@build_args || {}
|
103
|
+
end
|
104
|
+
|
54
105
|
# Clean method with default empty implementation
|
55
106
|
# Subclasses can override this method to implement cleanup logic
|
56
107
|
def clean
|
@@ -37,7 +37,19 @@ module Taski
|
|
37
37
|
def create_ref_method_if_needed
|
38
38
|
return if method_defined_for_define?(:ref)
|
39
39
|
|
40
|
-
define_singleton_method(:ref)
|
40
|
+
define_singleton_method(:ref) do |klass_name|
|
41
|
+
# During dependency analysis, track as dependency but defer resolution
|
42
|
+
if Thread.current[TASKI_ANALYZING_DEFINE_KEY]
|
43
|
+
# Create Reference object for deferred resolution
|
44
|
+
reference = Taski::Reference.new(klass_name)
|
45
|
+
|
46
|
+
# Track as dependency by throwing unresolved
|
47
|
+
throw :unresolved, [reference, :deref]
|
48
|
+
else
|
49
|
+
# At runtime, resolve to actual class
|
50
|
+
Object.const_get(klass_name)
|
51
|
+
end
|
52
|
+
end
|
41
53
|
mark_method_as_defined(:ref)
|
42
54
|
end
|
43
55
|
|
@@ -80,7 +92,11 @@ module Taski
|
|
80
92
|
|
81
93
|
# Reset resolution state
|
82
94
|
classes.each do |task_class|
|
83
|
-
task_class[:klass]
|
95
|
+
klass = task_class[:klass]
|
96
|
+
# Only reset Task classes, not Reference objects
|
97
|
+
if klass.respond_to?(:instance_variable_set) && !klass.is_a?(Taski::Reference)
|
98
|
+
klass.instance_variable_set(:@__resolve__, {})
|
99
|
+
end
|
84
100
|
end
|
85
101
|
|
86
102
|
classes
|
@@ -80,17 +80,7 @@ module Taski
|
|
80
80
|
|
81
81
|
# Build detailed error message for circular dependencies
|
82
82
|
def build_circular_dependency_message(cycle_path)
|
83
|
-
|
84
|
-
|
85
|
-
message = "Circular dependency detected!\n"
|
86
|
-
message += "Cycle: #{path_names.join(" → ")}\n\n"
|
87
|
-
message += "Detailed dependency chain:\n"
|
88
|
-
|
89
|
-
cycle_path.each_cons(2).with_index do |(from, to), index|
|
90
|
-
message += " #{index + 1}. #{from.name} depends on → #{to.name}\n"
|
91
|
-
end
|
92
|
-
|
93
|
-
message
|
83
|
+
Utils::CircularDependencyHelpers.build_error_message(cycle_path, "dependency")
|
94
84
|
end
|
95
85
|
|
96
86
|
public
|
@@ -133,6 +123,11 @@ module Taski
|
|
133
123
|
def dependency_exists?(dep_class)
|
134
124
|
(@dependencies || []).any? { |d| d[:klass] == dep_class }
|
135
125
|
end
|
126
|
+
|
127
|
+
private
|
128
|
+
|
129
|
+
include Utils::DependencyUtils
|
130
|
+
private :extract_class
|
136
131
|
end
|
137
132
|
end
|
138
133
|
end
|
@@ -8,9 +8,19 @@ module Taski
|
|
8
8
|
# === Lifecycle Management ===
|
9
9
|
|
10
10
|
# Build this task and all its dependencies
|
11
|
-
|
12
|
-
|
13
|
-
|
11
|
+
# @param args [Hash] Optional arguments for parametrized builds
|
12
|
+
# @return [Task] Returns task instance (singleton or temporary)
|
13
|
+
def build(**args)
|
14
|
+
if args.empty?
|
15
|
+
# Traditional build: singleton instance with caching
|
16
|
+
resolve_dependencies.reverse_each do |task_class|
|
17
|
+
task_class.ensure_instance_built
|
18
|
+
end
|
19
|
+
# Return the singleton instance for consistency
|
20
|
+
instance_variable_get(:@__task_instance)
|
21
|
+
else
|
22
|
+
# Parametrized build: temporary instance without caching
|
23
|
+
build_with_args(args)
|
14
24
|
end
|
15
25
|
end
|
16
26
|
|
@@ -41,6 +51,32 @@ module Taski
|
|
41
51
|
reset!
|
42
52
|
end
|
43
53
|
|
54
|
+
# === Parametrized Build Support ===
|
55
|
+
|
56
|
+
# Build temporary instance with arguments
|
57
|
+
# @param args [Hash] Build arguments
|
58
|
+
# @return [Task] Temporary task instance
|
59
|
+
def build_with_args(args)
|
60
|
+
# Resolve dependencies first (same as normal build)
|
61
|
+
resolve_dependencies.reverse_each do |task_class|
|
62
|
+
task_class.ensure_instance_built
|
63
|
+
end
|
64
|
+
|
65
|
+
# Create temporary instance with arguments
|
66
|
+
temp_instance = new
|
67
|
+
temp_instance.instance_variable_set(:@build_args, args)
|
68
|
+
|
69
|
+
# Build with logging using common utility
|
70
|
+
Utils::TaskBuildHelpers.with_build_logging(name.to_s,
|
71
|
+
dependencies: @dependencies || [],
|
72
|
+
args: args) do
|
73
|
+
temp_instance.build
|
74
|
+
temp_instance
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
private :build_with_args
|
79
|
+
|
44
80
|
# === Instance Management ===
|
45
81
|
|
46
82
|
# Ensure task instance is built (public because called from build)
|
@@ -93,18 +129,10 @@ module Taski
|
|
93
129
|
# @return [Task] Built task instance
|
94
130
|
def build_instance
|
95
131
|
instance = new
|
96
|
-
|
97
|
-
|
98
|
-
Taski.logger.task_build_start(name.to_s, dependencies: @dependencies || [])
|
132
|
+
Utils::TaskBuildHelpers.with_build_logging(name.to_s,
|
133
|
+
dependencies: @dependencies || []) do
|
99
134
|
instance.build
|
100
|
-
duration = Time.now - build_start_time
|
101
|
-
Taski.logger.task_build_complete(name.to_s, duration: duration)
|
102
135
|
instance
|
103
|
-
rescue => e
|
104
|
-
duration = Time.now - build_start_time
|
105
|
-
# Log the error with full context
|
106
|
-
Taski.logger.task_build_failed(name.to_s, error: e, duration: duration)
|
107
|
-
raise TaskBuildError, "Failed to build task #{name}: #{e.message}"
|
108
136
|
end
|
109
137
|
end
|
110
138
|
|
@@ -129,6 +157,8 @@ module Taski
|
|
129
157
|
end
|
130
158
|
end
|
131
159
|
|
160
|
+
private
|
161
|
+
|
132
162
|
# Build current dependency path from thread-local storage
|
133
163
|
# @return [Array<Class>] Array of classes in the current build path
|
134
164
|
def build_current_dependency_path
|
@@ -150,27 +180,11 @@ module Taski
|
|
150
180
|
# @param cycle_path [Array<Class>] The circular dependency path
|
151
181
|
# @return [String] Formatted error message
|
152
182
|
def build_runtime_circular_dependency_message(cycle_path)
|
153
|
-
|
154
|
-
|
155
|
-
message = "Circular dependency detected!\n"
|
156
|
-
message += "Cycle: #{path_names.join(" → ")}\n\n"
|
157
|
-
message += "The dependency chain is:\n"
|
158
|
-
|
159
|
-
cycle_path.each_cons(2).with_index do |(from, to), index|
|
160
|
-
message += " #{index + 1}. #{from.name} is trying to build → #{to.name}\n"
|
161
|
-
end
|
162
|
-
|
163
|
-
message += "\nThis creates an infinite loop that cannot be resolved."
|
164
|
-
message
|
183
|
+
Utils::CircularDependencyHelpers.build_error_message(cycle_path, "runtime")
|
165
184
|
end
|
166
185
|
|
167
|
-
|
168
|
-
|
169
|
-
# @return [Class] The dependency class
|
170
|
-
def extract_class(dep)
|
171
|
-
klass = dep[:klass]
|
172
|
-
klass.is_a?(Reference) ? klass.deref : klass
|
173
|
-
end
|
186
|
+
include Utils::DependencyUtils
|
187
|
+
private :extract_class
|
174
188
|
end
|
175
189
|
end
|
176
190
|
end
|
data/lib/taski/utils.rb
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Taski
|
4
|
+
# Common utility functions for the Taski framework
|
5
|
+
module Utils
|
6
|
+
# Handle circular dependency error message generation
|
7
|
+
module CircularDependencyHelpers
|
8
|
+
# Build detailed error message for circular dependencies
|
9
|
+
# @param cycle_path [Array<Class>] The circular dependency path
|
10
|
+
# @param context [String] Context of the error (dependency, runtime)
|
11
|
+
# @return [String] Formatted error message
|
12
|
+
def self.build_error_message(cycle_path, context = "dependency")
|
13
|
+
path_names = cycle_path.map { |klass| klass.name || klass.to_s }
|
14
|
+
|
15
|
+
message = "Circular dependency detected!\n"
|
16
|
+
message += "Cycle: #{path_names.join(" → ")}\n\n"
|
17
|
+
message += "The #{context} chain is:\n"
|
18
|
+
|
19
|
+
cycle_path.each_cons(2).with_index do |(from, to), index|
|
20
|
+
action = (context == "dependency") ? "depends on" : "is trying to build"
|
21
|
+
message += " #{index + 1}. #{from.name} #{action} → #{to.name}\n"
|
22
|
+
end
|
23
|
+
|
24
|
+
message += "\nThis creates an infinite loop that cannot be resolved." if context == "dependency"
|
25
|
+
message
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Common dependency utility functions
|
30
|
+
module DependencyUtils
|
31
|
+
# Extract class from dependency hash
|
32
|
+
# @param dep [Hash] Dependency information
|
33
|
+
# @return [Class] The dependency class
|
34
|
+
def extract_class(dep)
|
35
|
+
klass = dep[:klass]
|
36
|
+
klass.is_a?(Reference) ? klass.deref : klass
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Common task build utility functions
|
41
|
+
module TaskBuildHelpers
|
42
|
+
# Format arguments hash for display in error messages
|
43
|
+
# @param args [Hash] Arguments hash
|
44
|
+
# @return [String] Formatted arguments string
|
45
|
+
def self.format_args(args)
|
46
|
+
return "" if args.nil? || args.empty?
|
47
|
+
|
48
|
+
formatted_pairs = args.map do |key, value|
|
49
|
+
"#{key}: #{value.inspect}"
|
50
|
+
end
|
51
|
+
"{#{formatted_pairs.join(", ")}}"
|
52
|
+
end
|
53
|
+
|
54
|
+
# Execute block with comprehensive build logging and progress display
|
55
|
+
# @param task_name [String] Name of the task being built
|
56
|
+
# @param dependencies [Array] List of dependencies
|
57
|
+
# @param args [Hash] Build arguments for parametrized builds
|
58
|
+
# @yield Block to execute with logging
|
59
|
+
# @return [Object] Result of the block execution
|
60
|
+
def self.with_build_logging(task_name, dependencies: [], args: nil)
|
61
|
+
build_start_time = Time.now
|
62
|
+
|
63
|
+
begin
|
64
|
+
# Traditional logging first (before any stdout redirection)
|
65
|
+
Taski.logger.task_build_start(task_name, dependencies: dependencies, args: args)
|
66
|
+
|
67
|
+
# Show progress display if enabled (this may redirect stdout)
|
68
|
+
Taski.progress_display&.start_task(task_name, dependencies: dependencies)
|
69
|
+
|
70
|
+
result = yield
|
71
|
+
duration = Time.now - build_start_time
|
72
|
+
|
73
|
+
# Complete progress display first (this restores stdout)
|
74
|
+
Taski.progress_display&.complete_task(task_name, duration: duration)
|
75
|
+
|
76
|
+
# Then do logging (on restored stdout)
|
77
|
+
begin
|
78
|
+
Taski.logger.task_build_complete(task_name, duration: duration)
|
79
|
+
rescue IOError
|
80
|
+
# If logger fails due to closed stream, write to STDERR instead
|
81
|
+
warn "[#{Time.now.strftime("%Y-%m-%d %H:%M:%S.%3N")}] INFO Taski: Task build completed (task=#{task_name}, duration_ms=#{(duration * 1000).round(2)})"
|
82
|
+
end
|
83
|
+
|
84
|
+
result
|
85
|
+
rescue => e
|
86
|
+
duration = Time.now - build_start_time
|
87
|
+
|
88
|
+
# Complete progress display first (with error)
|
89
|
+
Taski.progress_display&.fail_task(task_name, error: e, duration: duration)
|
90
|
+
|
91
|
+
# Then do error logging (on restored stdout)
|
92
|
+
begin
|
93
|
+
Taski.logger.task_build_failed(task_name, error: e, duration: duration)
|
94
|
+
rescue IOError
|
95
|
+
# If logger fails due to closed stream, write to STDERR instead
|
96
|
+
warn "[#{Time.now.strftime("%Y-%m-%d %H:%M:%S.%3N")}] ERROR Taski: Task build failed (task=#{task_name}, error=#{e.message}, duration_ms=#{(duration * 1000).round(2)})"
|
97
|
+
end
|
98
|
+
|
99
|
+
error_message = "Failed to build task #{task_name}"
|
100
|
+
error_message += " with args #{format_args(args)}" if args && !args.empty?
|
101
|
+
error_message += ": #{e.message}"
|
102
|
+
raise TaskBuildError, error_message
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
data/lib/taski/version.rb
CHANGED
data/lib/taski.rb
CHANGED
@@ -6,8 +6,10 @@ require "monitor"
|
|
6
6
|
require_relative "taski/version"
|
7
7
|
require_relative "taski/exceptions"
|
8
8
|
require_relative "taski/logger"
|
9
|
+
require_relative "taski/progress_display"
|
9
10
|
require_relative "taski/reference"
|
10
11
|
require_relative "taski/dependency_analyzer"
|
12
|
+
require_relative "taski/utils"
|
11
13
|
|
12
14
|
# Load Task class components
|
13
15
|
require_relative "taski/task/base"
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: taski
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- ahogappa
|
8
8
|
bindir: exe
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-06-
|
10
|
+
date: 2025-06-27 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: prism
|
@@ -39,18 +39,23 @@ files:
|
|
39
39
|
- LICENSE
|
40
40
|
- README.md
|
41
41
|
- Rakefile
|
42
|
-
- examples/
|
43
|
-
- examples/
|
42
|
+
- examples/README.md
|
43
|
+
- examples/advanced_patterns.rb
|
44
|
+
- examples/progress_demo.rb
|
45
|
+
- examples/quick_start.rb
|
46
|
+
- examples/tree_demo.rb
|
44
47
|
- lib/taski.rb
|
45
48
|
- lib/taski/dependency_analyzer.rb
|
46
49
|
- lib/taski/exceptions.rb
|
47
50
|
- lib/taski/logger.rb
|
51
|
+
- lib/taski/progress_display.rb
|
48
52
|
- lib/taski/reference.rb
|
49
53
|
- lib/taski/task/base.rb
|
50
54
|
- lib/taski/task/define_api.rb
|
51
55
|
- lib/taski/task/dependency_resolver.rb
|
52
56
|
- lib/taski/task/exports_api.rb
|
53
57
|
- lib/taski/task/instance_management.rb
|
58
|
+
- lib/taski/utils.rb
|
54
59
|
- lib/taski/version.rb
|
55
60
|
- sig/taski.rbs
|
56
61
|
homepage: https://github.com/ahogappa/taski
|