hiiro 0.1.9 → 0.1.11
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-branch +213 -0
- data/bin/h-link +190 -0
- data/bin/h-pr +257 -0
- data/bin/h-session +1 -21
- data/bin/h-subtask +21 -0
- data/bin/h-wtree +16 -0
- data/lib/hiiro/version.rb +1 -1
- data/lib/hiiro.rb +17 -0
- data/plugins/task.rb +187 -0
- metadata +7 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c231fe73a649821cd62ad3bfafaf3a1176891fb6b37fa8a95d7e3a50499553ab
|
|
4
|
+
data.tar.gz: cc0759a2304557485168249934b108199dce624e972bd5dc4a8663cec283a60c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: db1cb32fc6c26d31268a845569400fb833509a5289c30b481fcc6a1e6ad0857a98eb837cbb1f1e9c957f45c8132703553d5ea593f60d93ac8bb88a53256205bb
|
|
7
|
+
data.tar.gz: 32a64ec1dc0acdbd91d2e823c20b65ebb56749b8988531e76efa7d5cd90de0e9ca918f5d1c68377b8632780f6beacf086472e13394c2b106a7f08d1a60080367
|
data/bin/h-branch
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require "hiiro"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "yaml"
|
|
6
|
+
|
|
7
|
+
Hiiro.load_env
|
|
8
|
+
hiiro = Hiiro.init(*ARGV, plugins: [Task])
|
|
9
|
+
|
|
10
|
+
class BranchManager
|
|
11
|
+
attr_reader :hiiro
|
|
12
|
+
|
|
13
|
+
def initialize(hiiro)
|
|
14
|
+
@hiiro = hiiro
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def help
|
|
18
|
+
puts "Usage: h branch <subcommand> [args]"
|
|
19
|
+
puts
|
|
20
|
+
puts "Subcommands:"
|
|
21
|
+
puts " save Record current branch for this task"
|
|
22
|
+
puts " history List branch history (oldest to newest)"
|
|
23
|
+
puts " history --worktree=X Filter by worktree"
|
|
24
|
+
puts " history --task=X Filter by task"
|
|
25
|
+
puts " history --session=X Filter by tmux session"
|
|
26
|
+
puts " current Show current branch info"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def save
|
|
30
|
+
branch_name = current_branch
|
|
31
|
+
unless branch_name
|
|
32
|
+
puts "ERROR: Not in a git repository"
|
|
33
|
+
return false
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
entry = build_entry(branch_name)
|
|
37
|
+
unless entry[:task]
|
|
38
|
+
puts "WARNING: Not in a task session, saving without task info"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
data = load_data
|
|
42
|
+
data['branches'] ||= []
|
|
43
|
+
|
|
44
|
+
# Check if this branch is already recorded for this task
|
|
45
|
+
existing = data['branches'].find do |b|
|
|
46
|
+
b['name'] == branch_name && b['task'] == entry[:task]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
if existing
|
|
50
|
+
# Update existing entry
|
|
51
|
+
existing.merge!(entry.transform_keys(&:to_s))
|
|
52
|
+
existing['updated_at'] = Time.now.iso8601
|
|
53
|
+
puts "Updated branch '#{branch_name}' for task '#{entry[:task]}'"
|
|
54
|
+
else
|
|
55
|
+
# Add new entry
|
|
56
|
+
data['branches'] << entry.transform_keys(&:to_s).merge('created_at' => Time.now.iso8601)
|
|
57
|
+
puts "Saved branch '#{branch_name}' for task '#{entry[:task]}'"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
save_data(data)
|
|
61
|
+
show_entry(entry)
|
|
62
|
+
true
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def history(args = [])
|
|
66
|
+
filters = parse_filters(args)
|
|
67
|
+
data = load_data
|
|
68
|
+
branches = data['branches'] || []
|
|
69
|
+
|
|
70
|
+
if branches.empty?
|
|
71
|
+
puts "No branches recorded."
|
|
72
|
+
puts "Use 'h branch save' to record the current branch."
|
|
73
|
+
return
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Apply filters
|
|
77
|
+
branches = filter_entries(branches, filters)
|
|
78
|
+
|
|
79
|
+
if branches.empty?
|
|
80
|
+
puts "No branches match the given filters."
|
|
81
|
+
return
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Sort by created_at (oldest first)
|
|
85
|
+
branches = branches.sort_by { |b| b['created_at'] || '' }
|
|
86
|
+
|
|
87
|
+
puts "Branch history (oldest to newest):"
|
|
88
|
+
puts
|
|
89
|
+
branches.each_with_index do |branch, idx|
|
|
90
|
+
puts format_entry(branch, idx + 1)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def current
|
|
95
|
+
branch_name = current_branch
|
|
96
|
+
unless branch_name
|
|
97
|
+
puts "ERROR: Not in a git repository"
|
|
98
|
+
return false
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
entry = build_entry(branch_name)
|
|
102
|
+
puts "Current branch info:"
|
|
103
|
+
puts
|
|
104
|
+
show_entry(entry)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
|
|
109
|
+
def current_branch
|
|
110
|
+
branch = `git rev-parse --abbrev-ref HEAD 2>/dev/null`.strip
|
|
111
|
+
branch.empty? ? nil : branch
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def build_entry(branch_name)
|
|
115
|
+
task_info = hiiro.task_manager.send(:current_task)
|
|
116
|
+
tmux_info = capture_tmux_info
|
|
117
|
+
|
|
118
|
+
{
|
|
119
|
+
name: branch_name,
|
|
120
|
+
worktree: task_info&.[](:tree),
|
|
121
|
+
task: task_info&.[](:task),
|
|
122
|
+
tmux: tmux_info
|
|
123
|
+
}
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def capture_tmux_info
|
|
127
|
+
return nil unless ENV['TMUX']
|
|
128
|
+
|
|
129
|
+
{
|
|
130
|
+
'session' => `tmux display-message -p '#S'`.strip,
|
|
131
|
+
'window' => `tmux display-message -p '#W'`.strip,
|
|
132
|
+
'pane' => ENV['TMUX_PANE']
|
|
133
|
+
}
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def parse_filters(args)
|
|
137
|
+
filters = {}
|
|
138
|
+
args.each do |arg|
|
|
139
|
+
case arg
|
|
140
|
+
when /^--worktree=(.+)$/
|
|
141
|
+
filters[:worktree] = $1
|
|
142
|
+
when /^--task=(.+)$/
|
|
143
|
+
filters[:task] = $1
|
|
144
|
+
when /^--session=(.+)$/
|
|
145
|
+
filters[:session] = $1
|
|
146
|
+
when /^-w(.+)$/
|
|
147
|
+
filters[:worktree] = $1
|
|
148
|
+
when /^-t(.+)$/
|
|
149
|
+
filters[:task] = $1
|
|
150
|
+
when /^-s(.+)$/
|
|
151
|
+
filters[:session] = $1
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
filters
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def filter_entries(entries, filters)
|
|
158
|
+
entries.select do |entry|
|
|
159
|
+
next false if filters[:worktree] && !entry['worktree']&.include?(filters[:worktree])
|
|
160
|
+
next false if filters[:task] && !entry['task']&.include?(filters[:task])
|
|
161
|
+
next false if filters[:session] && entry.dig('tmux', 'session') != filters[:session]
|
|
162
|
+
true
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def format_entry(entry, num)
|
|
167
|
+
lines = []
|
|
168
|
+
lines << "#{num}. #{entry['name']}"
|
|
169
|
+
lines << " Task: #{entry['task'] || '(none)'}"
|
|
170
|
+
lines << " Worktree: #{entry['worktree'] || '(none)'}"
|
|
171
|
+
if entry['tmux']
|
|
172
|
+
lines << " Tmux: #{entry['tmux']['session']}/#{entry['tmux']['window']}"
|
|
173
|
+
end
|
|
174
|
+
lines << " Created: #{entry['created_at']}"
|
|
175
|
+
lines.join("\n")
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def show_entry(entry)
|
|
179
|
+
puts " Branch: #{entry[:name]}"
|
|
180
|
+
puts " Task: #{entry[:task] || '(none)'}"
|
|
181
|
+
puts " Worktree: #{entry[:worktree] || '(none)'}"
|
|
182
|
+
if entry[:tmux]
|
|
183
|
+
puts " Tmux session: #{entry[:tmux]['session']}"
|
|
184
|
+
puts " Tmux window: #{entry[:tmux]['window']}"
|
|
185
|
+
puts " Tmux pane: #{entry[:tmux]['pane']}"
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def data_file
|
|
190
|
+
File.join(Dir.home, '.config', 'hiiro', 'branches.yml')
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def load_data
|
|
194
|
+
return {} unless File.exist?(data_file)
|
|
195
|
+
YAML.safe_load_file(data_file) || {}
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def save_data(data)
|
|
199
|
+
FileUtils.mkdir_p(File.dirname(data_file))
|
|
200
|
+
File.write(data_file, YAML.dump(data))
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
manager = BranchManager.new(hiiro)
|
|
205
|
+
|
|
206
|
+
hiiro.add_subcmd(:edit) { system(ENV['EDITOR'] || 'nvim', __FILE__) }
|
|
207
|
+
hiiro.add_subcmd(:save) { manager.save }
|
|
208
|
+
hiiro.add_subcmd(:history) { |*args| manager.history(args) }
|
|
209
|
+
hiiro.add_subcmd(:current) { manager.current }
|
|
210
|
+
|
|
211
|
+
hiiro.add_default { manager.help }
|
|
212
|
+
|
|
213
|
+
hiiro.run
|
data/bin/h-link
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require 'time'
|
|
6
|
+
|
|
7
|
+
require "hiiro"
|
|
8
|
+
|
|
9
|
+
LINKS_FILE = File.join(Dir.home, '.config/hiiro/links.yml')
|
|
10
|
+
|
|
11
|
+
o = Hiiro.init(*ARGV, plugins: [Tmux, Pins], links_file: LINKS_FILE)
|
|
12
|
+
|
|
13
|
+
def ensure_links_file
|
|
14
|
+
dir = File.dirname(LINKS_FILE)
|
|
15
|
+
FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
|
|
16
|
+
File.write(LINKS_FILE, [].to_yaml) unless File.exist?(LINKS_FILE)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def load_links
|
|
20
|
+
ensure_links_file
|
|
21
|
+
YAML.load_file(LINKS_FILE) || []
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def save_links(links)
|
|
25
|
+
ensure_links_file
|
|
26
|
+
File.write(LINKS_FILE, links.to_yaml)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def find_link_by_ref(ref, links)
|
|
30
|
+
if ref =~ /^\d+$/
|
|
31
|
+
idx = ref.to_i - 1
|
|
32
|
+
return [idx, links[idx]] if idx >= 0 && idx < links.length
|
|
33
|
+
else
|
|
34
|
+
links.each_with_index do |link, idx|
|
|
35
|
+
return [idx, link] if link['shorthand'] == ref
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
[nil, nil]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
o.add_subcmd(:save) do |*args|
|
|
42
|
+
if args.empty?
|
|
43
|
+
puts "Usage: h link save <url> [description...]"
|
|
44
|
+
exit 1
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
url = args.shift
|
|
48
|
+
description = args.join(' ')
|
|
49
|
+
|
|
50
|
+
links = load_links
|
|
51
|
+
links << {
|
|
52
|
+
'url' => url,
|
|
53
|
+
'description' => description,
|
|
54
|
+
'shorthand' => nil,
|
|
55
|
+
'created_at' => Time.now.iso8601
|
|
56
|
+
}
|
|
57
|
+
save_links(links)
|
|
58
|
+
|
|
59
|
+
puts "Saved link ##{links.length}: #{url}"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
o.add_subcmd(:ls) do |*args|
|
|
63
|
+
links = load_links
|
|
64
|
+
if links.empty?
|
|
65
|
+
puts "No links saved."
|
|
66
|
+
else
|
|
67
|
+
links.each_with_index do |link, idx|
|
|
68
|
+
num = (idx + 1).to_s.rjust(3)
|
|
69
|
+
shorthand = link['shorthand'] ? " [#{link['shorthand']}]" : ""
|
|
70
|
+
desc = link['description'].to_s.empty? ? "" : " - #{link['description']}"
|
|
71
|
+
puts "#{num}.#{shorthand} #{link['url']}#{desc}"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
o.add_subcmd(:list) do |*args|
|
|
77
|
+
o.run_subcmd(:ls, *args)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
o.add_subcmd(:search) do |*args|
|
|
81
|
+
if args.empty?
|
|
82
|
+
puts "Usage: h link search <term> [term...]"
|
|
83
|
+
exit 1
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
links = load_links
|
|
87
|
+
terms = args.map(&:downcase)
|
|
88
|
+
|
|
89
|
+
matches = links.each_with_index.select do |link, idx|
|
|
90
|
+
searchable = [
|
|
91
|
+
link['url'],
|
|
92
|
+
link['description'],
|
|
93
|
+
link['shorthand']
|
|
94
|
+
].compact.join(' ').downcase
|
|
95
|
+
|
|
96
|
+
terms.any? { |term| searchable.include?(term) }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
if matches.empty?
|
|
100
|
+
puts "No links found matching: #{args.join(' ')}"
|
|
101
|
+
else
|
|
102
|
+
matches.each do |link, idx|
|
|
103
|
+
num = (idx + 1).to_s.rjust(3)
|
|
104
|
+
shorthand = link['shorthand'] ? " [#{link['shorthand']}]" : ""
|
|
105
|
+
desc = link['description'].to_s.empty? ? "" : " - #{link['description']}"
|
|
106
|
+
puts "#{num}.#{shorthand} #{link['url']}#{desc}"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
o.add_subcmd(:select) do |*args|
|
|
112
|
+
links = load_links
|
|
113
|
+
if links.empty?
|
|
114
|
+
STDERR.puts "No links saved."
|
|
115
|
+
exit 1
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
lines = links.each_with_index.map do |link, idx|
|
|
119
|
+
num = (idx + 1).to_s.rjust(3)
|
|
120
|
+
desc = link['description'].to_s.empty? ? "" : " - #{link['description']}"
|
|
121
|
+
"#{num}. #{link['url']}#{desc}"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
require 'open3'
|
|
125
|
+
selected, status = Open3.capture2('sk', stdin_data: lines.join("\n"))
|
|
126
|
+
|
|
127
|
+
if status.success? && !selected.strip.empty?
|
|
128
|
+
if selected =~ /^\s*(\d+)\.\s+(\S+)/
|
|
129
|
+
print $2
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
o.add_subcmd(:edit) do |*args|
|
|
135
|
+
if args.empty?
|
|
136
|
+
puts "Usage: h link edit <number|shorthand>"
|
|
137
|
+
exit 1
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
links = load_links
|
|
141
|
+
idx, link = find_link_by_ref(args.first, links)
|
|
142
|
+
|
|
143
|
+
if link.nil?
|
|
144
|
+
puts "Link not found: #{args.first}"
|
|
145
|
+
exit 1
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
require 'tempfile'
|
|
149
|
+
tmpfile = Tempfile.new(['link-edit-', '.yml'])
|
|
150
|
+
tmpfile.write(link.to_yaml)
|
|
151
|
+
tmpfile.close
|
|
152
|
+
|
|
153
|
+
system(ENV['EDITOR'] || 'vim', tmpfile.path)
|
|
154
|
+
|
|
155
|
+
updated = YAML.load_file(tmpfile.path)
|
|
156
|
+
tmpfile.unlink
|
|
157
|
+
|
|
158
|
+
links[idx] = updated
|
|
159
|
+
save_links(links)
|
|
160
|
+
|
|
161
|
+
puts "Updated link ##{idx + 1}"
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
o.add_subcmd(:open) do |*args|
|
|
165
|
+
if args.empty?
|
|
166
|
+
puts "Usage: h link open <number|shorthand>"
|
|
167
|
+
exit 1
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
links = load_links
|
|
171
|
+
idx, link = find_link_by_ref(args.first, links)
|
|
172
|
+
|
|
173
|
+
if link.nil?
|
|
174
|
+
puts "Link not found: #{args.first}"
|
|
175
|
+
exit 1
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
system('open', link['url'])
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
o.add_subcmd(:path) do |*args|
|
|
182
|
+
print LINKS_FILE
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
begin
|
|
186
|
+
o.run
|
|
187
|
+
rescue => e
|
|
188
|
+
require 'pry'
|
|
189
|
+
binding.pry
|
|
190
|
+
end
|
data/bin/h-pr
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require "hiiro"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "yaml"
|
|
6
|
+
require "json"
|
|
7
|
+
|
|
8
|
+
Hiiro.load_env
|
|
9
|
+
hiiro = Hiiro.init(*ARGV, plugins: [Task])
|
|
10
|
+
|
|
11
|
+
class PRManager
|
|
12
|
+
attr_reader :hiiro
|
|
13
|
+
|
|
14
|
+
def initialize(hiiro)
|
|
15
|
+
@hiiro = hiiro
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def help
|
|
19
|
+
puts "Usage: h pr <subcommand> [args]"
|
|
20
|
+
puts
|
|
21
|
+
puts "Subcommands:"
|
|
22
|
+
puts " save [PR_NUMBER] Record PR for this task (auto-detects if omitted)"
|
|
23
|
+
puts " history List PR history (oldest to newest)"
|
|
24
|
+
puts " history --worktree=X Filter by worktree"
|
|
25
|
+
puts " history --task=X Filter by task"
|
|
26
|
+
puts " history --session=X Filter by tmux session"
|
|
27
|
+
puts " current Show current branch's PR info"
|
|
28
|
+
puts " open [PR_NUMBER] Open PR in browser"
|
|
29
|
+
puts " view [PR_NUMBER] View PR details in terminal"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def save(pr_number = nil)
|
|
33
|
+
pr_info = fetch_pr_info(pr_number)
|
|
34
|
+
unless pr_info
|
|
35
|
+
puts "ERROR: Could not find PR"
|
|
36
|
+
puts "Make sure you have the gh CLI installed and authenticated."
|
|
37
|
+
return false
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
entry = build_entry(pr_info)
|
|
41
|
+
unless entry[:task]
|
|
42
|
+
puts "WARNING: Not in a task session, saving without task info"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
data = load_data
|
|
46
|
+
data['prs'] ||= []
|
|
47
|
+
|
|
48
|
+
# Check if this PR is already recorded for this task
|
|
49
|
+
existing = data['prs'].find do |p|
|
|
50
|
+
p['number'] == pr_info['number'] && p['task'] == entry[:task]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
if existing
|
|
54
|
+
# Update existing entry
|
|
55
|
+
existing.merge!(entry.transform_keys(&:to_s))
|
|
56
|
+
existing['updated_at'] = Time.now.iso8601
|
|
57
|
+
puts "Updated PR ##{pr_info['number']} for task '#{entry[:task]}'"
|
|
58
|
+
else
|
|
59
|
+
# Add new entry
|
|
60
|
+
data['prs'] << entry.transform_keys(&:to_s).merge('created_at' => Time.now.iso8601)
|
|
61
|
+
puts "Saved PR ##{pr_info['number']} for task '#{entry[:task]}'"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
save_data(data)
|
|
65
|
+
show_entry(entry)
|
|
66
|
+
true
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def history(args = [])
|
|
70
|
+
filters = parse_filters(args)
|
|
71
|
+
data = load_data
|
|
72
|
+
prs = data['prs'] || []
|
|
73
|
+
|
|
74
|
+
if prs.empty?
|
|
75
|
+
puts "No PRs recorded."
|
|
76
|
+
puts "Use 'h pr save' to record the current PR."
|
|
77
|
+
return
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Apply filters
|
|
81
|
+
prs = filter_entries(prs, filters)
|
|
82
|
+
|
|
83
|
+
if prs.empty?
|
|
84
|
+
puts "No PRs match the given filters."
|
|
85
|
+
return
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Sort by created_at (oldest first)
|
|
89
|
+
prs = prs.sort_by { |p| p['created_at'] || '' }
|
|
90
|
+
|
|
91
|
+
puts "PR history (oldest to newest):"
|
|
92
|
+
puts
|
|
93
|
+
prs.each_with_index do |pr, idx|
|
|
94
|
+
puts format_entry(pr, idx + 1)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def current
|
|
99
|
+
pr_info = fetch_pr_info
|
|
100
|
+
unless pr_info
|
|
101
|
+
puts "No PR found for current branch."
|
|
102
|
+
puts "Create one with 'gh pr create' or specify a PR number."
|
|
103
|
+
return false
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
entry = build_entry(pr_info)
|
|
107
|
+
puts "Current PR info:"
|
|
108
|
+
puts
|
|
109
|
+
show_entry(entry)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def open(pr_number = nil)
|
|
113
|
+
if pr_number
|
|
114
|
+
system('gh', 'pr', 'view', pr_number.to_s, '--web')
|
|
115
|
+
else
|
|
116
|
+
system('gh', 'pr', 'view', '--web')
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def view(pr_number = nil)
|
|
121
|
+
if pr_number
|
|
122
|
+
system('gh', 'pr', 'view', pr_number.to_s)
|
|
123
|
+
else
|
|
124
|
+
system('gh', 'pr', 'view')
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
private
|
|
129
|
+
|
|
130
|
+
def fetch_pr_info(pr_number = nil)
|
|
131
|
+
cmd = if pr_number
|
|
132
|
+
['gh', 'pr', 'view', pr_number.to_s, '--json', 'number,title,url,headRefName,state']
|
|
133
|
+
else
|
|
134
|
+
['gh', 'pr', 'view', '--json', 'number,title,url,headRefName,state']
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
output = `#{cmd.join(' ')} 2>/dev/null`
|
|
138
|
+
return nil if output.empty?
|
|
139
|
+
|
|
140
|
+
JSON.parse(output)
|
|
141
|
+
rescue JSON::ParserError
|
|
142
|
+
nil
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def build_entry(pr_info)
|
|
146
|
+
task_info = hiiro.task_manager.send(:current_task)
|
|
147
|
+
tmux_info = capture_tmux_info
|
|
148
|
+
|
|
149
|
+
{
|
|
150
|
+
number: pr_info['number'],
|
|
151
|
+
title: pr_info['title'],
|
|
152
|
+
url: pr_info['url'],
|
|
153
|
+
branch: pr_info['headRefName'],
|
|
154
|
+
state: pr_info['state'],
|
|
155
|
+
worktree: task_info&.[](:tree),
|
|
156
|
+
task: task_info&.[](:task),
|
|
157
|
+
tmux: tmux_info
|
|
158
|
+
}
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def capture_tmux_info
|
|
162
|
+
return nil unless ENV['TMUX']
|
|
163
|
+
|
|
164
|
+
{
|
|
165
|
+
'session' => `tmux display-message -p '#S'`.strip,
|
|
166
|
+
'window' => `tmux display-message -p '#W'`.strip,
|
|
167
|
+
'pane' => ENV['TMUX_PANE']
|
|
168
|
+
}
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def parse_filters(args)
|
|
172
|
+
filters = {}
|
|
173
|
+
args.each do |arg|
|
|
174
|
+
case arg
|
|
175
|
+
when /^--worktree=(.+)$/
|
|
176
|
+
filters[:worktree] = $1
|
|
177
|
+
when /^--task=(.+)$/
|
|
178
|
+
filters[:task] = $1
|
|
179
|
+
when /^--session=(.+)$/
|
|
180
|
+
filters[:session] = $1
|
|
181
|
+
when /^-w(.+)$/
|
|
182
|
+
filters[:worktree] = $1
|
|
183
|
+
when /^-t(.+)$/
|
|
184
|
+
filters[:task] = $1
|
|
185
|
+
when /^-s(.+)$/
|
|
186
|
+
filters[:session] = $1
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
filters
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def filter_entries(entries, filters)
|
|
193
|
+
entries.select do |entry|
|
|
194
|
+
next false if filters[:worktree] && !entry['worktree']&.include?(filters[:worktree])
|
|
195
|
+
next false if filters[:task] && !entry['task']&.include?(filters[:task])
|
|
196
|
+
next false if filters[:session] && entry.dig('tmux', 'session') != filters[:session]
|
|
197
|
+
true
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def format_entry(entry, num)
|
|
202
|
+
lines = []
|
|
203
|
+
lines << "#{num}. PR ##{entry['number']}: #{entry['title']}"
|
|
204
|
+
lines << " Branch: #{entry['branch']}"
|
|
205
|
+
lines << " State: #{entry['state']}"
|
|
206
|
+
lines << " Task: #{entry['task'] || '(none)'}"
|
|
207
|
+
lines << " Worktree: #{entry['worktree'] || '(none)'}"
|
|
208
|
+
if entry['tmux']
|
|
209
|
+
lines << " Tmux: #{entry['tmux']['session']}/#{entry['tmux']['window']}"
|
|
210
|
+
end
|
|
211
|
+
lines << " Created: #{entry['created_at']}"
|
|
212
|
+
lines << " URL: #{entry['url']}"
|
|
213
|
+
lines.join("\n")
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def show_entry(entry)
|
|
217
|
+
puts " PR: ##{entry[:number]}"
|
|
218
|
+
puts " Title: #{entry[:title]}"
|
|
219
|
+
puts " Branch: #{entry[:branch]}"
|
|
220
|
+
puts " State: #{entry[:state]}"
|
|
221
|
+
puts " URL: #{entry[:url]}"
|
|
222
|
+
puts " Task: #{entry[:task] || '(none)'}"
|
|
223
|
+
puts " Worktree: #{entry[:worktree] || '(none)'}"
|
|
224
|
+
if entry[:tmux]
|
|
225
|
+
puts " Tmux session: #{entry[:tmux]['session']}"
|
|
226
|
+
puts " Tmux window: #{entry[:tmux]['window']}"
|
|
227
|
+
puts " Tmux pane: #{entry[:tmux]['pane']}"
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def data_file
|
|
232
|
+
File.join(Dir.home, '.config', 'hiiro', 'prs.yml')
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def load_data
|
|
236
|
+
return {} unless File.exist?(data_file)
|
|
237
|
+
YAML.safe_load_file(data_file) || {}
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def save_data(data)
|
|
241
|
+
FileUtils.mkdir_p(File.dirname(data_file))
|
|
242
|
+
File.write(data_file, YAML.dump(data))
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
manager = PRManager.new(hiiro)
|
|
247
|
+
|
|
248
|
+
hiiro.add_subcmd(:edit) { system(ENV['EDITOR'] || 'nvim', __FILE__) }
|
|
249
|
+
hiiro.add_subcmd(:save) { |pr_number=nil| manager.save(pr_number) }
|
|
250
|
+
hiiro.add_subcmd(:history) { |*args| manager.history(args) }
|
|
251
|
+
hiiro.add_subcmd(:current) { manager.current }
|
|
252
|
+
hiiro.add_subcmd(:open) { |pr_number=nil| manager.open(pr_number) }
|
|
253
|
+
hiiro.add_subcmd(:view) { |pr_number=nil| manager.view(pr_number) }
|
|
254
|
+
|
|
255
|
+
hiiro.add_default { manager.help }
|
|
256
|
+
|
|
257
|
+
hiiro.run
|
data/bin/h-session
CHANGED
|
@@ -1,26 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
require 'pry'
|
|
5
|
-
require 'rspec'
|
|
6
|
-
|
|
7
|
-
# $> h session
|
|
8
|
-
#
|
|
9
|
-
# Subcommand required!
|
|
10
|
-
#
|
|
11
|
-
# Possible subcommands:
|
|
12
|
-
# pin (subcommand) /Users/unixsuperhero/.config/hiiro/plugins/pins.rb:12
|
|
13
|
-
# edit (subcommand) /Users/unixsuperhero/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/hiiro-0.1.8/lib/hiiro.rb:14
|
|
14
|
-
# ses (subcommand) /Users/unixsuperhero/bin/h-session:99
|
|
15
|
-
# ls (subcommand) /Users/unixsuperhero/bin/h-session:111
|
|
16
|
-
# new (subcommand) /Users/unixsuperhero/bin/h-session:115
|
|
17
|
-
# kill (subcommand) /Users/unixsuperhero/bin/h-session:119
|
|
18
|
-
# attach (subcommand) /Users/unixsuperhero/bin/h-session:123
|
|
19
|
-
# rename (subcommand) /Users/unixsuperhero/bin/h-session:127
|
|
20
|
-
# switch (subcommand) /Users/unixsuperhero/bin/h-session:131
|
|
21
|
-
# detach (subcommand) /Users/unixsuperhero/bin/h-session:135
|
|
22
|
-
# has (subcommand) /Users/unixsuperhero/bin/h-session:139
|
|
23
|
-
# info (subcommand) /Users/unixsuperhero/bin/h-session:143
|
|
3
|
+
load File.join(Dir.home, 'bin', 'h')
|
|
24
4
|
|
|
25
5
|
o = Hiiro.init(*ARGV, plugins: [Pins])
|
|
26
6
|
|
data/bin/h-subtask
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
# Shorthand for `h task subtask`
|
|
4
|
+
|
|
5
|
+
require "hiiro"
|
|
6
|
+
require "fileutils"
|
|
7
|
+
require "yaml"
|
|
8
|
+
|
|
9
|
+
Hiiro.load_env
|
|
10
|
+
hiiro = Hiiro.init(*ARGV, plugins: [Tmux, Task])
|
|
11
|
+
|
|
12
|
+
tasks = hiiro.task_manager
|
|
13
|
+
|
|
14
|
+
hiiro.add_subcmd(:edit) { system(ENV['EDITOR'] || 'nvim', __FILE__) }
|
|
15
|
+
hiiro.add_subcmd(:ls) { tasks.list_subtasks }
|
|
16
|
+
hiiro.add_subcmd(:new) { |subtask_name| tasks.new_subtask(subtask_name) }
|
|
17
|
+
hiiro.add_subcmd(:switch) { |subtask_name| tasks.switch_subtask(subtask_name) }
|
|
18
|
+
|
|
19
|
+
hiiro.add_default { tasks.subtask_help }
|
|
20
|
+
|
|
21
|
+
hiiro.run
|
data/bin/h-wtree
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require 'hiiro'
|
|
4
|
+
|
|
5
|
+
Hiiro.run(*ARGV) do |h|
|
|
6
|
+
h.add_subcmd(:ls) { |*args| system('git', 'worktree', 'list', *args) }
|
|
7
|
+
h.add_subcmd(:list) { |*args| h.run_subcmd(:ls, *args) }
|
|
8
|
+
h.add_subcmd(:add) { |*args| system('git', 'worktree', 'add', *args) }
|
|
9
|
+
h.add_subcmd(:lock) { |*args| system('git', 'worktree', 'lock', *args) }
|
|
10
|
+
h.add_subcmd(:move) { |*args| system('git', 'worktree', 'move', *args) }
|
|
11
|
+
h.add_subcmd(:prune) { |*args| system('git', 'worktree', 'prune', *args) }
|
|
12
|
+
h.add_subcmd(:remove) { |*args| system('git', 'worktree', 'remove', *args) }
|
|
13
|
+
h.add_subcmd(:repair) { |*args| system('git', 'worktree', 'repair', *args) }
|
|
14
|
+
h.add_subcmd(:unlock) { |*args| system('git', 'worktree', 'unlock', *args) }
|
|
15
|
+
end
|
|
16
|
+
|
data/lib/hiiro/version.rb
CHANGED
data/lib/hiiro.rb
CHANGED
|
@@ -19,6 +19,12 @@ class Hiiro
|
|
|
19
19
|
end
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
+
def self.run(*args, plugins: [], logging: false, **values, &block)
|
|
23
|
+
hiiro = init(*args, plugins:,logging:,**values, &block)
|
|
24
|
+
|
|
25
|
+
hiiro.run
|
|
26
|
+
end
|
|
27
|
+
|
|
22
28
|
def self.load_env
|
|
23
29
|
Config.plugin_files.each do |plugin_file|
|
|
24
30
|
require plugin_file
|
|
@@ -82,6 +88,11 @@ class Hiiro
|
|
|
82
88
|
end
|
|
83
89
|
alias add_subcmd add_subcommand
|
|
84
90
|
|
|
91
|
+
def run_subcommand(name, *args)
|
|
92
|
+
runners.run_subcommand(name, *args)
|
|
93
|
+
end
|
|
94
|
+
alias run_subcmd run_subcommand
|
|
95
|
+
|
|
85
96
|
def full_name
|
|
86
97
|
runner&.full_name || [bin_name, subcmd].join(?-)
|
|
87
98
|
end
|
|
@@ -226,6 +237,12 @@ class Hiiro
|
|
|
226
237
|
@subcommands[name] = Subcommand.new(bin_name, name, handler, values)
|
|
227
238
|
end
|
|
228
239
|
|
|
240
|
+
def run_subcommand(name, *args)
|
|
241
|
+
cmd = subcommands[name]
|
|
242
|
+
|
|
243
|
+
cmd&.run(*args)
|
|
244
|
+
end
|
|
245
|
+
|
|
229
246
|
def exact_runner
|
|
230
247
|
all_runners.find { |r| r.exact_match?(subcmd) }
|
|
231
248
|
end
|
data/plugins/task.rb
CHANGED
|
@@ -26,6 +26,7 @@ module Task
|
|
|
26
26
|
status: ->(*sargs) { tasks.status },
|
|
27
27
|
save: ->(*sargs) { tasks.save_current },
|
|
28
28
|
stop: ->(*sargs) { tasks.stop_current },
|
|
29
|
+
subtask: ->(*sargs) { tasks.handle_subtask(*sargs) },
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
case args
|
|
@@ -51,6 +52,14 @@ module Task
|
|
|
51
52
|
tasks.stop_current
|
|
52
53
|
in ['stop', task_name]
|
|
53
54
|
tasks.stop_task(task_name)
|
|
55
|
+
in ['subtask']
|
|
56
|
+
tasks.subtask_help
|
|
57
|
+
in ['subtask', 'ls']
|
|
58
|
+
tasks.list_subtasks
|
|
59
|
+
in ['subtask', 'new', subtask_name]
|
|
60
|
+
tasks.new_subtask(subtask_name)
|
|
61
|
+
in ['subtask', 'switch', subtask_name]
|
|
62
|
+
tasks.switch_subtask(subtask_name)
|
|
54
63
|
in [subcmd, *sargs]
|
|
55
64
|
match = runner_map.keys.find { |full_subcmd| full_subcmd.to_s.start_with?(subcmd) }
|
|
56
65
|
|
|
@@ -94,6 +103,7 @@ module Task
|
|
|
94
103
|
puts " save Save current tmux session info for this task"
|
|
95
104
|
puts " status, st Show current task status"
|
|
96
105
|
puts " stop Stop working on current task (worktree becomes available)"
|
|
106
|
+
puts " subtask <subcmd> Manage subtasks (ls, new, switch)"
|
|
97
107
|
end
|
|
98
108
|
|
|
99
109
|
# List all worktrees and their active tasks
|
|
@@ -363,6 +373,183 @@ module Task
|
|
|
363
373
|
true
|
|
364
374
|
end
|
|
365
375
|
|
|
376
|
+
# Subtask management
|
|
377
|
+
def subtask_help
|
|
378
|
+
puts "Usage: h task subtask <subcommand> [args]"
|
|
379
|
+
puts " h subtask <subcommand> [args]"
|
|
380
|
+
puts
|
|
381
|
+
puts "Subcommands:"
|
|
382
|
+
puts " ls List subtasks for current task"
|
|
383
|
+
puts " new SUBTASK_NAME Start a new subtask (creates worktree and session)"
|
|
384
|
+
puts " switch SUBTASK_NAME Switch to subtask's tmux session"
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def handle_subtask(*args)
|
|
388
|
+
case args
|
|
389
|
+
in []
|
|
390
|
+
subtask_help
|
|
391
|
+
in ['ls']
|
|
392
|
+
list_subtasks
|
|
393
|
+
in ['new', subtask_name]
|
|
394
|
+
new_subtask(subtask_name)
|
|
395
|
+
in ['switch', subtask_name]
|
|
396
|
+
switch_subtask(subtask_name)
|
|
397
|
+
else
|
|
398
|
+
puts "Unknown subtask command: #{args.inspect}"
|
|
399
|
+
subtask_help
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def list_subtasks
|
|
404
|
+
current = current_task
|
|
405
|
+
unless current
|
|
406
|
+
puts "ERROR: Not currently in a task session"
|
|
407
|
+
return false
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
parent_task = current[:task]
|
|
411
|
+
subtasks = subtasks_for_task(parent_task)
|
|
412
|
+
|
|
413
|
+
if subtasks.empty?
|
|
414
|
+
puts "No subtasks for task '#{parent_task}'."
|
|
415
|
+
puts
|
|
416
|
+
puts "Create one with 'h subtask new SUBTASK_NAME'"
|
|
417
|
+
return
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
puts "Subtasks for '#{parent_task}':"
|
|
421
|
+
puts
|
|
422
|
+
subtasks.each do |subtask|
|
|
423
|
+
marker = subtask['active'] ? "*" : " "
|
|
424
|
+
puts format("%s %-25s tree: %-15s created: %s",
|
|
425
|
+
marker,
|
|
426
|
+
subtask['name'],
|
|
427
|
+
subtask['worktree'] || '(none)',
|
|
428
|
+
subtask['created_at']&.split('T')&.first || '?'
|
|
429
|
+
)
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
def new_subtask(subtask_name)
|
|
434
|
+
current = current_task
|
|
435
|
+
unless current
|
|
436
|
+
puts "ERROR: Not currently in a task session"
|
|
437
|
+
puts "Use 'h task start TASK_NAME' first"
|
|
438
|
+
return false
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
parent_task = current[:task]
|
|
442
|
+
full_subtask_name = "#{parent_task}/#{subtask_name}"
|
|
443
|
+
|
|
444
|
+
# Check if subtask already exists
|
|
445
|
+
existing = subtasks_for_task(parent_task).find { |s| s['name'] == subtask_name }
|
|
446
|
+
if existing
|
|
447
|
+
puts "Subtask '#{subtask_name}' already exists for task '#{parent_task}'"
|
|
448
|
+
puts "Switching to existing session..."
|
|
449
|
+
switch_subtask(subtask_name)
|
|
450
|
+
return true
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
# Create new worktree for subtask
|
|
454
|
+
puts "Creating new worktree for subtask '#{subtask_name}'..."
|
|
455
|
+
new_path = File.join(Dir.home, 'work', full_subtask_name)
|
|
456
|
+
|
|
457
|
+
result = system('git', '-C', main_repo_path, 'worktree', 'add', '--detach', new_path)
|
|
458
|
+
unless result
|
|
459
|
+
puts "ERROR: Failed to create worktree"
|
|
460
|
+
return false
|
|
461
|
+
end
|
|
462
|
+
clear_worktree_cache
|
|
463
|
+
|
|
464
|
+
# Associate subtask with tree
|
|
465
|
+
assign_task_to_tree(full_subtask_name, full_subtask_name)
|
|
466
|
+
|
|
467
|
+
# Record subtask in parent's metadata
|
|
468
|
+
add_subtask_to_task(parent_task, {
|
|
469
|
+
'name' => subtask_name,
|
|
470
|
+
'worktree' => full_subtask_name,
|
|
471
|
+
'session' => full_subtask_name,
|
|
472
|
+
'created_at' => Time.now.iso8601,
|
|
473
|
+
'active' => true
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
# Create/switch to tmux session
|
|
477
|
+
Dir.chdir(new_path)
|
|
478
|
+
hiiro.start_tmux_session(full_subtask_name)
|
|
479
|
+
|
|
480
|
+
puts "Started subtask '#{subtask_name}' for task '#{parent_task}'"
|
|
481
|
+
true
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
def switch_subtask(subtask_name)
|
|
485
|
+
current = current_task
|
|
486
|
+
unless current
|
|
487
|
+
puts "ERROR: Not currently in a task session"
|
|
488
|
+
puts "Use 'h task start TASK_NAME' first"
|
|
489
|
+
return false
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
parent_task = current[:task]
|
|
493
|
+
# Handle if we're in a subtask - get the parent
|
|
494
|
+
if parent_task.include?('/')
|
|
495
|
+
parent_task = parent_task.split('/').first
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
subtask = find_subtask(parent_task, subtask_name)
|
|
499
|
+
unless subtask
|
|
500
|
+
puts "Subtask '#{subtask_name}' not found for task '#{parent_task}'"
|
|
501
|
+
puts
|
|
502
|
+
list_subtasks
|
|
503
|
+
return false
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
session_name = subtask['session'] || "#{parent_task}/#{subtask_name}"
|
|
507
|
+
tree_name = subtask['worktree'] || session_name
|
|
508
|
+
|
|
509
|
+
# Check if session exists
|
|
510
|
+
session_exists = system('tmux', 'has-session', '-t', session_name, err: File::NULL)
|
|
511
|
+
|
|
512
|
+
if session_exists
|
|
513
|
+
hiiro.start_tmux_session(session_name)
|
|
514
|
+
else
|
|
515
|
+
# Create new session in the worktree path
|
|
516
|
+
path = tree_path(tree_name)
|
|
517
|
+
if Dir.exist?(path)
|
|
518
|
+
Dir.chdir(path)
|
|
519
|
+
hiiro.start_tmux_session(session_name)
|
|
520
|
+
else
|
|
521
|
+
puts "ERROR: Worktree path '#{path}' does not exist"
|
|
522
|
+
return false
|
|
523
|
+
end
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
puts "Switched to subtask '#{subtask_name}'"
|
|
527
|
+
true
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
private
|
|
531
|
+
|
|
532
|
+
def subtasks_for_task(task_name)
|
|
533
|
+
meta = task_metadata(task_name)
|
|
534
|
+
return [] unless meta
|
|
535
|
+
meta['subtasks'] || []
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
def find_subtask(parent_task, subtask_name)
|
|
539
|
+
subtasks = subtasks_for_task(parent_task)
|
|
540
|
+
subtasks.find { |s| s['name'].start_with?(subtask_name) }
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
def add_subtask_to_task(parent_task, subtask_data)
|
|
544
|
+
meta = task_metadata(parent_task) || {}
|
|
545
|
+
meta['subtasks'] ||= []
|
|
546
|
+
meta['subtasks'] << subtask_data
|
|
547
|
+
FileUtils.mkdir_p(task_dir)
|
|
548
|
+
File.write(task_metadata_file(parent_task), YAML.dump(meta))
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
public
|
|
552
|
+
|
|
366
553
|
def list_configured_apps
|
|
367
554
|
if apps_config.any?
|
|
368
555
|
puts "Configured apps (#{apps_config_file}):"
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hiiro
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.11
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Joshua Toyota
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-01-
|
|
11
|
+
date: 2026-01-28 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: pry
|
|
@@ -39,13 +39,18 @@ files:
|
|
|
39
39
|
- README.md
|
|
40
40
|
- Rakefile
|
|
41
41
|
- bin/h
|
|
42
|
+
- bin/h-branch
|
|
42
43
|
- bin/h-buffer
|
|
44
|
+
- bin/h-link
|
|
43
45
|
- bin/h-pane
|
|
44
46
|
- bin/h-plugin
|
|
47
|
+
- bin/h-pr
|
|
45
48
|
- bin/h-session
|
|
49
|
+
- bin/h-subtask
|
|
46
50
|
- bin/h-task
|
|
47
51
|
- bin/h-video
|
|
48
52
|
- bin/h-window
|
|
53
|
+
- bin/h-wtree
|
|
49
54
|
- docs/README.md
|
|
50
55
|
- docs/h-buffer.md
|
|
51
56
|
- docs/h-pane.md
|