hiiro 0.1.10 → 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-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 +11 -4
- data/bin/h-project +0 -176
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-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,13 +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
|
+
autorequire:
|
|
8
9
|
bindir: exe
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date:
|
|
11
|
+
date: 2026-01-28 00:00:00.000000000 Z
|
|
11
12
|
dependencies:
|
|
12
13
|
- !ruby/object:Gem::Dependency
|
|
13
14
|
name: pry
|
|
@@ -38,14 +39,18 @@ files:
|
|
|
38
39
|
- README.md
|
|
39
40
|
- Rakefile
|
|
40
41
|
- bin/h
|
|
42
|
+
- bin/h-branch
|
|
41
43
|
- bin/h-buffer
|
|
44
|
+
- bin/h-link
|
|
42
45
|
- bin/h-pane
|
|
43
46
|
- bin/h-plugin
|
|
44
|
-
- bin/h-
|
|
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
|
|
@@ -72,6 +77,7 @@ metadata:
|
|
|
72
77
|
homepage_uri: https://github.com/unixsuperhero/hiiro
|
|
73
78
|
source_code_uri: https://github.com/unixsuperhero/hiiro
|
|
74
79
|
changelog_uri: https://github.com/unixsuperhero/hiiro/blob/main/CHANGELOG.md
|
|
80
|
+
post_install_message:
|
|
75
81
|
rdoc_options: []
|
|
76
82
|
require_paths:
|
|
77
83
|
- lib
|
|
@@ -86,7 +92,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
86
92
|
- !ruby/object:Gem::Version
|
|
87
93
|
version: '0'
|
|
88
94
|
requirements: []
|
|
89
|
-
rubygems_version: 3.
|
|
95
|
+
rubygems_version: 3.3.7
|
|
96
|
+
signing_key:
|
|
90
97
|
specification_version: 4
|
|
91
98
|
summary: A lightweight CLI framework for Ruby
|
|
92
99
|
test_files: []
|
data/bin/h-project
DELETED
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
|
-
load '/Users/unixsuperhero/bin/h'
|
|
3
|
-
|
|
4
|
-
o = Hiiro.init(*ARGV)
|
|
5
|
-
|
|
6
|
-
# Helper to start or attach to a tmux session
|
|
7
|
-
def start_tmux_session(session_name)
|
|
8
|
-
session_name = session_name.to_s
|
|
9
|
-
|
|
10
|
-
unless system('tmux', 'has-session', '-t', session_name)
|
|
11
|
-
system('tmux', 'new', '-d', '-A', '-s', session_name)
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
if ENV['TMUX']
|
|
15
|
-
system('tmux', 'switchc', '-t', session_name)
|
|
16
|
-
elsif ENV['NVIM']
|
|
17
|
-
puts "Can't attach to tmux inside a vim terminal"
|
|
18
|
-
else
|
|
19
|
-
system('tmux', 'new', '-A', '-s', session_name)
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
# Get project directories from ~/proj/
|
|
24
|
-
def project_dirs
|
|
25
|
-
Dir.glob(File.join(Dir.home, 'proj', '*/')).map { |path|
|
|
26
|
-
[File.basename(path), path]
|
|
27
|
-
}.to_h
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
# Get projects from config file
|
|
31
|
-
def projects_from_config
|
|
32
|
-
projects_file = File.join(Dir.home, '.config/hiiro', 'projects.yml')
|
|
33
|
-
|
|
34
|
-
return {} unless File.exist?(projects_file)
|
|
35
|
-
|
|
36
|
-
require 'yaml'
|
|
37
|
-
YAML.safe_load_file(projects_file)
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
# === OPEN PROJECT (default) ===
|
|
41
|
-
|
|
42
|
-
o.add_subcmd(:open) { |project_name|
|
|
43
|
-
re = /#{project_name}/i
|
|
44
|
-
|
|
45
|
-
conf_matches = projects_from_config.select { |k, v| k.match?(re) }
|
|
46
|
-
dir_matches = project_dirs.select { |proj, path| proj.match?(re) }
|
|
47
|
-
|
|
48
|
-
matches = dir_matches.merge(conf_matches)
|
|
49
|
-
if matches.count > 1
|
|
50
|
-
matches = matches.select { |name, path| name == project_name }
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
case matches.count
|
|
54
|
-
when 0
|
|
55
|
-
name = 'proj'
|
|
56
|
-
path = File.join(Dir.home, 'proj')
|
|
57
|
-
|
|
58
|
-
unless Dir.exist?(path)
|
|
59
|
-
puts "Error: #{path.inspect} does not exist"
|
|
60
|
-
exit 1
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
puts "changing dir: #{path}"
|
|
64
|
-
Dir.chdir(path)
|
|
65
|
-
|
|
66
|
-
start_tmux_session(name)
|
|
67
|
-
when 1
|
|
68
|
-
name, path = matches.first
|
|
69
|
-
|
|
70
|
-
puts "changing dir: #{path}"
|
|
71
|
-
Dir.chdir(path)
|
|
72
|
-
|
|
73
|
-
start_tmux_session(name)
|
|
74
|
-
when (2..)
|
|
75
|
-
puts "ERROR: Multiple matches found"
|
|
76
|
-
puts
|
|
77
|
-
puts "Matches:"
|
|
78
|
-
matches.each { |name, path|
|
|
79
|
-
puts format(" %s: %s", name, path)
|
|
80
|
-
}
|
|
81
|
-
end
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
# === LIST PROJECTS ===
|
|
85
|
-
|
|
86
|
-
o.add_subcmd(:list) { |*args|
|
|
87
|
-
dirs = project_dirs
|
|
88
|
-
conf = projects_from_config
|
|
89
|
-
|
|
90
|
-
all_projects = dirs.merge(conf)
|
|
91
|
-
|
|
92
|
-
puts "Projects:"
|
|
93
|
-
puts
|
|
94
|
-
all_projects.keys.sort.each do |name|
|
|
95
|
-
path = all_projects[name]
|
|
96
|
-
source = conf.key?(name) ? '[config]' : '[dir]'
|
|
97
|
-
puts format(" %-12s %-8s %s", name, source, path)
|
|
98
|
-
end
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
o.add_subcmd(:ls) { |*args|
|
|
102
|
-
o.run_subcmd(:list, *args)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
# === SHOW CONFIG FILE ===
|
|
106
|
-
|
|
107
|
-
o.add_subcmd(:config) { |*args|
|
|
108
|
-
projects_file = File.join(Dir.home, '.config/hiiro', 'projects.yml')
|
|
109
|
-
|
|
110
|
-
if File.exist?(projects_file)
|
|
111
|
-
puts File.read(projects_file)
|
|
112
|
-
else
|
|
113
|
-
puts "No config file found at: #{projects_file}"
|
|
114
|
-
puts
|
|
115
|
-
puts "Create it with YAML format:"
|
|
116
|
-
puts " project_name: /path/to/project"
|
|
117
|
-
end
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
# === EDIT CONFIG FILE ===
|
|
121
|
-
|
|
122
|
-
o.add_subcmd(:edit) { |*args|
|
|
123
|
-
projects_file = File.join(Dir.home, '.config/hiiro', 'projects.yml')
|
|
124
|
-
editor = ENV['EDITOR'] || 'vim'
|
|
125
|
-
|
|
126
|
-
# Create config dir if needed
|
|
127
|
-
config_dir = File.dirname(projects_file)
|
|
128
|
-
Dir.mkdir(config_dir) unless Dir.exist?(config_dir)
|
|
129
|
-
|
|
130
|
-
# Create empty file if it doesn't exist
|
|
131
|
-
unless File.exist?(projects_file)
|
|
132
|
-
File.write(projects_file, "# Project aliases\n# project_name: /path/to/project\n")
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
exec(editor, projects_file)
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
# === HELP ===
|
|
139
|
-
|
|
140
|
-
o.add_subcmd(:help) { |*args|
|
|
141
|
-
puts <<~HELP
|
|
142
|
-
h-project - Project directory and tmux session manager
|
|
143
|
-
|
|
144
|
-
USAGE:
|
|
145
|
-
h-project <project_name> Open project (fuzzy match) and start tmux session
|
|
146
|
-
h-project open <project_name> Same as above
|
|
147
|
-
h-project list List all known projects
|
|
148
|
-
h-project ls Alias for list
|
|
149
|
-
h-project config Show config file contents
|
|
150
|
-
h-project edit Edit config file
|
|
151
|
-
|
|
152
|
-
SOURCES:
|
|
153
|
-
Projects are discovered from two sources:
|
|
154
|
-
1. Directories in ~/proj/
|
|
155
|
-
2. Entries in ~/.config/hiiro/projects.yml
|
|
156
|
-
|
|
157
|
-
CONFIG FORMAT (projects.yml):
|
|
158
|
-
project_name: /path/to/project
|
|
159
|
-
another: /some/other/path
|
|
160
|
-
|
|
161
|
-
MATCHING:
|
|
162
|
-
Project names are matched using regex (case insensitive).
|
|
163
|
-
If multiple matches are found, an exact match is preferred.
|
|
164
|
-
If still ambiguous, all matches are displayed.
|
|
165
|
-
HELP
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
# === DEFAULT SUBCOMMAND ===
|
|
169
|
-
|
|
170
|
-
o.default_subcommand(:open)
|
|
171
|
-
|
|
172
|
-
if o.runnable?
|
|
173
|
-
o.run
|
|
174
|
-
else
|
|
175
|
-
o.run_subcmd(:help)
|
|
176
|
-
end
|