hiiro 0.1.29 → 0.1.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/bin/h-subtask +1 -1
- data/bin/h-task +71 -22
- data/lib/hiiro/version.rb +1 -1
- data/plugins/task.rb +147 -22
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f9acf06495590129ec1870e030280f89addc30177e768c360aa8e6891d69ece7
|
|
4
|
+
data.tar.gz: afdc44652ae0ea1efbc107f8bd4b972c88477f07cf247665a66104859f49a0c2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 74d7b72bcb20c9cfdde0ef7c63d02d270f6f95fd05510fb5ff25f0543c4830f15418718f693fce532c8388f9c9a80851a9d13969646d75b187119f08d3d9cd64
|
|
7
|
+
data.tar.gz: 5f00c18b9bf9c47f8063a37dd6984bcbf97d7bdc4d06f06d918e1aa93aa48c316a14fd76301c56e029eafab2acacbc163f3e50c040b6d85f64217611554a56ae
|
data/bin/h-subtask
CHANGED
|
@@ -16,7 +16,7 @@ hiiro.add_subcmd(:edit) { system(ENV['EDITOR'] || 'nvim', __FILE__) }
|
|
|
16
16
|
hiiro.add_subcmd(:list) { tasks.list_subtasks }
|
|
17
17
|
hiiro.add_subcmd(:ls) { tasks.list_subtasks }
|
|
18
18
|
hiiro.add_subcmd(:new) { |subtask_name| tasks.new_subtask(subtask_name) }
|
|
19
|
-
hiiro.add_subcmd(:switch) { |subtask_name| tasks.switch_subtask(subtask_name) }
|
|
19
|
+
hiiro.add_subcmd(:switch) { |subtask_name=nil| tasks.switch_subtask(subtask_name) }
|
|
20
20
|
hiiro.add_subcmd(:app) { |app_name| tasks.subtask_open_app(app_name) }
|
|
21
21
|
hiiro.add_subcmd(:path) { |app_name=nil| tasks.subtask_app_path(app_name) }
|
|
22
22
|
hiiro.add_subcmd(:cd) { |*args| tasks.subtask_cd_app(*args) }
|
data/bin/h-task
CHANGED
|
@@ -44,23 +44,55 @@ class TaskManager
|
|
|
44
44
|
active, available = trees.partition { |tree_name| task_for_tree(tree_name) }
|
|
45
45
|
sessions = tmux_sessions
|
|
46
46
|
|
|
47
|
+
# Group active trees by parent task
|
|
48
|
+
groups = {}
|
|
47
49
|
active.each do |tree_name|
|
|
48
50
|
task = task_for_tree(tree_name)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
51
|
+
parent = task.include?('/') ? task.split('/').first : task
|
|
52
|
+
groups[parent] ||= []
|
|
53
|
+
groups[parent] << { tree: tree_name, task: task }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
first_group = true
|
|
57
|
+
groups.each do |parent, entries|
|
|
58
|
+
puts unless first_group
|
|
59
|
+
first_group = false
|
|
60
|
+
|
|
61
|
+
# Sort so the main entry (parent itself or parent/main) comes first
|
|
62
|
+
entries.sort_by! { |e| e[:task] == parent || e[:task].end_with?('/main') ? 0 : 1 }
|
|
63
|
+
|
|
64
|
+
entries.each_with_index do |entry, i|
|
|
65
|
+
tree_name = entry[:tree]
|
|
66
|
+
task = entry[:task]
|
|
67
|
+
marker = (current && current[:tree] == tree_name) ? "*" : " "
|
|
68
|
+
branch = worktree_branch(tree_name)
|
|
69
|
+
branch_str = branch ? " [#{branch}]" : ""
|
|
70
|
+
|
|
71
|
+
# Check if there's a tmux session for this task
|
|
72
|
+
session_name = session_name_for(task)
|
|
73
|
+
has_session = sessions.include?(session_name)
|
|
74
|
+
session_marker = has_session ? "+" : " "
|
|
75
|
+
|
|
76
|
+
if i == 0
|
|
77
|
+
# Parent task line
|
|
78
|
+
display_name = parent
|
|
79
|
+
puts format("%s%s %s%s", marker, session_marker, display_name, branch_str)
|
|
80
|
+
else
|
|
81
|
+
# Subtask line: align /child_name under the parent name
|
|
82
|
+
child_name = task.include?('/') ? task.split('/', 2).last : task
|
|
83
|
+
padding = " " * parent.length
|
|
84
|
+
puts format("%s%s %s/%s%s", marker, session_marker, padding, child_name, branch_str)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
58
87
|
end
|
|
59
88
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
89
|
+
if available.any?
|
|
90
|
+
puts
|
|
91
|
+
available.each do |tree_name|
|
|
92
|
+
branch = worktree_branch(tree_name)
|
|
93
|
+
branch_str = branch ? " [#{branch}]" : ""
|
|
94
|
+
puts format(" %-20s (available)%s", tree_name, branch_str)
|
|
95
|
+
end
|
|
64
96
|
end
|
|
65
97
|
|
|
66
98
|
# List tmux sessions without associated tasks
|
|
@@ -415,11 +447,11 @@ class TaskManager
|
|
|
415
447
|
worktree_info.keys.sort
|
|
416
448
|
end
|
|
417
449
|
|
|
418
|
-
# Parse git worktree list output into { name => path } hash
|
|
419
|
-
def
|
|
420
|
-
@
|
|
450
|
+
# Parse git worktree list output into { name => { path:, branch: } } hash
|
|
451
|
+
def worktree_details
|
|
452
|
+
@worktree_details ||= begin
|
|
421
453
|
output = `git -C #{main_repo_path} worktree list --porcelain 2>/dev/null`
|
|
422
|
-
|
|
454
|
+
details = {}
|
|
423
455
|
current_path = nil
|
|
424
456
|
|
|
425
457
|
work_dir = File.join(Dir.home, 'work')
|
|
@@ -433,6 +465,12 @@ class TaskManager
|
|
|
433
465
|
# Skip bare repo
|
|
434
466
|
current_path = nil
|
|
435
467
|
elsif line.start_with?('branch ') || line == 'detached'
|
|
468
|
+
branch = if line.start_with?('branch ')
|
|
469
|
+
line.sub('branch refs/heads/', '')
|
|
470
|
+
else
|
|
471
|
+
'(detached)'
|
|
472
|
+
end
|
|
473
|
+
|
|
436
474
|
# Capture worktree (both named branches and detached HEAD)
|
|
437
475
|
if current_path && current_path != main_repo_path
|
|
438
476
|
# Use relative path from ~/work/ to support nested worktrees
|
|
@@ -442,17 +480,28 @@ class TaskManager
|
|
|
442
480
|
else
|
|
443
481
|
File.basename(current_path)
|
|
444
482
|
end
|
|
445
|
-
|
|
483
|
+
details[name] = { path: current_path, branch: branch }
|
|
446
484
|
end
|
|
447
485
|
current_path = nil
|
|
448
486
|
end
|
|
449
487
|
end
|
|
450
488
|
|
|
451
|
-
|
|
489
|
+
details
|
|
452
490
|
end
|
|
453
491
|
end
|
|
454
492
|
|
|
493
|
+
# Backward-compatible { name => path } hash
|
|
494
|
+
def worktree_info
|
|
495
|
+
@worktree_info ||= worktree_details.transform_values { |v| v[:path] }
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
# Get branch name for a worktree
|
|
499
|
+
def worktree_branch(tree_name)
|
|
500
|
+
worktree_details.dig(tree_name, :branch)
|
|
501
|
+
end
|
|
502
|
+
|
|
455
503
|
def clear_worktree_cache
|
|
504
|
+
@worktree_details = nil
|
|
456
505
|
@worktree_info = nil
|
|
457
506
|
end
|
|
458
507
|
|
|
@@ -701,8 +750,8 @@ class TaskManager
|
|
|
701
750
|
|
|
702
751
|
# Migrate flat worktrees (~/work/task) to nested (~/work/task/main)
|
|
703
752
|
def migrate_worktrees
|
|
704
|
-
# Find flat worktrees that need migration (not already nested
|
|
705
|
-
flat_trees = trees.reject { |name| name.include?('/')
|
|
753
|
+
# Find flat worktrees that need migration (not already nested)
|
|
754
|
+
flat_trees = trees.reject { |name| name.include?('/') }
|
|
706
755
|
|
|
707
756
|
if flat_trees.empty?
|
|
708
757
|
puts "No flat worktrees to migrate."
|
|
@@ -751,7 +800,7 @@ class TaskManager
|
|
|
751
800
|
data = load_assignments
|
|
752
801
|
data.delete(name)
|
|
753
802
|
data[new_tree_name] = task
|
|
754
|
-
save_assignments(
|
|
803
|
+
save_assignments(data)
|
|
755
804
|
end
|
|
756
805
|
|
|
757
806
|
# Step 4: Update task metadata
|
data/lib/hiiro/version.rb
CHANGED
data/plugins/task.rb
CHANGED
|
@@ -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
|
|
|
@@ -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
|
|