hiiro 0.1.30 → 0.1.32
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 +4 -23
- data/TODO.md +21 -0
- data/bin/g-pr +1 -1
- data/bin/h-branch +1 -1
- data/bin/{h-subtask → h-osubtask} +2 -2
- data/bin/{h-task → h-otask} +68 -19
- data/bin/h-pr +1 -1
- data/exe/h +1 -1
- data/lib/hiiro/version.rb +1 -1
- data/notes +224 -0
- data/plugins/{task.rb → old_task.rb} +149 -24
- data/plugins/tasks.rb +813 -0
- metadata +8 -5
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module OldTask
|
|
4
4
|
def self.load(hiiro)
|
|
5
5
|
hiiro.load_plugin(Tmux)
|
|
6
6
|
attach_methods(hiiro)
|
|
@@ -58,6 +58,8 @@ module Task
|
|
|
58
58
|
tasks.new_subtask(subtask_name)
|
|
59
59
|
in ['subtask', 'switch', subtask_name]
|
|
60
60
|
tasks.switch_subtask(subtask_name)
|
|
61
|
+
in ['subtask', 'switch']
|
|
62
|
+
tasks.switch_subtask
|
|
61
63
|
in [subcmd, *sargs]
|
|
62
64
|
match = runner_map.keys.find { |full_subcmd| full_subcmd.to_s.start_with?(subcmd) }
|
|
63
65
|
|
|
@@ -77,7 +79,7 @@ module Task
|
|
|
77
79
|
def self.attach_methods(hiiro)
|
|
78
80
|
hiiro.instance_eval do
|
|
79
81
|
def task_manager
|
|
80
|
-
@task_manager ||=
|
|
82
|
+
@task_manager ||= OldTask::TaskManager.new(self)
|
|
81
83
|
end
|
|
82
84
|
end
|
|
83
85
|
end
|
|
@@ -119,16 +121,50 @@ module Task
|
|
|
119
121
|
current = current_task
|
|
120
122
|
active, available = trees.partition { |tree_name| task_for_tree(tree_name) }
|
|
121
123
|
|
|
124
|
+
# Group active trees by parent task
|
|
125
|
+
groups = {}
|
|
122
126
|
active.each do |tree_name|
|
|
123
127
|
task = task_for_tree(tree_name)
|
|
124
|
-
|
|
125
|
-
|
|
128
|
+
parent = task.include?('/') ? task.split('/').first : task
|
|
129
|
+
groups[parent] ||= []
|
|
130
|
+
groups[parent] << { tree: tree_name, task: task }
|
|
126
131
|
end
|
|
127
132
|
|
|
128
|
-
|
|
133
|
+
first_group = true
|
|
134
|
+
groups.each do |parent, entries|
|
|
135
|
+
puts unless first_group
|
|
136
|
+
first_group = false
|
|
137
|
+
|
|
138
|
+
# Sort so the main entry (parent itself or parent/main) comes first
|
|
139
|
+
entries.sort_by! { |e| e[:task] == parent || e[:task].end_with?('/main') ? 0 : 1 }
|
|
140
|
+
|
|
141
|
+
entries.each_with_index do |entry, i|
|
|
142
|
+
tree_name = entry[:tree]
|
|
143
|
+
task = entry[:task]
|
|
144
|
+
marker = (current && current[:tree] == tree_name) ? "*" : " "
|
|
145
|
+
branch = worktree_branch(tree_name)
|
|
146
|
+
branch_str = branch ? " [#{branch}]" : ""
|
|
147
|
+
|
|
148
|
+
if i == 0
|
|
149
|
+
# Parent task line
|
|
150
|
+
display_name = parent
|
|
151
|
+
puts format("%s %s%s", marker, display_name, branch_str)
|
|
152
|
+
else
|
|
153
|
+
# Subtask line: align /child_name under the parent name
|
|
154
|
+
child_name = task.include?('/') ? task.split('/', 2).last : task
|
|
155
|
+
padding = " " * parent.length
|
|
156
|
+
puts format("%s %s/%s%s", marker, padding, child_name, branch_str)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
129
160
|
|
|
130
|
-
available.
|
|
131
|
-
puts
|
|
161
|
+
if available.any?
|
|
162
|
+
puts
|
|
163
|
+
available.each do |tree_name|
|
|
164
|
+
branch = worktree_branch(tree_name)
|
|
165
|
+
branch_str = branch ? " [#{branch}]" : ""
|
|
166
|
+
puts format(" %-20s (available)%s", tree_name, branch_str)
|
|
167
|
+
end
|
|
132
168
|
end
|
|
133
169
|
end
|
|
134
170
|
|
|
@@ -395,6 +431,8 @@ module Task
|
|
|
395
431
|
list_subtasks
|
|
396
432
|
in ['new', subtask_name]
|
|
397
433
|
new_subtask(subtask_name)
|
|
434
|
+
in ['switch']
|
|
435
|
+
switch_subtask
|
|
398
436
|
in ['switch', subtask_name]
|
|
399
437
|
switch_subtask(subtask_name)
|
|
400
438
|
in ['app', app_name]
|
|
@@ -437,22 +475,27 @@ module Task
|
|
|
437
475
|
end
|
|
438
476
|
subtasks = subtasks_for_task(parent_task)
|
|
439
477
|
|
|
440
|
-
if subtasks.empty?
|
|
441
|
-
puts "No subtasks for task '#{parent_task}'."
|
|
442
|
-
puts
|
|
443
|
-
puts "Create one with 'h subtask new SUBTASK_NAME'"
|
|
444
|
-
return
|
|
445
|
-
end
|
|
446
|
-
|
|
447
478
|
puts "Subtasks for '#{parent_task}':"
|
|
448
479
|
puts
|
|
480
|
+
|
|
481
|
+
# Always show the parent (main) task first
|
|
482
|
+
parent_tree = tree_for_task(parent_task)
|
|
483
|
+
current_tree = current[:tree]
|
|
484
|
+
parent_marker = (parent_tree && parent_tree == current_tree) ? "*" : " "
|
|
485
|
+
parent_branch = worktree_branch(parent_tree) if parent_tree
|
|
486
|
+
branch_info = parent_branch ? " branch: #{parent_branch}" : ""
|
|
487
|
+
puts format("%s %-25s tree: %-15s%s", parent_marker, "(main)", parent_tree || '(none)', branch_info)
|
|
488
|
+
|
|
449
489
|
subtasks.each do |subtask|
|
|
450
|
-
marker = subtask['
|
|
451
|
-
|
|
490
|
+
marker = (subtask['worktree'] == current_tree) ? "*" : " "
|
|
491
|
+
st_branch = worktree_branch(subtask['worktree']) if subtask['worktree']
|
|
492
|
+
st_branch_info = st_branch ? " branch: #{st_branch}" : ""
|
|
493
|
+
puts format("%s %-25s tree: %-15s created: %s%s",
|
|
452
494
|
marker,
|
|
453
495
|
subtask['name'],
|
|
454
496
|
subtask['worktree'] || '(none)',
|
|
455
|
-
subtask['created_at']&.split('T')&.first || '?'
|
|
497
|
+
subtask['created_at']&.split('T')&.first || '?',
|
|
498
|
+
st_branch_info
|
|
456
499
|
)
|
|
457
500
|
end
|
|
458
501
|
end
|
|
@@ -519,7 +562,7 @@ module Task
|
|
|
519
562
|
true
|
|
520
563
|
end
|
|
521
564
|
|
|
522
|
-
def switch_subtask(subtask_name)
|
|
565
|
+
def switch_subtask(subtask_name = nil)
|
|
523
566
|
current = current_task
|
|
524
567
|
unless current
|
|
525
568
|
puts "ERROR: Not currently in a task session"
|
|
@@ -533,6 +576,41 @@ module Task
|
|
|
533
576
|
parent_task = parent_task.split('/').first
|
|
534
577
|
end
|
|
535
578
|
|
|
579
|
+
# If no subtask name, use interactive selection
|
|
580
|
+
if subtask_name.nil? || subtask_name.empty?
|
|
581
|
+
subtask_name = select_subtask_interactive(parent_task)
|
|
582
|
+
return false unless subtask_name
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
# Switch to parent task if "main" or "parent"
|
|
586
|
+
if subtask_name == 'main' || subtask_name == 'parent'
|
|
587
|
+
session_name = session_name_for(parent_task)
|
|
588
|
+
tree_name = tree_for_task(parent_task)
|
|
589
|
+
|
|
590
|
+
unless tree_name
|
|
591
|
+
puts "ERROR: Parent task '#{parent_task}' has no worktree"
|
|
592
|
+
return false
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
session_exists = system('tmux', 'has-session', '-t', session_name, err: File::NULL)
|
|
596
|
+
|
|
597
|
+
if session_exists
|
|
598
|
+
hiiro.start_tmux_session(session_name)
|
|
599
|
+
else
|
|
600
|
+
path = tree_path(tree_name)
|
|
601
|
+
if Dir.exist?(path)
|
|
602
|
+
Dir.chdir(path)
|
|
603
|
+
hiiro.start_tmux_session(session_name)
|
|
604
|
+
else
|
|
605
|
+
puts "ERROR: Worktree path '#{path}' does not exist"
|
|
606
|
+
return false
|
|
607
|
+
end
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
puts "Switched to parent task '#{parent_task}'"
|
|
611
|
+
return true
|
|
612
|
+
end
|
|
613
|
+
|
|
536
614
|
subtask = find_subtask(parent_task, subtask_name)
|
|
537
615
|
unless subtask
|
|
538
616
|
puts "Subtask '#{subtask_name}' not found for task '#{parent_task}'"
|
|
@@ -565,6 +643,36 @@ module Task
|
|
|
565
643
|
true
|
|
566
644
|
end
|
|
567
645
|
|
|
646
|
+
# Interactive subtask selection using sk
|
|
647
|
+
def select_subtask_interactive(parent_task)
|
|
648
|
+
subtasks = subtasks_for_task(parent_task)
|
|
649
|
+
parent_tree = tree_for_task(parent_task)
|
|
650
|
+
|
|
651
|
+
# Build selection lines: include parent (main) and all subtasks
|
|
652
|
+
lines = []
|
|
653
|
+
lines << "(main)" if parent_tree
|
|
654
|
+
subtasks.each do |subtask|
|
|
655
|
+
next unless subtask['active']
|
|
656
|
+
lines << subtask['name']
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
if lines.empty?
|
|
660
|
+
puts "No subtasks to switch to."
|
|
661
|
+
return nil
|
|
662
|
+
end
|
|
663
|
+
|
|
664
|
+
require 'open3'
|
|
665
|
+
selected, status = Open3.capture2('sk', stdin_data: lines.join("\n"))
|
|
666
|
+
|
|
667
|
+
if status.success? && !selected.strip.empty?
|
|
668
|
+
choice = selected.strip
|
|
669
|
+
return 'main' if choice == '(main)'
|
|
670
|
+
return choice
|
|
671
|
+
end
|
|
672
|
+
|
|
673
|
+
nil
|
|
674
|
+
end
|
|
675
|
+
|
|
568
676
|
# Show status for the current subtask
|
|
569
677
|
def subtask_status
|
|
570
678
|
current = current_task
|
|
@@ -774,11 +882,11 @@ module Task
|
|
|
774
882
|
worktree_info.keys.sort
|
|
775
883
|
end
|
|
776
884
|
|
|
777
|
-
# Parse git worktree list output into { name => path } hash
|
|
778
|
-
def
|
|
779
|
-
@
|
|
885
|
+
# Parse git worktree list output into { name => { path:, branch: } } hash
|
|
886
|
+
def worktree_details
|
|
887
|
+
@worktree_details ||= begin
|
|
780
888
|
output = `git -C #{main_repo_path} worktree list --porcelain 2>/dev/null`
|
|
781
|
-
|
|
889
|
+
details = {}
|
|
782
890
|
current_path = nil
|
|
783
891
|
|
|
784
892
|
work_dir = File.join(Dir.home, 'work')
|
|
@@ -792,6 +900,12 @@ module Task
|
|
|
792
900
|
# Skip bare repo
|
|
793
901
|
current_path = nil
|
|
794
902
|
elsif line.start_with?('branch ') || line == 'detached'
|
|
903
|
+
branch = if line.start_with?('branch ')
|
|
904
|
+
line.sub('branch refs/heads/', '')
|
|
905
|
+
else
|
|
906
|
+
'(detached)'
|
|
907
|
+
end
|
|
908
|
+
|
|
795
909
|
# Capture worktree (both named branches and detached HEAD)
|
|
796
910
|
if current_path && current_path != main_repo_path
|
|
797
911
|
# Use relative path from ~/work/ to support nested worktrees
|
|
@@ -801,17 +915,28 @@ module Task
|
|
|
801
915
|
else
|
|
802
916
|
File.basename(current_path)
|
|
803
917
|
end
|
|
804
|
-
|
|
918
|
+
details[name] = { path: current_path, branch: branch }
|
|
805
919
|
end
|
|
806
920
|
current_path = nil
|
|
807
921
|
end
|
|
808
922
|
end
|
|
809
923
|
|
|
810
|
-
|
|
924
|
+
details
|
|
811
925
|
end
|
|
812
926
|
end
|
|
813
927
|
|
|
928
|
+
# Backward-compatible { name => path } hash
|
|
929
|
+
def worktree_info
|
|
930
|
+
@worktree_info ||= worktree_details.transform_values { |v| v[:path] }
|
|
931
|
+
end
|
|
932
|
+
|
|
933
|
+
# Get branch name for a worktree
|
|
934
|
+
def worktree_branch(tree_name)
|
|
935
|
+
worktree_details.dig(tree_name, :branch)
|
|
936
|
+
end
|
|
937
|
+
|
|
814
938
|
def clear_worktree_cache
|
|
939
|
+
@worktree_details = nil
|
|
815
940
|
@worktree_info = nil
|
|
816
941
|
end
|
|
817
942
|
|