hiiro 0.1.26 → 0.1.28
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 +72 -108
- data/bin/h-subtask +11 -1
- data/bin/h-task +121 -8
- data/docs/README.md +38 -6
- data/lib/hiiro/history.rb +0 -1
- data/lib/hiiro/version.rb +1 -1
- data/plugins/task.rb +190 -11
- data/script/compare +2 -2
- data/script/sync +1 -1
- 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: 98bcbf770e1e25eeb2190918e4bf20e80d84732a77758eac57bcebf247db6a2c
|
|
4
|
+
data.tar.gz: 8e804419accbb33e83d241bb200e045889c872d92a99f465683e8d2e8c0868df
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: afbf8cb894b5815b2c3a8620fff092dcb91d4bf02b773e3350aa913ffc36878e5a28688f6059686525887ea616b211995d0f52c5cc7835573ee1fc202802da0b
|
|
7
|
+
data.tar.gz: 15b90ba14b1b0fd4485bd0bce87c5218c2ea0d7ce94beccccabd6587b56d9d22721add771ba66458bc09f444aa2767ecac94e8b3e2658413893b54e53a3e1b59
|
data/README.md
CHANGED
|
@@ -69,6 +69,78 @@ h ping
|
|
|
69
69
|
# => pong
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
+
## Subcommands
|
|
73
|
+
|
|
74
|
+
### Base Commands
|
|
75
|
+
|
|
76
|
+
| Command | Description |
|
|
77
|
+
|---------|-------------|
|
|
78
|
+
| `h version` | Display the Hiiro version |
|
|
79
|
+
| `h ping` | Simple test command (returns "pong") |
|
|
80
|
+
| `h setup` | Install plugins and subcommands to system paths |
|
|
81
|
+
| `h edit` | Open the h script in your editor |
|
|
82
|
+
| `h path` | Print the current directory |
|
|
83
|
+
| `h ppath` | Print project path (git root + relative dir) |
|
|
84
|
+
| `h rpath` | Print relative path from git root |
|
|
85
|
+
| `h pin` | Per-command key-value storage (via Pins plugin) |
|
|
86
|
+
| `h project` | Project navigation with tmux integration (via Project plugin) |
|
|
87
|
+
| `h task` | Task management across git worktrees (via Task plugin) |
|
|
88
|
+
| `h notify` | macOS desktop notifications via terminal-notifier (via Notify plugin) |
|
|
89
|
+
|
|
90
|
+
### External Subcommands
|
|
91
|
+
|
|
92
|
+
| Command | Description |
|
|
93
|
+
|---------|-------------|
|
|
94
|
+
| `h branch` | Record and manage git branch history for tasks |
|
|
95
|
+
| `h buffer` | Tmux paste buffer management |
|
|
96
|
+
| `h dot` | Compare directories and generate symlink/diff commands |
|
|
97
|
+
| `h dotfiles` | Manage dotfiles in ~/proj/home |
|
|
98
|
+
| `h home` | Manage home directory files with edit and search |
|
|
99
|
+
| `h html` | Generate an HTML index of MP4 videos in current directory |
|
|
100
|
+
| `h link` | Manage saved links with URL, description, and shorthand |
|
|
101
|
+
| `h mic` | Control macOS microphone input volume |
|
|
102
|
+
| `h note` | Create, edit, list, and display notes |
|
|
103
|
+
| `h pane` | Tmux pane management |
|
|
104
|
+
| `h plugin` | Manage hiiro plugins (list, edit, search) |
|
|
105
|
+
| `h pr` | Record PR information linked to tasks |
|
|
106
|
+
| `h pr-monitor` | Monitor pull requests |
|
|
107
|
+
| `h pr-watch` | Watch pull requests for updates |
|
|
108
|
+
| `h project` | Open projects with tmux session management |
|
|
109
|
+
| `h runtask` | Run templated task scripts |
|
|
110
|
+
| `h serve` | Start a miniserve HTTP server on port 1111 |
|
|
111
|
+
| `h session` | Tmux session management |
|
|
112
|
+
| `h sha` | Extract short SHA from git log |
|
|
113
|
+
| `h subtask` | Shorthand for task subtask management |
|
|
114
|
+
| `h task` | Comprehensive task manager for git worktrees |
|
|
115
|
+
| `h video` | Video inspection and operations via ffprobe/ffmpeg |
|
|
116
|
+
| `h vim` | Manage nvim configuration with edit and search |
|
|
117
|
+
| `h window` | Tmux window management |
|
|
118
|
+
| `h wtree` | Git worktree management |
|
|
119
|
+
|
|
120
|
+
## Abbreviations
|
|
121
|
+
|
|
122
|
+
Any subcommand can be abbreviated as long as the prefix uniquely matches:
|
|
123
|
+
|
|
124
|
+
```sh
|
|
125
|
+
h ex hel # matches h example hello
|
|
126
|
+
h te # matches h test (if unique)
|
|
127
|
+
h pp # matches h ppath
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
If multiple commands match, the first match wins and a warning is logged (when logging is enabled).
|
|
131
|
+
|
|
132
|
+
## Plugins
|
|
133
|
+
|
|
134
|
+
Plugins are Ruby modules loaded from `~/.config/hiiro/plugins/`:
|
|
135
|
+
|
|
136
|
+
| Plugin | Description |
|
|
137
|
+
|--------|-------------|
|
|
138
|
+
| Pins | Per-command YAML key-value storage |
|
|
139
|
+
| Project | Project directory navigation with tmux session management |
|
|
140
|
+
| Task | Task lifecycle management across git worktrees with subtask support |
|
|
141
|
+
| Tmux | Tmux session helpers used by Project and Task |
|
|
142
|
+
| Notify | macOS desktop notifications via terminal-notifier |
|
|
143
|
+
|
|
72
144
|
## Adding Subcommands
|
|
73
145
|
|
|
74
146
|
### Method 1: External Executables
|
|
@@ -127,114 +199,6 @@ hiiro.add_subcommand(:pwd) do |*args, **values|
|
|
|
127
199
|
end
|
|
128
200
|
```
|
|
129
201
|
|
|
130
|
-
## Abbreviations
|
|
131
|
-
|
|
132
|
-
Any subcommand can be abbreviated as long as the prefix uniquely matches:
|
|
133
|
-
|
|
134
|
-
```sh
|
|
135
|
-
h ex hel # matches h example hello
|
|
136
|
-
h te # matches h test (if unique)
|
|
137
|
-
h pp # matches h ppath
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
If multiple commands match, the first match wins and a warning is logged (when logging is enabled).
|
|
141
|
-
|
|
142
|
-
## Built-in Plugins
|
|
143
|
-
|
|
144
|
-
### Pins
|
|
145
|
-
|
|
146
|
-
Key-value storage persisted per command:
|
|
147
|
-
|
|
148
|
-
```sh
|
|
149
|
-
h pin # List all pins
|
|
150
|
-
h pin mykey # Get value
|
|
151
|
-
h pin mykey myvalue # Set value
|
|
152
|
-
h pin set mykey myvalue # Set value (explicit)
|
|
153
|
-
h pin rm mykey # Remove pin
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
Pins are stored in `~/.config/hiiro/pins/<command>.yml`.
|
|
157
|
-
|
|
158
|
-
### Tmux Subcommands
|
|
159
|
-
|
|
160
|
-
Hiiro includes several tmux wrapper commands for managing sessions, windows, panes, and buffers. See [docs/](docs/) for full documentation.
|
|
161
|
-
|
|
162
|
-
```sh
|
|
163
|
-
h session ls # List tmux sessions
|
|
164
|
-
h window new # Create new window
|
|
165
|
-
h pane split # Split current pane
|
|
166
|
-
h buffer ls # List paste buffers
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
### Project
|
|
170
|
-
|
|
171
|
-
Quick project navigation with tmux integration:
|
|
172
|
-
|
|
173
|
-
```sh
|
|
174
|
-
h project myproj # cd to ~/proj/myproj and start tmux session
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
Projects can be configured in `~/.config/hiiro/projects.yml`:
|
|
178
|
-
|
|
179
|
-
```yaml
|
|
180
|
-
myproject: /path/to/project
|
|
181
|
-
work: ~/work/main
|
|
182
|
-
```
|
|
183
|
-
|
|
184
|
-
### Task
|
|
185
|
-
|
|
186
|
-
Manage development tasks across git worktrees in `~/work/`:
|
|
187
|
-
|
|
188
|
-
```sh
|
|
189
|
-
h task list # Show trees and their active tasks
|
|
190
|
-
h task start TICKET-123 # Start working on a task
|
|
191
|
-
h task status # Show current task info
|
|
192
|
-
h task app frontend # Open app directory in new tmux window
|
|
193
|
-
h task save # Save current tmux window state
|
|
194
|
-
h task stop # Release tree for other tasks
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
Configure apps in `~/.config/hiiro/apps.yml`:
|
|
198
|
-
|
|
199
|
-
```yaml
|
|
200
|
-
frontend: apps/frontend
|
|
201
|
-
api: services/api
|
|
202
|
-
admin: admin_portal/admin
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
### Notify (macOS)
|
|
206
|
-
|
|
207
|
-
Desktop notifications via `terminal-notifier`:
|
|
208
|
-
|
|
209
|
-
```sh
|
|
210
|
-
h notify "Build complete"
|
|
211
|
-
h notify "Click me" "https://example.com"
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
### Video
|
|
215
|
-
|
|
216
|
-
FFmpeg wrapper for common video operations:
|
|
217
|
-
|
|
218
|
-
```sh
|
|
219
|
-
h video info movie.mp4 # Human-readable video summary
|
|
220
|
-
h video resize720 movie.mp4 # Resize to 720p
|
|
221
|
-
h video clip movie.mp4 00:01:30 60 # Extract 60s clip starting at 1:30
|
|
222
|
-
h video gif movie.mp4 # Create animated GIF
|
|
223
|
-
h video help # Full command list
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
See [docs/h-video.md](docs/h-video.md) for complete documentation.
|
|
227
|
-
|
|
228
|
-
### Plugin
|
|
229
|
-
|
|
230
|
-
Manage hiiro plugins:
|
|
231
|
-
|
|
232
|
-
```sh
|
|
233
|
-
h plugin ls # List installed plugins
|
|
234
|
-
h plugin edit pins # Edit a plugin file
|
|
235
|
-
h plugin rg "some_method" # Search plugin code
|
|
236
|
-
```
|
|
237
|
-
|
|
238
202
|
## Writing Plugins
|
|
239
203
|
|
|
240
204
|
Plugins are Ruby modules that extend Hiiro instances:
|
data/bin/h-subtask
CHANGED
|
@@ -13,10 +13,20 @@ hiiro = Hiiro.init(*ARGV, plugins: [Tmux, Task])
|
|
|
13
13
|
tasks = hiiro.task_manager
|
|
14
14
|
|
|
15
15
|
hiiro.add_subcmd(:edit) { system(ENV['EDITOR'] || 'nvim', __FILE__) }
|
|
16
|
+
hiiro.add_subcmd(:list) { tasks.list_subtasks }
|
|
16
17
|
hiiro.add_subcmd(:ls) { tasks.list_subtasks }
|
|
17
18
|
hiiro.add_subcmd(:new) { |subtask_name| tasks.new_subtask(subtask_name) }
|
|
18
19
|
hiiro.add_subcmd(:switch) { |subtask_name| tasks.switch_subtask(subtask_name) }
|
|
20
|
+
hiiro.add_subcmd(:app) { |app_name| tasks.subtask_open_app(app_name) }
|
|
21
|
+
hiiro.add_subcmd(:path) { |app_name=nil| tasks.subtask_app_path(app_name) }
|
|
22
|
+
hiiro.add_subcmd(:cd) { |*args| tasks.subtask_cd_app(*args) }
|
|
23
|
+
hiiro.add_subcmd(:apps) { tasks.list_configured_apps }
|
|
24
|
+
hiiro.add_subcmd(:status) { tasks.subtask_status }
|
|
25
|
+
hiiro.add_subcmd(:st) { tasks.subtask_status }
|
|
26
|
+
hiiro.add_subcmd(:save) { tasks.save_subtask }
|
|
27
|
+
hiiro.add_subcmd(:stop) { |subtask_name=nil| subtask_name ? tasks.stop_subtask(subtask_name) : tasks.stop_subtask }
|
|
28
|
+
hiiro.add_subcmd(:current) { print tasks.current_subtask_name }
|
|
19
29
|
|
|
20
|
-
|
|
30
|
+
hiiro.add_default { tasks.subtask_help }
|
|
21
31
|
|
|
22
32
|
hiiro.run
|
data/bin/h-task
CHANGED
|
@@ -90,13 +90,18 @@ class TaskManager
|
|
|
90
90
|
# Find an available worktree to reuse, or create a new one
|
|
91
91
|
available_tree = find_available_tree
|
|
92
92
|
|
|
93
|
+
# New tasks get a nested structure: task_name/main
|
|
94
|
+
# so subtasks can live alongside as task_name/subtask_name
|
|
95
|
+
subtree_name = "#{task_name}/main"
|
|
96
|
+
|
|
93
97
|
if available_tree
|
|
94
|
-
# Rename the available worktree to the task
|
|
98
|
+
# Rename the available worktree to the task's main subtree
|
|
95
99
|
old_path = tree_path(available_tree)
|
|
96
|
-
new_path = File.join(
|
|
100
|
+
new_path = File.join(Dir.home, 'work', subtree_name)
|
|
97
101
|
|
|
98
|
-
if available_tree !=
|
|
99
|
-
puts "Renaming worktree '#{available_tree}' to '#{
|
|
102
|
+
if available_tree != subtree_name
|
|
103
|
+
puts "Renaming worktree '#{available_tree}' to '#{subtree_name}'..."
|
|
104
|
+
FileUtils.mkdir_p(File.dirname(new_path))
|
|
100
105
|
result = system('git', '-C', main_repo_path, 'worktree', 'move', old_path, new_path)
|
|
101
106
|
unless result
|
|
102
107
|
puts "ERROR: Failed to rename worktree"
|
|
@@ -105,14 +110,15 @@ class TaskManager
|
|
|
105
110
|
clear_worktree_cache
|
|
106
111
|
end
|
|
107
112
|
|
|
108
|
-
final_tree_name =
|
|
113
|
+
final_tree_name = subtree_name
|
|
109
114
|
final_tree_path = new_path
|
|
110
115
|
else
|
|
111
116
|
# No available worktree, create a new one
|
|
112
117
|
puts "Creating new worktree for '#{task_name}'..."
|
|
113
|
-
new_path = File.join(Dir.home, 'work',
|
|
118
|
+
new_path = File.join(Dir.home, 'work', subtree_name)
|
|
114
119
|
|
|
115
120
|
# Create worktree from main branch (detached to avoid branch conflicts)
|
|
121
|
+
FileUtils.mkdir_p(File.dirname(new_path))
|
|
116
122
|
result = system('git', '-C', main_repo_path, 'worktree', 'add', '--detach', new_path)
|
|
117
123
|
unless result
|
|
118
124
|
puts "ERROR: Failed to create worktree"
|
|
@@ -120,7 +126,7 @@ class TaskManager
|
|
|
120
126
|
end
|
|
121
127
|
clear_worktree_cache
|
|
122
128
|
|
|
123
|
-
final_tree_name =
|
|
129
|
+
final_tree_name = subtree_name
|
|
124
130
|
final_tree_path = new_path
|
|
125
131
|
end
|
|
126
132
|
|
|
@@ -421,6 +427,9 @@ class TaskManager
|
|
|
421
427
|
info = {}
|
|
422
428
|
current_path = nil
|
|
423
429
|
|
|
430
|
+
work_dir = File.join(Dir.home, 'work')
|
|
431
|
+
work_prefix = work_dir + '/'
|
|
432
|
+
|
|
424
433
|
output.lines.each do |line|
|
|
425
434
|
line = line.strip
|
|
426
435
|
if line.start_with?('worktree ')
|
|
@@ -431,7 +440,13 @@ class TaskManager
|
|
|
431
440
|
elsif line.start_with?('branch ') || line == 'detached'
|
|
432
441
|
# Capture worktree (both named branches and detached HEAD)
|
|
433
442
|
if current_path && current_path != main_repo_path
|
|
434
|
-
|
|
443
|
+
# Use relative path from ~/work/ to support nested worktrees
|
|
444
|
+
# e.g. ~/work/my-task/main -> "my-task/main"
|
|
445
|
+
name = if current_path.start_with?(work_prefix)
|
|
446
|
+
current_path.sub(work_prefix, '')
|
|
447
|
+
else
|
|
448
|
+
File.basename(current_path)
|
|
449
|
+
end
|
|
435
450
|
info[name] = current_path
|
|
436
451
|
end
|
|
437
452
|
current_path = nil
|
|
@@ -693,6 +708,103 @@ class TaskManager
|
|
|
693
708
|
return [] unless $?.success?
|
|
694
709
|
output.lines.map(&:strip)
|
|
695
710
|
end
|
|
711
|
+
|
|
712
|
+
# Migrate flat worktrees (~/work/task) to nested (~/work/task/main)
|
|
713
|
+
def migrate_worktrees
|
|
714
|
+
# Find flat worktrees that need migration (not already nested, not reserved)
|
|
715
|
+
flat_trees = trees.reject { |name| name.include?('/') || RESERVED_WORKTREES.key?(name) }
|
|
716
|
+
|
|
717
|
+
if flat_trees.empty?
|
|
718
|
+
puts "No flat worktrees to migrate."
|
|
719
|
+
return
|
|
720
|
+
end
|
|
721
|
+
|
|
722
|
+
sessions = tmux_sessions
|
|
723
|
+
|
|
724
|
+
puts "Worktrees to migrate:"
|
|
725
|
+
puts
|
|
726
|
+
flat_trees.each do |name|
|
|
727
|
+
task = task_for_tree(name)
|
|
728
|
+
session_marker = sessions.include?(session_name_for(task || name)) ? " (tmux session running)" : ""
|
|
729
|
+
puts format(" %-20s => %s/main%s", name, name, session_marker)
|
|
730
|
+
end
|
|
731
|
+
puts
|
|
732
|
+
|
|
733
|
+
flat_trees.each do |name|
|
|
734
|
+
task = task_for_tree(name)
|
|
735
|
+
old_path = tree_path(name)
|
|
736
|
+
temp_name = "__migrating_#{name}"
|
|
737
|
+
temp_path = File.join(Dir.home, 'work', temp_name)
|
|
738
|
+
new_tree_name = "#{name}/main"
|
|
739
|
+
new_path = File.join(Dir.home, 'work', new_tree_name)
|
|
740
|
+
|
|
741
|
+
puts "Migrating '#{name}' -> '#{new_tree_name}'..."
|
|
742
|
+
|
|
743
|
+
# Step 1: Move worktree to temp location (frees up the directory name)
|
|
744
|
+
result = system('git', '-C', main_repo_path, 'worktree', 'move', old_path, temp_path)
|
|
745
|
+
unless result
|
|
746
|
+
puts " ERROR: Failed to move to temp location, skipping"
|
|
747
|
+
next
|
|
748
|
+
end
|
|
749
|
+
|
|
750
|
+
# Step 2: Create parent directory and move to final nested location
|
|
751
|
+
FileUtils.mkdir_p(File.join(Dir.home, 'work', name))
|
|
752
|
+
result = system('git', '-C', main_repo_path, 'worktree', 'move', temp_path, new_path)
|
|
753
|
+
unless result
|
|
754
|
+
puts " ERROR: Failed to move to final location, rolling back..."
|
|
755
|
+
system('git', '-C', main_repo_path, 'worktree', 'move', temp_path, old_path)
|
|
756
|
+
next
|
|
757
|
+
end
|
|
758
|
+
|
|
759
|
+
# Step 3: Update assignments (tree name changes, task name stays)
|
|
760
|
+
if task
|
|
761
|
+
data = load_assignments
|
|
762
|
+
data.delete(name)
|
|
763
|
+
data[new_tree_name] = task
|
|
764
|
+
save_assignments(RESERVED_WORKTREES.merge(data))
|
|
765
|
+
end
|
|
766
|
+
|
|
767
|
+
# Step 4: Update task metadata
|
|
768
|
+
if task
|
|
769
|
+
meta = task_metadata(task)
|
|
770
|
+
if meta
|
|
771
|
+
meta['tree'] = new_tree_name
|
|
772
|
+
FileUtils.mkdir_p(task_dir)
|
|
773
|
+
File.write(task_metadata_file(task), YAML.dump(meta))
|
|
774
|
+
end
|
|
775
|
+
end
|
|
776
|
+
|
|
777
|
+
# Step 5: Update tmux panes if a session is running
|
|
778
|
+
session_name = session_name_for(task || name)
|
|
779
|
+
if sessions.include?(session_name)
|
|
780
|
+
update_tmux_panes(session_name, old_path, new_path)
|
|
781
|
+
end
|
|
782
|
+
|
|
783
|
+
clear_worktree_cache
|
|
784
|
+
@assignments = nil
|
|
785
|
+
puts " Done."
|
|
786
|
+
end
|
|
787
|
+
|
|
788
|
+
puts
|
|
789
|
+
puts "Migration complete."
|
|
790
|
+
end
|
|
791
|
+
|
|
792
|
+
# Send `cd` to tmux panes whose cwd is under the old path
|
|
793
|
+
def update_tmux_panes(session_name, old_path, new_path)
|
|
794
|
+
output = `tmux list-panes -s -t '#{session_name}' -F '\#{pane_id}:\#{pane_current_path}' 2>/dev/null`
|
|
795
|
+
return unless $?.success?
|
|
796
|
+
|
|
797
|
+
output.lines.each do |line|
|
|
798
|
+
pane_id, pane_path = line.strip.split(':', 2)
|
|
799
|
+
next unless pane_path
|
|
800
|
+
|
|
801
|
+
if pane_path == old_path || pane_path.start_with?(old_path + '/')
|
|
802
|
+
updated_path = pane_path.sub(old_path, new_path)
|
|
803
|
+
system('tmux', 'send-keys', '-t', pane_id, "cd #{updated_path}", 'Enter')
|
|
804
|
+
puts " Updated pane #{pane_id}: cd #{updated_path}"
|
|
805
|
+
end
|
|
806
|
+
end
|
|
807
|
+
end
|
|
696
808
|
end
|
|
697
809
|
|
|
698
810
|
# Create task manager instance
|
|
@@ -713,5 +825,6 @@ hiiro.add_subcmd(:st) { tasks.status }
|
|
|
713
825
|
hiiro.add_subcmd(:save) { tasks.save_current }
|
|
714
826
|
hiiro.add_subcmd(:stop) { |task_name=nil| task_name ? tasks.stop_task(task_name) : tasks.stop_current }
|
|
715
827
|
hiiro.add_subcmd(:current) { print tasks.task_name }
|
|
828
|
+
hiiro.add_subcmd(:migrate) { tasks.migrate_worktrees }
|
|
716
829
|
|
|
717
830
|
hiiro.run
|
data/docs/README.md
CHANGED
|
@@ -9,11 +9,30 @@ This directory contains detailed documentation for all Hiiro subcommands.
|
|
|
9
9
|
| Command | Description |
|
|
10
10
|
|---------|-------------|
|
|
11
11
|
| [h-buffer](h-buffer.md) | Tmux paste buffer management |
|
|
12
|
+
| h-branch | Record and manage git branch history for tasks |
|
|
13
|
+
| h-dot | Compare directories and generate symlink/diff commands |
|
|
14
|
+
| h-dotfiles | Manage dotfiles in ~/proj/home |
|
|
15
|
+
| h-home | Manage home directory files with edit and search |
|
|
16
|
+
| h-html | Generate an HTML index of MP4 videos in current directory |
|
|
17
|
+
| h-link | Manage saved links with URL, description, and shorthand |
|
|
18
|
+
| h-mic | Control macOS microphone input volume |
|
|
19
|
+
| h-note | Create, edit, list, and display notes |
|
|
12
20
|
| [h-pane](h-pane.md) | Tmux pane management |
|
|
13
|
-
| [h-plugin](h-plugin.md) |
|
|
21
|
+
| [h-plugin](h-plugin.md) | Manage hiiro plugins (list, edit, search) |
|
|
22
|
+
| h-pr | Record PR information linked to tasks |
|
|
23
|
+
| h-pr-monitor | Monitor pull requests |
|
|
24
|
+
| h-pr-watch | Watch pull requests for updates |
|
|
25
|
+
| h-project | Open projects with tmux session management |
|
|
26
|
+
| h-runtask | Run templated task scripts |
|
|
27
|
+
| h-serve | Start a miniserve HTTP server on port 1111 |
|
|
14
28
|
| [h-session](h-session.md) | Tmux session management |
|
|
15
|
-
|
|
|
29
|
+
| h-sha | Extract short SHA from git log |
|
|
30
|
+
| h-subtask | Shorthand for task subtask management |
|
|
31
|
+
| h-task | Comprehensive task manager for git worktrees |
|
|
32
|
+
| [h-video](h-video.md) | Video inspection and operations via ffprobe/ffmpeg |
|
|
33
|
+
| h-vim | Manage nvim configuration with edit and search |
|
|
16
34
|
| [h-window](h-window.md) | Tmux window management |
|
|
35
|
+
| h-wtree | Git worktree management |
|
|
17
36
|
|
|
18
37
|
## Base Commands
|
|
19
38
|
|
|
@@ -21,14 +40,27 @@ The main `h` command includes these built-in subcommands:
|
|
|
21
40
|
|
|
22
41
|
| Command | Description |
|
|
23
42
|
|---------|-------------|
|
|
43
|
+
| `h version` | Display the Hiiro version |
|
|
44
|
+
| `h ping` | Simple test command (returns "pong") |
|
|
45
|
+
| `h setup` | Install plugins and subcommands to system paths |
|
|
24
46
|
| `h edit` | Open the h script in your editor |
|
|
25
47
|
| `h path` | Print the current directory |
|
|
26
48
|
| `h ppath` | Print project path (git root + relative dir) |
|
|
27
49
|
| `h rpath` | Print relative path from git root |
|
|
28
|
-
| `h
|
|
29
|
-
| `h
|
|
30
|
-
| `h
|
|
31
|
-
| `h
|
|
50
|
+
| `h pin` | Per-command key-value storage (via Pins plugin) |
|
|
51
|
+
| `h project` | Project navigation with tmux integration (via Project plugin) |
|
|
52
|
+
| `h task` | Task management across git worktrees (via Task plugin) |
|
|
53
|
+
| `h notify` | macOS desktop notifications via terminal-notifier (via Notify plugin) |
|
|
54
|
+
|
|
55
|
+
## Plugins
|
|
56
|
+
|
|
57
|
+
| Plugin | Description |
|
|
58
|
+
|--------|-------------|
|
|
59
|
+
| Pins | Per-command YAML key-value storage |
|
|
60
|
+
| Project | Project directory navigation with tmux session management |
|
|
61
|
+
| Task | Task lifecycle management across git worktrees with subtask support |
|
|
62
|
+
| Tmux | Tmux session helpers used by Project and Task |
|
|
63
|
+
| Notify | macOS desktop notifications via terminal-notifier |
|
|
32
64
|
|
|
33
65
|
## Abbreviations
|
|
34
66
|
|
data/lib/hiiro/history.rb
CHANGED
data/lib/hiiro/version.rb
CHANGED
data/plugins/task.rb
CHANGED
|
@@ -158,13 +158,18 @@ module Task
|
|
|
158
158
|
# Find an available worktree to reuse, or create a new one
|
|
159
159
|
available_tree = tree || find_available_tree
|
|
160
160
|
|
|
161
|
+
# New tasks get a nested structure: task_name/main
|
|
162
|
+
# so subtasks can live alongside as task_name/subtask_name
|
|
163
|
+
subtree_name = "#{task_name}/main"
|
|
164
|
+
|
|
161
165
|
if available_tree
|
|
162
|
-
# Rename the available worktree to the task
|
|
166
|
+
# Rename the available worktree to the task's main subtree
|
|
163
167
|
old_path = tree_path(available_tree)
|
|
164
|
-
new_path = File.join(
|
|
168
|
+
new_path = File.join(Dir.home, 'work', subtree_name)
|
|
165
169
|
|
|
166
|
-
if available_tree !=
|
|
167
|
-
puts "Renaming worktree '#{available_tree}' to '#{
|
|
170
|
+
if available_tree != subtree_name
|
|
171
|
+
puts "Renaming worktree '#{available_tree}' to '#{subtree_name}'..."
|
|
172
|
+
FileUtils.mkdir_p(File.dirname(new_path))
|
|
168
173
|
result = system('git', '-C', main_repo_path, 'worktree', 'move', old_path, new_path)
|
|
169
174
|
unless result
|
|
170
175
|
puts "ERROR: Failed to rename worktree"
|
|
@@ -173,14 +178,15 @@ module Task
|
|
|
173
178
|
clear_worktree_cache
|
|
174
179
|
end
|
|
175
180
|
|
|
176
|
-
final_tree_name =
|
|
181
|
+
final_tree_name = subtree_name
|
|
177
182
|
final_tree_path = new_path
|
|
178
183
|
else
|
|
179
184
|
# No available worktree, create a new one
|
|
180
185
|
puts "Creating new worktree for '#{task_name}'..."
|
|
181
|
-
new_path = File.join(Dir.home, 'work',
|
|
186
|
+
new_path = File.join(Dir.home, 'work', subtree_name)
|
|
182
187
|
|
|
183
188
|
# Create worktree from main branch (detached to avoid branch conflicts)
|
|
189
|
+
FileUtils.mkdir_p(File.dirname(new_path))
|
|
184
190
|
result = system('git', '-C', main_repo_path, 'worktree', 'add', '--detach', new_path)
|
|
185
191
|
unless result
|
|
186
192
|
puts "ERROR: Failed to create worktree"
|
|
@@ -188,7 +194,7 @@ module Task
|
|
|
188
194
|
end
|
|
189
195
|
clear_worktree_cache
|
|
190
196
|
|
|
191
|
-
final_tree_name =
|
|
197
|
+
final_tree_name = subtree_name
|
|
192
198
|
final_tree_path = new_path
|
|
193
199
|
end
|
|
194
200
|
|
|
@@ -377,21 +383,49 @@ module Task
|
|
|
377
383
|
puts " h subtask <subcommand> [args]"
|
|
378
384
|
puts
|
|
379
385
|
puts "Subcommands:"
|
|
380
|
-
puts " ls
|
|
386
|
+
puts " list, ls List subtasks for current task"
|
|
381
387
|
puts " new SUBTASK_NAME Start a new subtask (creates worktree and session)"
|
|
382
388
|
puts " switch SUBTASK_NAME Switch to subtask's tmux session"
|
|
389
|
+
puts " app APP_NAME Open a tmux window for an app in current subtask"
|
|
390
|
+
puts " apps List configured apps from apps.yml"
|
|
391
|
+
puts " cd [APP_NAME] Change directory to app in current subtask"
|
|
392
|
+
puts " path [APP_NAME] Print app path in current subtask"
|
|
393
|
+
puts " status, st Show current subtask status"
|
|
394
|
+
puts " save Save current subtask session info"
|
|
395
|
+
puts " stop [SUBTASK_NAME] Stop working on current/named subtask"
|
|
396
|
+
puts " current Print current subtask name"
|
|
383
397
|
end
|
|
384
398
|
|
|
385
399
|
def handle_subtask(*args)
|
|
386
400
|
case args
|
|
387
401
|
in []
|
|
388
402
|
subtask_help
|
|
389
|
-
in ['ls']
|
|
403
|
+
in ['list'] | ['ls']
|
|
390
404
|
list_subtasks
|
|
391
405
|
in ['new', subtask_name]
|
|
392
406
|
new_subtask(subtask_name)
|
|
393
407
|
in ['switch', subtask_name]
|
|
394
408
|
switch_subtask(subtask_name)
|
|
409
|
+
in ['app', app_name]
|
|
410
|
+
subtask_open_app(app_name)
|
|
411
|
+
in ['apps']
|
|
412
|
+
list_configured_apps
|
|
413
|
+
in ['cd', *cd_args]
|
|
414
|
+
subtask_cd_app(*cd_args)
|
|
415
|
+
in ['path']
|
|
416
|
+
subtask_app_path
|
|
417
|
+
in ['path', app_name]
|
|
418
|
+
subtask_app_path(app_name)
|
|
419
|
+
in ['status'] | ['st']
|
|
420
|
+
subtask_status
|
|
421
|
+
in ['save']
|
|
422
|
+
save_subtask
|
|
423
|
+
in ['stop']
|
|
424
|
+
stop_subtask
|
|
425
|
+
in ['stop', subtask_name]
|
|
426
|
+
stop_subtask(subtask_name)
|
|
427
|
+
in ['current']
|
|
428
|
+
print current_subtask_name
|
|
395
429
|
else
|
|
396
430
|
puts "Unknown subtask command: #{args.inspect}"
|
|
397
431
|
subtask_help
|
|
@@ -437,7 +471,15 @@ module Task
|
|
|
437
471
|
end
|
|
438
472
|
|
|
439
473
|
parent_task = current[:task]
|
|
440
|
-
|
|
474
|
+
|
|
475
|
+
# Avoid nesting worktrees: if parent_task is itself a worktree,
|
|
476
|
+
# use parent_task_subtasks/ as the directory instead of parent_task/
|
|
477
|
+
parent_dir = if worktree_info.key?(parent_task)
|
|
478
|
+
"#{parent_task}_subtasks"
|
|
479
|
+
else
|
|
480
|
+
parent_task
|
|
481
|
+
end
|
|
482
|
+
full_subtask_name = "#{parent_dir}/#{subtask_name}"
|
|
441
483
|
|
|
442
484
|
# Check if subtask already exists
|
|
443
485
|
existing = subtasks_for_task(parent_task).find { |s| s['name'] == subtask_name }
|
|
@@ -525,6 +567,122 @@ module Task
|
|
|
525
567
|
true
|
|
526
568
|
end
|
|
527
569
|
|
|
570
|
+
# Show status for the current subtask
|
|
571
|
+
def subtask_status
|
|
572
|
+
current = current_task
|
|
573
|
+
unless current
|
|
574
|
+
puts "Not currently in a task session"
|
|
575
|
+
return
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
parent_task = parent_task_name(current[:task])
|
|
579
|
+
subtask_name = current_subtask_name_from(current[:task])
|
|
580
|
+
|
|
581
|
+
puts "Parent task: #{parent_task}"
|
|
582
|
+
if subtask_name
|
|
583
|
+
puts "Subtask: #{subtask_name}"
|
|
584
|
+
else
|
|
585
|
+
puts "Subtask: (main)"
|
|
586
|
+
end
|
|
587
|
+
puts "Worktree: #{current[:tree]}"
|
|
588
|
+
puts "Path: #{tree_path(current[:tree])}"
|
|
589
|
+
puts "Session: #{current[:session]}"
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
# Save current subtask tmux session state
|
|
593
|
+
def save_subtask
|
|
594
|
+
current = current_task
|
|
595
|
+
unless current
|
|
596
|
+
puts "ERROR: Not currently in a task session"
|
|
597
|
+
return false
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
parent_task = parent_task_name(current[:task])
|
|
601
|
+
session = current[:session]
|
|
602
|
+
|
|
603
|
+
windows = capture_tmux_windows(session)
|
|
604
|
+
|
|
605
|
+
save_task_metadata(parent_task,
|
|
606
|
+
tree: current[:tree],
|
|
607
|
+
session: session,
|
|
608
|
+
windows: windows,
|
|
609
|
+
saved_at: Time.now.iso8601
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
puts "Saved subtask session state (#{windows.count} windows)"
|
|
613
|
+
true
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
# Stop a subtask (remove from parent's subtask list, unassign tree)
|
|
617
|
+
def stop_subtask(subtask_name = nil)
|
|
618
|
+
current = current_task
|
|
619
|
+
unless current
|
|
620
|
+
puts "Not currently in a task session"
|
|
621
|
+
return false
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
parent_task = parent_task_name(current[:task])
|
|
625
|
+
|
|
626
|
+
if subtask_name.nil?
|
|
627
|
+
# Stop the current subtask
|
|
628
|
+
subtask_name = current_subtask_name_from(current[:task])
|
|
629
|
+
unless subtask_name
|
|
630
|
+
puts "Not currently in a subtask (in main task)"
|
|
631
|
+
return false
|
|
632
|
+
end
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
subtask = find_subtask(parent_task, subtask_name)
|
|
636
|
+
unless subtask
|
|
637
|
+
puts "Subtask '#{subtask_name}' not found for task '#{parent_task}'"
|
|
638
|
+
return false
|
|
639
|
+
end
|
|
640
|
+
|
|
641
|
+
tree_name = subtask['worktree']
|
|
642
|
+
if tree_name
|
|
643
|
+
unassign_task_from_tree(tree_name)
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
# Mark subtask as inactive in parent metadata
|
|
647
|
+
meta = task_metadata(parent_task) || {}
|
|
648
|
+
if meta['subtasks']
|
|
649
|
+
meta['subtasks'].each do |s|
|
|
650
|
+
if s['name'].start_with?(subtask_name)
|
|
651
|
+
s['active'] = false
|
|
652
|
+
end
|
|
653
|
+
end
|
|
654
|
+
FileUtils.mkdir_p(task_dir)
|
|
655
|
+
File.write(task_metadata_file(parent_task), YAML.dump(meta))
|
|
656
|
+
end
|
|
657
|
+
|
|
658
|
+
puts "Stopped subtask '#{subtask_name}' (worktree available for reuse)"
|
|
659
|
+
true
|
|
660
|
+
end
|
|
661
|
+
|
|
662
|
+
# Print the current subtask name
|
|
663
|
+
def current_subtask_name
|
|
664
|
+
current = current_task
|
|
665
|
+
return nil unless current
|
|
666
|
+
|
|
667
|
+
subtask_name = current_subtask_name_from(current[:task])
|
|
668
|
+
subtask_name || current[:task]
|
|
669
|
+
end
|
|
670
|
+
|
|
671
|
+
# Open an app window scoped to the current subtask's tree
|
|
672
|
+
def subtask_open_app(app_name)
|
|
673
|
+
open_app(app_name)
|
|
674
|
+
end
|
|
675
|
+
|
|
676
|
+
# cd to app in current subtask's tree
|
|
677
|
+
def subtask_cd_app(*args)
|
|
678
|
+
cd_app(*args)
|
|
679
|
+
end
|
|
680
|
+
|
|
681
|
+
# Print app path in current subtask's tree
|
|
682
|
+
def subtask_app_path(app_name = nil, task: nil)
|
|
683
|
+
app_path(app_name, task: task || current_task&.dig(:task))
|
|
684
|
+
end
|
|
685
|
+
|
|
528
686
|
private
|
|
529
687
|
|
|
530
688
|
def subtasks_for_task(task_name)
|
|
@@ -546,6 +704,18 @@ module Task
|
|
|
546
704
|
File.write(task_metadata_file(parent_task), YAML.dump(meta))
|
|
547
705
|
end
|
|
548
706
|
|
|
707
|
+
# Extract parent task name from a potentially nested task/subtask name
|
|
708
|
+
def parent_task_name(task_name)
|
|
709
|
+
task_name.include?('/') ? task_name.split('/').first : task_name
|
|
710
|
+
end
|
|
711
|
+
|
|
712
|
+
# Extract subtask name from a nested task name (returns nil if main)
|
|
713
|
+
def current_subtask_name_from(task_name)
|
|
714
|
+
return nil unless task_name.include?('/')
|
|
715
|
+
parts = task_name.split('/', 2)
|
|
716
|
+
parts.last == 'main' ? nil : parts.last
|
|
717
|
+
end
|
|
718
|
+
|
|
549
719
|
public
|
|
550
720
|
|
|
551
721
|
def list_configured_apps
|
|
@@ -613,6 +783,9 @@ module Task
|
|
|
613
783
|
info = {}
|
|
614
784
|
current_path = nil
|
|
615
785
|
|
|
786
|
+
work_dir = File.join(Dir.home, 'work')
|
|
787
|
+
work_prefix = work_dir + '/'
|
|
788
|
+
|
|
616
789
|
output.lines.each do |line|
|
|
617
790
|
line = line.strip
|
|
618
791
|
if line.start_with?('worktree ')
|
|
@@ -623,7 +796,13 @@ module Task
|
|
|
623
796
|
elsif line.start_with?('branch ') || line == 'detached'
|
|
624
797
|
# Capture worktree (both named branches and detached HEAD)
|
|
625
798
|
if current_path && current_path != main_repo_path
|
|
626
|
-
|
|
799
|
+
# Use relative path from ~/work/ to support nested worktrees
|
|
800
|
+
# e.g. ~/work/my-task/main -> "my-task/main"
|
|
801
|
+
name = if current_path.start_with?(work_prefix)
|
|
802
|
+
current_path.sub(work_prefix, '')
|
|
803
|
+
else
|
|
804
|
+
File.basename(current_path)
|
|
805
|
+
end
|
|
627
806
|
info[name] = current_path
|
|
628
807
|
end
|
|
629
808
|
current_path = nil
|
data/script/compare
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
|
|
3
3
|
diff_dirs() {
|
|
4
|
-
diff -qrs
|
|
4
|
+
diff -qrs bin/ ~/bin/ | egrep '\bbin(: |\/)h(\b|-)' | egrep -v 'identical'
|
|
5
5
|
|
|
6
|
-
diff -qrs ~/.config/hiiro/plugins/
|
|
6
|
+
diff -qrs plugins/ ~/.config/hiiro/plugins/ | egrep -v 'identical'
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
# s@Only in \(bin|plugins\): \(.*\)@cp -v \1/\2 $HOME/\1/@;
|
data/script/sync
CHANGED