hiiro 0.1.265 → 0.1.266

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: dc901c630e69f036ba17ebb63bf913618ea825ce6746d5db20df55177fd0b9ba
4
- data.tar.gz: 29ef84306acd767569975e56c2c44e10868f7490040b94d80d73f20db7ade326
3
+ metadata.gz: 7eecd997945d2c597a4b6e9c28ec2d8b485871ae91f304695cddeec40414c605
4
+ data.tar.gz: ccb6d7acb90b1d794c650ddad420d7c113baa9e8819ae2f2ec205e1fec6cf652
5
5
  SHA512:
6
- metadata.gz: 053fa8e529cb941c97447e0540cbaa2ebb48634a47911627760cd01af2f5f3251d403b69a8296443f33531efcf039bfcba738ee5f02065a475583da1bb8c7a70
7
- data.tar.gz: 40073e682ab750f5e5633ea511a500bc2537708aa2abcb392b994cbd30a9c580227058859829f10838669ce5f790693d34473a480d680d11191f6b57227275ab
6
+ metadata.gz: 0f8a631772992bfc9f61d9a8ea25c16e993459f1792441a963d7e2ff1563b3bcce9ce4bbf213225ebc0eb7e03527be0a316599417b34832b24056e30649e7547
7
+ data.tar.gz: 78a971962958be80d9936c0606329a54b5a6a4ec6cf4a8185f07d1b0bf3d61c9d504eba8e9bebd6e07627e71feb89ba968ab216e8ace23852fde2200390a7623
data/CHANGELOG.md CHANGED
@@ -1 +1 @@
1
- Done. The CHANGELOG.md has been updated with v0.1.265 released today (2026-03-18), with all the unreleased features now captured in that release. An empty Unreleased section remains at the top for future entries.
1
+ Done. CHANGELOG.md updated with v0.1.266 section at the top.
data/bin/h-pane CHANGED
@@ -107,63 +107,11 @@ Hiiro.run(*ARGV, plugins: [Pins]) do
107
107
  end
108
108
  }
109
109
 
110
- add_subcmd(:sw) { |target = nil, *args|
110
+ add_subcmd(:sw, :switch) { |target = nil, *args|
111
111
  target ||= fuzzyfind_from_map(tmux.panes(all: true).name_map)
112
112
  tmux.open_session(target) if target
113
113
  }
114
114
 
115
- add_subcmd(:switch) { |name = nil|
116
- homes = load_homes.call
117
-
118
- if name.nil?
119
- if homes.empty?
120
- puts "No home panes configured. Use 'h pane home add <name> <session> [path]'"
121
- next
122
- end
123
- display_map = homes.each_with_object({}) do |(n, cfg), h|
124
- label = "#{n} [#{cfg['session']}#{cfg['path'] ? " #{cfg['path']}" : ''}]"
125
- h[label] = n
126
- end
127
- name = fuzzyfind_from_map(display_map)
128
- next unless name
129
- end
130
-
131
- cfg = homes[name]
132
- unless cfg
133
- matched = homes.keys.select { |k| k.start_with?(name) }
134
- case matched.size
135
- when 1
136
- name = matched.first
137
- cfg = homes[name]
138
- when 0
139
- puts "Home pane '#{name}' not found"
140
- next
141
- else
142
- puts "Ambiguous home pane '#{name}': #{matched.join(', ')}"
143
- next
144
- end
145
- end
146
-
147
- session = cfg['session']
148
- path = cfg['path']
149
-
150
- if !tmux.session_exists?(session)
151
- tmux.new_session(session, detached: true, start_directory: path, window_name: name)
152
- else
153
- window_names = `tmux list-windows -t #{session.shellescape} -F '\#{window_name}' 2>/dev/null`.lines(chomp: true)
154
- unless window_names.include?(name)
155
- tmux.new_window(name: name, target: session, start_directory: path)
156
- end
157
- end
158
-
159
- target = "#{session}:#{name}"
160
- if tmux.in_tmux?
161
- system('tmux', 'switch-client', '-t', target)
162
- else
163
- system('tmux', 'attach-session', '-t', target)
164
- end
165
- }
166
-
167
115
  add_subcmd(:home) { |*args|
168
116
  run_child(:home) do |h|
169
117
  h.add_subcmd(:ls) {
@@ -204,6 +152,59 @@ Hiiro.run(*ARGV, plugins: [Pins]) do
204
152
  puts "Home pane '#{name}' not found"
205
153
  end
206
154
  }
155
+
156
+ h.add_subcmd(:switch) { |name = nil|
157
+ homes = load_homes.call
158
+
159
+ if name.nil?
160
+ if homes.empty?
161
+ puts "No home panes configured. Use 'h pane home add <name> <session> [path]'"
162
+ next
163
+ end
164
+ display_map = homes.each_with_object({}) do |(n, cfg), map|
165
+ label = "#{n} [#{cfg['session']}#{cfg['path'] ? " #{cfg['path']}" : ''}]"
166
+ map[label] = n
167
+ end
168
+ name = fuzzyfind_from_map(display_map)
169
+ next unless name
170
+ end
171
+
172
+ cfg = homes[name]
173
+ unless cfg
174
+ matched = homes.keys.select { |k| k.start_with?(name) }
175
+ case matched.size
176
+ when 1
177
+ name = matched.first
178
+ cfg = homes[name]
179
+ when 0
180
+ puts "Home pane '#{name}' not found"
181
+ next
182
+ else
183
+ puts "Ambiguous home pane '#{name}': #{matched.join(', ')}"
184
+ next
185
+ end
186
+ end
187
+
188
+ session = cfg['session']
189
+ path = cfg['path']
190
+
191
+ if !tmux.session_exists?(session)
192
+ tmux.new_session(session, detached: true, start_directory: path, window_name: name)
193
+ else
194
+ window_names = `tmux list-windows -t #{session.shellescape} -F '\#{window_name}' 2>/dev/null`.lines(chomp: true)
195
+ unless window_names.include?(name)
196
+ dir = path || tmux.find_session(session)&.path
197
+ tmux.new_window(name: name, target: session, start_directory: dir)
198
+ end
199
+ end
200
+
201
+ target = "#{session}:#{name}"
202
+ if tmux.in_tmux?
203
+ system('tmux', 'switch-client', '-t', target)
204
+ else
205
+ system('tmux', 'attach-session', '-t', target)
206
+ end
207
+ }
207
208
  end
208
209
  }
209
210
 
data/bin/h-pr CHANGED
@@ -1863,6 +1863,115 @@ Hiiro.run(*ARGV, plugins: [Pins]) do
1863
1863
  end
1864
1864
  end
1865
1865
 
1866
+ add_subcmd(:dep) do |*args|
1867
+ run_child(:dep) do |h|
1868
+ h.add_subcmd(:add) { |ref = nil, *dep_refs|
1869
+ if ref.nil? || dep_refs.empty?
1870
+ puts "Usage: h pr dep add <pr> <dep1> [dep2 ...]"
1871
+ next
1872
+ end
1873
+
1874
+ pr_number = resolve_pr.call(ref)
1875
+ next unless pr_number
1876
+
1877
+ dep_numbers = dep_refs.map { |r| resolve_pr.call(r) }.compact
1878
+ if dep_numbers.empty?
1879
+ puts "No valid dependency PRs resolved"
1880
+ next
1881
+ end
1882
+
1883
+ pinned = pinned_manager.load_pinned
1884
+ pr = pinned.find { |p| p.number.to_s == pr_number.to_s }
1885
+ unless pr
1886
+ puts "PR ##{pr_number} is not tracked — pin it first with: h pr track #{pr_number}"
1887
+ next
1888
+ end
1889
+
1890
+ pr.depends_on = (Array(pr.depends_on) + dep_numbers.map(&:to_i)).uniq.sort
1891
+ pinned_manager.save_pinned(pinned)
1892
+ puts "PR ##{pr_number} now depends on: #{pr.depends_on.map { |n| "##{n}" }.join(', ')}"
1893
+ }
1894
+
1895
+ h.add_subcmd(:rm) { |ref = nil, *dep_refs|
1896
+ if ref.nil?
1897
+ puts "Usage: h pr dep rm <pr> [dep1 dep2 ...] (omit deps to clear all)"
1898
+ next
1899
+ end
1900
+
1901
+ pr_number = resolve_pr.call(ref)
1902
+ next unless pr_number
1903
+
1904
+ pinned = pinned_manager.load_pinned
1905
+ pr = pinned.find { |p| p.number.to_s == pr_number.to_s }
1906
+ unless pr
1907
+ puts "PR ##{pr_number} not in tracked list"
1908
+ next
1909
+ end
1910
+
1911
+ if dep_refs.empty?
1912
+ pr.depends_on = nil
1913
+ puts "Cleared all dependencies from ##{pr_number}"
1914
+ else
1915
+ dep_numbers = dep_refs.map { |r| resolve_pr.call(r) }.compact.map(&:to_i)
1916
+ pr.depends_on = (Array(pr.depends_on) - dep_numbers).then { |d| d.empty? ? nil : d }
1917
+ puts "Removed dep(s) #{dep_numbers.map { |n| "##{n}" }.join(', ')} from ##{pr_number}"
1918
+ puts " Remaining: #{Array(pr.depends_on).map { |n| "##{n}" }.join(', ').then { |s| s.empty? ? '(none)' : s }}"
1919
+ end
1920
+
1921
+ pinned_manager.save_pinned(pinned)
1922
+ }
1923
+
1924
+ h.add_subcmd(:ls) { |ref = nil|
1925
+ pinned = pinned_manager.load_pinned
1926
+
1927
+ if ref
1928
+ pr_number = resolve_pr.call(ref)
1929
+ next unless pr_number
1930
+ pr = pinned.find { |p| p.number.to_s == pr_number.to_s }
1931
+ unless pr
1932
+ puts "PR ##{pr_number} not in tracked list"
1933
+ next
1934
+ end
1935
+ deps = Array(pr.depends_on)
1936
+ if deps.empty?
1937
+ puts "##{pr_number} has no dependencies"
1938
+ else
1939
+ puts "##{pr_number}: #{pr.title}"
1940
+ puts " depends on:"
1941
+ deps.each do |dep_num|
1942
+ dep = pinned.find { |p| p.number == dep_num }
1943
+ label = dep ? " ##{dep_num}: #{dep.title}" : " ##{dep_num} (not tracked)"
1944
+ puts label
1945
+ end
1946
+ end
1947
+ else
1948
+ with_deps = pinned.select { |p| Array(p.depends_on).any? }
1949
+ if with_deps.empty?
1950
+ puts "No dependency relationships configured."
1951
+ puts "Use: h pr dep add <pr> <dep1> [dep2 ...]"
1952
+ else
1953
+ with_deps.each do |pr|
1954
+ puts "##{pr.number}: #{pr.title}"
1955
+ Array(pr.depends_on).each do |dep_num|
1956
+ dep = pinned.find { |p| p.number == dep_num }
1957
+ label = dep ? " └─ ##{dep_num}: #{dep.title}" : " └─ ##{dep_num} (not tracked)"
1958
+ puts label
1959
+ end
1960
+ end
1961
+ end
1962
+ end
1963
+ }
1964
+
1965
+ h.add_subcmd(:config) {
1966
+ puts Hiiro::Git::Pr::PINNED_FILE
1967
+ }
1968
+
1969
+ h.add_subcmd(:edit) {
1970
+ edit_files Hiiro::Git::Pr::PINNED_FILE
1971
+ }
1972
+ end
1973
+ end
1974
+
1866
1975
  add_subcmd(:config) do |*args|
1867
1976
  make_child do
1868
1977
  add_subcmd(:path) do
data/lib/hiiro/git/pr.rb CHANGED
@@ -9,7 +9,7 @@ class Hiiro
9
9
  attr_accessor :number, :title, :state, :url, :head_branch, :base_branch,
10
10
  :repo, :slot, :is_draft, :mergeable, :review_decision,
11
11
  :checks, :check_runs, :reviews, :last_checked, :pinned_at, :updated_at,
12
- :task, :worktree, :tmux_session, :tags, :assigned, :authored
12
+ :task, :worktree, :tmux_session, :tags, :assigned, :authored, :depends_on
13
13
 
14
14
  # Load all pinned PRs from YAML, returning an array of Pr instances.
15
15
  def self.pinned_prs
@@ -58,6 +58,7 @@ class Hiiro
58
58
  tags: hash['tags'],
59
59
  assigned: hash['assigned'],
60
60
  authored: hash['authored'],
61
+ depends_on: hash['depends_on'],
61
62
  )
62
63
  end
63
64
 
@@ -163,7 +164,8 @@ class Hiiro
163
164
  repo: nil, slot: nil, is_draft: nil, mergeable: nil, review_decision: nil,
164
165
  checks: nil, check_runs: nil, reviews: nil, last_checked: nil,
165
166
  pinned_at: nil, updated_at: nil,
166
- task: nil, worktree: nil, tmux_session: nil, tags: nil, assigned: nil, authored: nil)
167
+ task: nil, worktree: nil, tmux_session: nil, tags: nil, assigned: nil, authored: nil,
168
+ depends_on: nil)
167
169
  @number = number
168
170
  @title = title
169
171
  @state = state
@@ -187,6 +189,7 @@ class Hiiro
187
189
  @tags = tags
188
190
  @assigned = assigned
189
191
  @authored = authored
192
+ @depends_on = depends_on ? Array(depends_on).map(&:to_i) : nil
190
193
  end
191
194
 
192
195
  def open? = state&.upcase == 'OPEN'
@@ -233,6 +236,7 @@ class Hiiro
233
236
  'tags' => (Array(tags).empty? ? nil : tags),
234
237
  'assigned' => assigned,
235
238
  'authored' => authored,
239
+ 'depends_on' => (Array(depends_on).empty? ? nil : depends_on),
236
240
  }.compact
237
241
  end
238
242
 
data/lib/hiiro/queue.rb CHANGED
@@ -138,12 +138,16 @@ class Hiiro
138
138
  # Determine target tmux session and working directory from frontmatter
139
139
  target_session = TMUX_SESSION
140
140
  working_dir = Dir.pwd
141
+ tree_root = nil
141
142
 
142
143
  if prompt_obj
143
144
  if prompt_obj.task
144
145
  target_session = prompt_obj.session_name
145
146
  tree = prompt_obj.task.tree
146
- working_dir = tree.path if tree
147
+ if tree
148
+ working_dir = tree.path
149
+ tree_root = tree.path
150
+ end
147
151
  elsif prompt_obj.session
148
152
  target_session = prompt_obj.session.name
149
153
  working_dir = prompt_obj.session.path || working_dir
@@ -151,6 +155,22 @@ class Hiiro
151
155
 
152
156
  if prompt_obj.tree
153
157
  working_dir = prompt_obj.tree.path
158
+ tree_root = prompt_obj.tree.path
159
+ end
160
+
161
+ # Resolve app + dir frontmatter on top of whatever tree root we have
162
+ if prompt_obj.app_name
163
+ env = Environment.current rescue nil
164
+ app = env&.find_app(prompt_obj.app_name)
165
+ if app
166
+ root = tree_root || git_root_of(working_dir)
167
+ app_dir = File.join(root, app.relative_path)
168
+ working_dir = prompt_obj.rel_dir ? File.join(app_dir, prompt_obj.rel_dir) : app_dir
169
+ else
170
+ warn "hq: app '#{prompt_obj.app_name}' not found — falling back to tree/session dir"
171
+ end
172
+ elsif prompt_obj.rel_dir
173
+ working_dir = File.join(tree_root || working_dir, prompt_obj.rel_dir)
154
174
  end
155
175
  end
156
176
 
@@ -298,6 +318,11 @@ class Hiiro
298
318
  windows.include?(wname)
299
319
  end
300
320
 
321
+ def git_root_of(dir)
322
+ root = `git -C #{Shellwords.shellescape(dir)} rev-parse --show-toplevel 2>/dev/null`.strip
323
+ root.empty? ? dir : root
324
+ end
325
+
301
326
  def slugify(text)
302
327
  text.downcase.gsub(/[^a-z0-9]+/, '-').gsub(/^-|-$/, '')[0, 60]
303
328
  end
@@ -491,17 +516,17 @@ class Hiiro
491
516
  elsif args.any?
492
517
  content = args.join(' ')
493
518
  else
494
- # Pre-fill with frontmatter template if task_info or flags require it
495
- fm_content = if ti || opts.ignore
496
- fm_lines = ["---"]
497
- fm_lines << "task_name: #{ti[:task_name]}" if ti&.dig(:task_name)
498
- fm_lines << "tree_name: #{ti[:tree_name]}" if ti&.dig(:tree_name)
499
- fm_lines << "session_name: #{ti[:session_name]}" if ti&.dig(:session_name)
500
- fm_lines << "ignore: true" if opts.ignore
501
- fm_lines << "---"
502
- fm_lines << ""
503
- fm_lines.join("\n")
504
- end
519
+ # Pre-fill with frontmatter template
520
+ fm_lines = ["---"]
521
+ fm_lines << "task_name: #{ti[:task_name]}" if ti&.dig(:task_name)
522
+ fm_lines << "tree_name: #{ti[:tree_name]}" if ti&.dig(:tree_name)
523
+ fm_lines << "session_name: #{ti[:session_name]}" if ti&.dig(:session_name)
524
+ fm_lines << "ignore: true" if opts.ignore
525
+ fm_lines << "# app: <partial-app-name> (run claude from this app's directory)"
526
+ fm_lines << "# dir: <relative-path> (subdir within app or tree root)"
527
+ fm_lines << "---"
528
+ fm_lines << ""
529
+ fm_content = fm_lines.join("\n")
505
530
 
506
531
  input = InputFile.md_file(hiiro: h, content: fm_content, append: !!fm_content, prefix: 'hq-')
507
532
  input.edit
@@ -553,16 +578,15 @@ class Hiiro
553
578
  path = File.join(q.queue_dirs[:wip], "#{name}.md")
554
579
 
555
580
  unless File.exist?(path)
556
- # Pre-fill with frontmatter if task_info available
557
- if ti
558
- fm_lines = ["---"]
559
- fm_lines << "task_name: #{ti[:task_name]}" if ti[:task_name]
560
- fm_lines << "tree_name: #{ti[:tree_name]}" if ti[:tree_name]
561
- fm_lines << "session_name: #{ti[:session_name]}" if ti[:session_name]
562
- fm_lines << "---"
563
- fm_lines << ""
564
- File.write(path, fm_lines.join("\n"))
565
- end
581
+ fm_lines = ["---"]
582
+ fm_lines << "task_name: #{ti[:task_name]}" if ti&.dig(:task_name)
583
+ fm_lines << "tree_name: #{ti[:tree_name]}" if ti&.dig(:tree_name)
584
+ fm_lines << "session_name: #{ti[:session_name]}" if ti&.dig(:session_name)
585
+ fm_lines << "# app: <partial-app-name> (run claude from this app's directory)"
586
+ fm_lines << "# dir: <relative-path> (subdir within app or tree root)"
587
+ fm_lines << "---"
588
+ fm_lines << ""
589
+ File.write(path, fm_lines.join("\n"))
566
590
  end
567
591
 
568
592
  h.edit_files(path)
@@ -731,6 +755,14 @@ class Hiiro
731
755
  doc.front_matter['tree_name']
732
756
  end
733
757
 
758
+ def app_name
759
+ doc.front_matter['app']
760
+ end
761
+
762
+ def rel_dir
763
+ doc.front_matter['dir']
764
+ end
765
+
734
766
  def session_name
735
767
  doc.front_matter['session_name'] || task&.session_name
736
768
  end
data/lib/hiiro/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  class Hiiro
2
- VERSION = "0.1.265"
2
+ VERSION = "0.1.266"
3
3
  end
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.265
4
+ version: 0.1.266
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Toyota