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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 73b9be67839d84fe5bf4166509623e58aee1067d2176f2f984b71b3bd3d0832b
4
- data.tar.gz: 4fd46fe1da0f037a14bf35262fdaa36e7ed7b1271a044072db9a2b76b4d2244a
3
+ metadata.gz: 98bcbf770e1e25eeb2190918e4bf20e80d84732a77758eac57bcebf247db6a2c
4
+ data.tar.gz: 8e804419accbb33e83d241bb200e045889c872d92a99f465683e8d2e8c0868df
5
5
  SHA512:
6
- metadata.gz: 6b7dbaa0d37759fa8a12aa2b8d47c9bffc7fcb441f44970cd622ddc751144b20144b5ddd25f9dc6b0496df0cee1d5c70d3c2b24115948a9e1b16a26f5395616b
7
- data.tar.gz: cf1827be5abebc173856ef9a098db8ca2cca0cc2c3cbe46da03633f42e31c473a20f81c11096a62c267fa7ead37c56bb1f482e619b6edd861beedfb7a4babfa9
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
- # hiiro.add_default { tasks.subtask_help }
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 name
98
+ # Rename the available worktree to the task's main subtree
95
99
  old_path = tree_path(available_tree)
96
- new_path = File.join(File.dirname(old_path), task_name)
100
+ new_path = File.join(Dir.home, 'work', subtree_name)
97
101
 
98
- if available_tree != task_name
99
- puts "Renaming worktree '#{available_tree}' to '#{task_name}'..."
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 = task_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', task_name)
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 = task_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
- name = File.basename(current_path)
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) | Hiiro plugin management |
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
- | [h-video](h-video.md) | FFmpeg wrapper for video operations |
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 ping` | Simple test command (returns "pong") |
29
- | `h pin` | Key-value storage (via Pins plugin) |
30
- | `h project` | Project navigation (via Project plugin) |
31
- | `h task` | Task management (via Task plugin) |
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
@@ -176,7 +176,6 @@ class Hiiro
176
176
  source: 'manual',
177
177
  cmd: hiiro.full_command,
178
178
  )
179
- puts "Added history entry: #{description}"
180
179
  true
181
180
  end
182
181
 
data/lib/hiiro/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  class Hiiro
2
- VERSION = "0.1.26"
2
+ VERSION = "0.1.28"
3
3
  end
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 name
166
+ # Rename the available worktree to the task's main subtree
163
167
  old_path = tree_path(available_tree)
164
- new_path = File.join(File.dirname(old_path), task_name)
168
+ new_path = File.join(Dir.home, 'work', subtree_name)
165
169
 
166
- if available_tree != task_name
167
- puts "Renaming worktree '#{available_tree}' to '#{task_name}'..."
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 = task_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', task_name)
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 = task_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 List subtasks for current task"
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
- full_subtask_name = "#{parent_task}/#{subtask_name}"
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
- name = File.basename(current_path)
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 ~/bin/ bin/ | egrep '\bbin(: |\/)h(\b|-)' | egrep -v 'identical'
4
+ diff -qrs bin/ ~/bin/ | egrep '\bbin(: |\/)h(\b|-)' | egrep -v 'identical'
5
5
 
6
- diff -qrs ~/.config/hiiro/plugins/ plugins/ | egrep -v 'identical'
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
@@ -8,7 +8,7 @@ set -e
8
8
  REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)"
9
9
  cd "$REPO_DIR"
10
10
 
11
- OTHER_DIR="$REPO_DIR/../home/"
11
+ OTHER_DIR="$HOME"
12
12
 
13
13
  echo "Comparing directories with $OTHER_DIR"
14
14
  echo "Repository: $REPO_DIR ($0)"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hiiro
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.26
4
+ version: 0.1.28
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Toyota