taski 0.9.0 → 0.9.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/CHANGELOG.md +25 -0
- data/README.md +43 -0
- data/docs/GUIDE.md +40 -1
- data/lib/taski/execution/executor.rb +32 -23
- data/lib/taski/execution/fiber_protocol.rb +27 -0
- data/lib/taski/execution/task_wrapper.rb +2 -2
- data/lib/taski/execution/worker_pool.rb +95 -54
- data/lib/taski/progress/config.rb +90 -0
- data/lib/taski/progress/layout/base.rb +25 -3
- data/lib/taski/progress/layout/simple.rb +17 -31
- data/lib/taski/progress/layout/theme_drop.rb +1 -1
- data/lib/taski/progress/layout/tree/event.rb +49 -0
- data/lib/taski/progress/layout/tree/live.rb +85 -0
- data/lib/taski/progress/layout/tree/structure.rb +142 -0
- data/lib/taski/progress/layout/tree.rb +25 -283
- data/lib/taski/progress/theme/base.rb +1 -1
- data/lib/taski/progress/theme/compact.rb +1 -1
- data/lib/taski/static_analysis/start_dep_analyzer.rb +400 -0
- data/lib/taski/task.rb +11 -6
- data/lib/taski/task_proxy.rb +59 -0
- data/lib/taski/test_helper.rb +1 -1
- data/lib/taski/version.rb +1 -1
- data/lib/taski.rb +25 -7
- metadata +22 -1
|
@@ -1,299 +1,41 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "
|
|
4
|
-
require_relative "
|
|
3
|
+
require_relative "tree/structure"
|
|
4
|
+
require_relative "tree/live"
|
|
5
|
+
require_relative "tree/event"
|
|
5
6
|
|
|
6
7
|
module Taski
|
|
7
8
|
module Progress
|
|
8
9
|
module Layout
|
|
9
|
-
# Tree layout for hierarchical task display.
|
|
10
|
-
# Renders tasks in a tree structure with visual connectors
|
|
10
|
+
# Tree layout module for hierarchical task display.
|
|
11
|
+
# Renders tasks in a tree structure with visual connectors.
|
|
11
12
|
#
|
|
12
|
-
#
|
|
13
|
-
# - TTY
|
|
14
|
-
# - Non-TTY
|
|
13
|
+
# Contains two implementations:
|
|
14
|
+
# - Tree::Live — TTY periodic-update with spinner animation
|
|
15
|
+
# - Tree::Event — Non-TTY event-driven incremental output
|
|
15
16
|
#
|
|
16
|
-
#
|
|
17
|
-
# BuildApplication
|
|
18
|
-
# ├── ⠹ SetupDatabase
|
|
19
|
-
# │ ├── ✓ CreateSchema (50ms)
|
|
20
|
-
# │ └── ⠹ SeedData
|
|
21
|
-
# ├── ○ ExtractLayers
|
|
22
|
-
# │ ├── ✓ DownloadLayer1 (100ms)
|
|
23
|
-
# │ └── ○ DownloadLayer2
|
|
24
|
-
# └── ✓ RunSystemCommand (200ms)
|
|
17
|
+
# Use Tree.for to automatically select the appropriate implementation.
|
|
25
18
|
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
19
|
+
# @example Auto-select based on output TTY
|
|
20
|
+
# layout = Taski::Progress::Layout::Tree.for
|
|
28
21
|
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
#
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def initialize(output: $stderr, theme: nil)
|
|
43
|
-
theme ||= Theme::Detail.new
|
|
44
|
-
super
|
|
45
|
-
@tree_nodes = {}
|
|
46
|
-
@node_depths = {}
|
|
47
|
-
@node_is_last = {}
|
|
48
|
-
@renderer_thread = nil
|
|
49
|
-
@running = false
|
|
50
|
-
@running_mutex = Mutex.new
|
|
51
|
-
@last_line_count = 0
|
|
52
|
-
@non_tty_started = false
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
# Returns the tree structure as a string.
|
|
56
|
-
# Uses the current theme to render task content for each node.
|
|
57
|
-
def render_tree
|
|
58
|
-
build_tree_lines.join("\n") + "\n"
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
# Override on_start to handle non-TTY mode
|
|
62
|
-
def on_start
|
|
63
|
-
@monitor.synchronize do
|
|
64
|
-
@nest_level += 1
|
|
65
|
-
return if @nest_level > 1
|
|
66
|
-
|
|
67
|
-
@start_time = Time.now
|
|
68
|
-
|
|
69
|
-
if should_activate?
|
|
70
|
-
@active = true
|
|
71
|
-
else
|
|
72
|
-
# Non-TTY mode: output execution start message
|
|
73
|
-
@non_tty_started = true
|
|
74
|
-
output_line(render_execution_started(@root_task_class)) if @root_task_class
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
handle_start if @active
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
# Override on_stop to handle non-TTY mode
|
|
82
|
-
def on_stop
|
|
83
|
-
was_active = false
|
|
84
|
-
non_tty_was_started = false
|
|
85
|
-
@monitor.synchronize do
|
|
86
|
-
@nest_level -= 1 if @nest_level > 0
|
|
87
|
-
return unless @nest_level == 0
|
|
88
|
-
was_active = @active
|
|
89
|
-
non_tty_was_started = @non_tty_started
|
|
90
|
-
@active = false
|
|
91
|
-
@non_tty_started = false
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
if was_active
|
|
95
|
-
handle_stop
|
|
96
|
-
elsif non_tty_was_started
|
|
97
|
-
# Non-TTY mode: output execution summary
|
|
98
|
-
output_execution_summary
|
|
99
|
-
end
|
|
100
|
-
flush_queued_messages
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
protected
|
|
104
|
-
|
|
105
|
-
def handle_ready
|
|
106
|
-
graph = context&.dependency_graph
|
|
107
|
-
root = context&.root_task_class
|
|
108
|
-
return unless graph && root
|
|
109
|
-
|
|
110
|
-
tree = build_tree_from_graph(root, graph)
|
|
111
|
-
register_tree_nodes(tree, depth: 0, is_last: true, ancestors_last: [])
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
# In TTY mode, tree is updated by render_live periodically.
|
|
115
|
-
# In non-TTY mode, output lines immediately with tree prefix.
|
|
116
|
-
def handle_task_update(task_class, current_state, phase)
|
|
117
|
-
return if @active # TTY mode: skip per-event output
|
|
118
|
-
|
|
119
|
-
# Non-TTY mode: output with tree prefix
|
|
120
|
-
progress = @tasks[task_class]
|
|
121
|
-
duration = compute_duration(progress, phase)
|
|
122
|
-
text = render_for_task_event(task_class, current_state, duration, nil, phase)
|
|
123
|
-
output_with_prefix(task_class, text) if text
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
def handle_group_started(task_class, group_name, phase)
|
|
127
|
-
return if @active # TTY mode: skip per-event output
|
|
128
|
-
|
|
129
|
-
# Non-TTY mode: output with tree prefix
|
|
130
|
-
text = render_group_started(task_class, group_name: group_name)
|
|
131
|
-
output_with_prefix(task_class, text) if text
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
def handle_group_completed(task_class, group_name, phase, duration)
|
|
135
|
-
return if @active # TTY mode: skip per-event output
|
|
136
|
-
|
|
137
|
-
# Non-TTY mode: output with tree prefix
|
|
138
|
-
text = render_group_succeeded(task_class, group_name: group_name, task_duration: duration)
|
|
139
|
-
output_with_prefix(task_class, text) if text
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
def should_activate?
|
|
143
|
-
tty?
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
def handle_start
|
|
147
|
-
@running_mutex.synchronize { @running = true }
|
|
148
|
-
start_spinner_timer
|
|
149
|
-
@output.print "\e[?25l" # Hide cursor
|
|
150
|
-
@renderer_thread = Thread.new do
|
|
151
|
-
loop do
|
|
152
|
-
break unless @running_mutex.synchronize { @running }
|
|
153
|
-
render_live
|
|
154
|
-
sleep @theme.render_interval
|
|
155
|
-
end
|
|
156
|
-
end
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
def handle_stop
|
|
160
|
-
@running_mutex.synchronize { @running = false }
|
|
161
|
-
@renderer_thread&.join
|
|
162
|
-
stop_spinner_timer
|
|
163
|
-
@output.print "\e[?25h" # Show cursor
|
|
164
|
-
render_final
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
private
|
|
168
|
-
|
|
169
|
-
# Output text with tree prefix for the given task
|
|
170
|
-
def output_with_prefix(task_class, text)
|
|
171
|
-
prefix = build_tree_prefix(task_class)
|
|
172
|
-
output_line("#{prefix}#{text}")
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
# Output execution summary for non-TTY mode
|
|
176
|
-
def output_execution_summary
|
|
177
|
-
output_line(render_execution_summary)
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
def build_tree_from_graph(task_class, graph, ancestors = Set.new)
|
|
181
|
-
is_circular = ancestors.include?(task_class)
|
|
182
|
-
node = {task_class: task_class, is_circular: is_circular, children: []}
|
|
183
|
-
return node if is_circular
|
|
184
|
-
|
|
185
|
-
new_ancestors = ancestors + [task_class]
|
|
186
|
-
deps = graph.dependencies_for(task_class)
|
|
187
|
-
deps.each do |dep|
|
|
188
|
-
child_node = build_tree_from_graph(dep, graph, new_ancestors)
|
|
189
|
-
node[:children] << child_node
|
|
190
|
-
end
|
|
191
|
-
node
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
def register_tree_nodes(node, depth:, is_last:, ancestors_last:)
|
|
195
|
-
return unless node
|
|
196
|
-
|
|
197
|
-
task_class = node[:task_class]
|
|
198
|
-
@tasks[task_class] ||= new_task_progress
|
|
199
|
-
@tree_nodes[task_class] = node
|
|
200
|
-
@node_depths[task_class] = depth
|
|
201
|
-
@node_is_last[task_class] = {is_last: is_last, ancestors_last: ancestors_last.dup}
|
|
202
|
-
|
|
203
|
-
children = node[:children]
|
|
204
|
-
children.each_with_index do |child, index|
|
|
205
|
-
child_is_last = (index == children.size - 1)
|
|
206
|
-
new_ancestors_last = ancestors_last + [is_last]
|
|
207
|
-
register_tree_nodes(child, depth: depth + 1, is_last: child_is_last, ancestors_last: new_ancestors_last)
|
|
208
|
-
end
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
def render_live
|
|
212
|
-
@monitor.synchronize do
|
|
213
|
-
lines = build_tree_lines
|
|
214
|
-
clear_previous_output
|
|
215
|
-
lines.each { |line| @output.puts line }
|
|
216
|
-
@output.flush
|
|
217
|
-
@last_line_count = lines.size
|
|
218
|
-
end
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
def render_final
|
|
222
|
-
@monitor.synchronize do
|
|
223
|
-
lines = build_tree_lines
|
|
224
|
-
clear_previous_output
|
|
225
|
-
|
|
226
|
-
lines.each { |line| @output.puts line }
|
|
227
|
-
@output.puts render_execution_summary
|
|
228
|
-
@output.flush
|
|
229
|
-
end
|
|
230
|
-
end
|
|
231
|
-
|
|
232
|
-
def clear_previous_output
|
|
233
|
-
return if @last_line_count == 0
|
|
234
|
-
# Move cursor up and clear lines
|
|
235
|
-
@output.print "\e[#{@last_line_count}A\e[J"
|
|
236
|
-
end
|
|
237
|
-
|
|
238
|
-
def build_tree_lines
|
|
239
|
-
return [] unless @root_task_class
|
|
240
|
-
|
|
241
|
-
lines = []
|
|
242
|
-
root_node = @tree_nodes[@root_task_class]
|
|
243
|
-
build_node_lines(root_node, lines)
|
|
244
|
-
lines
|
|
245
|
-
end
|
|
246
|
-
|
|
247
|
-
def build_node_lines(node, lines)
|
|
248
|
-
return unless node
|
|
249
|
-
|
|
250
|
-
task_class = node[:task_class]
|
|
251
|
-
prefix = build_tree_prefix(task_class)
|
|
252
|
-
content = build_task_content(task_class)
|
|
253
|
-
lines << "#{prefix}#{content}"
|
|
254
|
-
|
|
255
|
-
node[:children].each do |child|
|
|
256
|
-
build_node_lines(child, lines)
|
|
257
|
-
end
|
|
258
|
-
end
|
|
259
|
-
|
|
260
|
-
def build_task_content(task_class)
|
|
261
|
-
progress = @tasks[task_class]
|
|
262
|
-
|
|
263
|
-
case progress&.dig(:run_state)
|
|
264
|
-
when :running
|
|
265
|
-
render_task_started(task_class)
|
|
266
|
-
when :completed
|
|
267
|
-
render_task_succeeded(task_class, task_duration: compute_duration(progress, :run))
|
|
268
|
-
when :failed
|
|
269
|
-
render_task_failed(task_class, error: nil)
|
|
270
|
-
when :skipped
|
|
271
|
-
render_task_skipped(task_class)
|
|
22
|
+
# @example Explicit selection
|
|
23
|
+
# layout = Taski::Progress::Layout::Tree::Live.new # TTY
|
|
24
|
+
# layout = Taski::Progress::Layout::Tree::Event.new # non-TTY
|
|
25
|
+
module Tree
|
|
26
|
+
# Factory method to create the appropriate tree layout.
|
|
27
|
+
# Returns Tree::Live for TTY outputs, Tree::Event otherwise.
|
|
28
|
+
#
|
|
29
|
+
# @param output [IO] Output stream (default: $stderr)
|
|
30
|
+
# @param theme [Theme::Base, nil] Theme instance
|
|
31
|
+
# @return [Tree::Live, Tree::Event]
|
|
32
|
+
def self.for(output: $stderr, theme: nil)
|
|
33
|
+
if output.respond_to?(:tty?) && output.tty?
|
|
34
|
+
Live.new(output: output, theme: theme)
|
|
272
35
|
else
|
|
273
|
-
|
|
274
|
-
render_task_template(:task_pending, task:, execution: execution_drop)
|
|
36
|
+
Event.new(output: output, theme: theme)
|
|
275
37
|
end
|
|
276
38
|
end
|
|
277
|
-
|
|
278
|
-
def build_tree_prefix(task_class)
|
|
279
|
-
depth = @node_depths[task_class]
|
|
280
|
-
return "" if depth.nil? || depth == 0
|
|
281
|
-
|
|
282
|
-
last_info = @node_is_last[task_class]
|
|
283
|
-
return "" unless last_info
|
|
284
|
-
|
|
285
|
-
ancestors_last = last_info[:ancestors_last]
|
|
286
|
-
is_last = last_info[:is_last]
|
|
287
|
-
|
|
288
|
-
prefix = ""
|
|
289
|
-
# Skip the first ancestor (root) since root has no visual prefix
|
|
290
|
-
ancestors_last[1..].each do |ancestor_is_last|
|
|
291
|
-
prefix += ancestor_is_last ? SPACE : VERTICAL
|
|
292
|
-
end
|
|
293
|
-
|
|
294
|
-
prefix += is_last ? LAST_BRANCH : BRANCH
|
|
295
|
-
prefix
|
|
296
|
-
end
|
|
297
39
|
end
|
|
298
40
|
end
|
|
299
41
|
end
|
|
@@ -84,7 +84,7 @@ module Taski
|
|
|
84
84
|
end
|
|
85
85
|
|
|
86
86
|
def execution_complete
|
|
87
|
-
"[TASKI] Completed: {{ execution.
|
|
87
|
+
"[TASKI] Completed: {{ execution.done_count }}/{{ execution.total_count }} tasks ({{ execution.total_duration | format_duration }})"
|
|
88
88
|
end
|
|
89
89
|
|
|
90
90
|
def execution_fail
|
|
@@ -45,7 +45,7 @@ module Taski
|
|
|
45
45
|
|
|
46
46
|
# Execution complete with icon
|
|
47
47
|
def execution_complete
|
|
48
|
-
"{% icon %} [TASKI] Completed: {{ execution.
|
|
48
|
+
"{% icon %} [TASKI] Completed: {{ execution.done_count }}/{{ execution.total_count }} tasks ({{ execution.total_duration | format_duration }})"
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
# Execution fail with icon
|