hiiro 0.1.230 → 0.1.232

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.
Files changed (5) hide show
  1. checksums.yaml +4 -4
  2. data/bin/h-notify +152 -67
  3. data/bin/h-pr +75 -0
  4. data/lib/hiiro/version.rb +1 -1
  5. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: deca910b037607a912c6aee097095ec1598dd13a449e403a8958cf94abfe675b
4
- data.tar.gz: 17dd925fba07d3e8b4b827c9fa986f605c7cb37bea25441d5a084d5b2feef701
3
+ metadata.gz: a0d1ba68ed577241b03e05bbc748dbc880b0c411547a646bdc7bb9c77733f8c3
4
+ data.tar.gz: e25ca589b0609094596456dbef9f9afcb83963045dea1e2d809a50c227f4d7cd
5
5
  SHA512:
6
- metadata.gz: 0ef54e118391b0f7c99a238626ebe005db813cf21fbab0136075d0927c52c464af117368a487c410f786113203b055c0c9a4f8f184f87a639e9b32214a6c728c
7
- data.tar.gz: f88f14553901994056540f67ba493f2b1b0247fb46355baa66da87856ace0ac949106a68331f0cedd97d6a5496cb910d9451e74a4ed6dea1b9f970fd8b6fa95c
6
+ metadata.gz: 5c3558fd44e96b1b68ead9f8526f05c7de18e56fd0cd7345c77a3d8c649f6a236a82133aecd94676efd988e90dd192804a33120aca15aca73d1394d3c4043f76
7
+ data.tar.gz: e1ca686b11cdf7f0d795eb0a03a4275e660af825cf30f01a36563586f5843a75e5a4148bba648cf8b7c9bac264bcbac1b2f2dbb79c7324fedc349621f8e17c39
data/bin/h-notify CHANGED
@@ -13,6 +13,25 @@ TYPE_PRESETS = {
13
13
 
14
14
  LOG_FILE = Hiiro::Config.data_path('notify_log.yml')
15
15
 
16
+ TMUX_CONF = File.join(Dir.home, '.tmux.conf')
17
+ NOTIFY_CONF = File.join(Dir.home, '.config', 'tmux', 'h-notify.tmux.conf')
18
+ CLAUDE_SETTINGS = File.join(Dir.home, '.claude', 'settings.json')
19
+
20
+ NOTIFY_TMUX_HOOKS = %w[after-kill-pane window-unlinked session-closed].freeze
21
+
22
+ NOTIFY_TMUX_CONF_CONTENT = <<~'CONF'
23
+ # --- Notify hooks (clean up notifications when panes/windows/sessions close) ---
24
+ set-hook -g after-kill-pane "run-shell -b 'h notify remove_pane \#{hook_pane_id}'"
25
+ set-hook -g window-unlinked "run-shell -b 'h notify remove_window \#{hook_window_id}'"
26
+ set-hook -g session-closed "run-shell -b 'h notify remove_session \#{hook_session_name}'"
27
+
28
+ # --- Notify keybinding (prefix + N to open notification menu) ---
29
+ bind-key N run-shell "h notify menu"
30
+ CONF
31
+
32
+ CLAUDE_NOTIFICATION_CMD = %q(MSG=$(cat | jq -r '.message // "Needs input"'); h alert -t 'Claude Code' -m "$MSG" -s k -c "echo switchc -t '$TMUX_PANE' | pbcopy"; h notify push -t info "$MSG")
33
+ CLAUDE_STOP_CMD = %q(h alert -t 'Claude Code' -m 'Work completed' -s b -c "echo switchc -t '$TMUX_PANE' | pbcopy"; h notify push -t success 'Work completed')
34
+
16
35
  def read_log
17
36
  return {} unless File.exist?(LOG_FILE)
18
37
  YAML.safe_load(File.read(LOG_FILE)) || {}
@@ -26,6 +45,16 @@ def write_log(data)
26
45
  File.write(LOG_FILE, data.to_yaml)
27
46
  end
28
47
 
48
+ def current_pane
49
+ # Prefer $TMUX_PANE env var — it's reliably set even in hook subprocesses
50
+ env_id = ENV['TMUX_PANE'].to_s
51
+ if !env_id.empty?
52
+ Hiiro::Tmux::Panes.fetch(all: true).find_by_id(env_id)
53
+ else
54
+ Hiiro::Tmux::Pane.current
55
+ end
56
+ end
57
+
29
58
  def pane_exists?(pane_id)
30
59
  Hiiro::Tmux::Panes.fetch(all: true).find_by_id(pane_id)
31
60
  end
@@ -44,7 +73,7 @@ Hiiro.run(*ARGV, tasks: true) do
44
73
  next
45
74
  end
46
75
 
47
- pane = Hiiro::Tmux::Pane.current
76
+ pane = current_pane
48
77
  session = pane&.session_name || Hiiro::Tmux::Session.current&.name
49
78
 
50
79
  unless pane && session
@@ -64,7 +93,7 @@ Hiiro.run(*ARGV, tasks: true) do
64
93
 
65
94
  data = read_log
66
95
  data[session] ||= []
67
- # One entry per pane — replace existing entry for this pane, newest first
96
+ # One entry per pane — replace existing, newest first
68
97
  data[session].reject! { |e| e['pane_id'] == pane.id }
69
98
  data[session].unshift(entry)
70
99
  write_log(data)
@@ -167,7 +196,7 @@ Hiiro.run(*ARGV, tasks: true) do
167
196
  tmux_client.display_message('Notifications cleared')
168
197
  end
169
198
 
170
- # Called by tmux hook: after-kill-pane / pane-died
199
+ # Called by tmux hook: after-kill-pane
171
200
  add_subcmd(:remove_pane) do |pane_id = nil|
172
201
  next unless pane_id
173
202
  data = read_log
@@ -175,7 +204,7 @@ Hiiro.run(*ARGV, tasks: true) do
175
204
  write_log(data)
176
205
  end
177
206
 
178
- # Called by tmux hook: window-closed
207
+ # Called by tmux hook: window-unlinked
179
208
  add_subcmd(:remove_window) do |window_id = nil|
180
209
  next unless window_id
181
210
  data = read_log
@@ -191,78 +220,134 @@ Hiiro.run(*ARGV, tasks: true) do
191
220
  write_log(data)
192
221
  end
193
222
 
194
- add_subcmd(:setup) do
195
- conf_dir = File.join(Dir.home, '.config', 'tmux')
196
- conf_file = File.join(conf_dir, 'h-notify.tmux.conf')
197
- tmux_conf = File.join(Dir.home, '.tmux.conf')
198
- source_line = "source-file #{conf_file}"
199
-
200
- FileUtils.mkdir_p(conf_dir)
201
-
202
- notify_conf = <<~'CONF'
203
- # --- Notify hooks (clean up notifications when panes/windows/sessions close) ---
204
- set-hook -g after-kill-pane "run-shell -b 'h notify remove_pane \#{hook_pane_id}'"
205
- set-hook -g window-unlinked "run-shell -b 'h notify remove_window \#{hook_window_id}'"
206
- set-hook -g session-closed "run-shell -b 'h notify remove_session \#{hook_session_name}'"
207
-
208
- # --- Notify keybinding (prefix + N to open notification menu) ---
209
- bind-key N run-shell "h notify menu"
210
- CONF
211
-
212
- File.write(conf_file, notify_conf)
213
- puts "Wrote #{conf_file}"
214
-
215
- if File.exist?(tmux_conf)
216
- existing = File.read(tmux_conf)
217
- if existing.include?(source_line)
218
- puts "#{tmux_conf} already sources #{conf_file}"
219
- else
220
- File.open(tmux_conf, 'a') { |f| f.puts; f.puts source_line }
221
- puts "Added '#{source_line}' to #{tmux_conf}"
223
+ # --- h notify tmux ---
224
+
225
+ add_subcmd(:tmux) do
226
+ make_child {
227
+ add_subcmd(:setup) do
228
+ FileUtils.mkdir_p(File.dirname(NOTIFY_CONF))
229
+ File.write(NOTIFY_CONF, NOTIFY_TMUX_CONF_CONTENT)
230
+ puts "Wrote #{NOTIFY_CONF}"
222
231
  end
223
- else
224
- File.write(tmux_conf, source_line + "\n")
225
- puts "Created #{tmux_conf} with source line"
226
- end
227
232
 
228
- puts
229
- puts "Reload tmux config:"
230
- puts " tmux source-file ~/.tmux.conf"
231
- puts
232
- puts "─────────────────────────────────────────────────"
233
- puts "Claude Code hook setup"
234
- puts "─────────────────────────────────────────────────"
235
- puts
236
- puts "Run this to auto-update ~/.claude/settings.json:"
237
- puts " h notify update_hooks"
238
- puts
239
- puts "Or manually update hooks to:"
240
- puts
241
- puts ' Notification: h alert ... ; h notify push -t info "$MSG"'
242
- puts ' Stop: h alert ... ; h notify push -t success "Work completed"'
233
+ add_subcmd(:add_hooks) do
234
+ source_line = "source-file #{NOTIFY_CONF}"
235
+
236
+ if File.exist?(TMUX_CONF)
237
+ existing = File.read(TMUX_CONF)
238
+ if existing.include?(source_line)
239
+ puts "#{TMUX_CONF} already sources #{NOTIFY_CONF}"
240
+ else
241
+ File.open(TMUX_CONF, 'a') { |f| f.puts; f.puts source_line }
242
+ puts "Added '#{source_line}' to #{TMUX_CONF}"
243
+ end
244
+ else
245
+ File.write(TMUX_CONF, source_line + "\n")
246
+ puts "Created #{TMUX_CONF} with source line"
247
+ end
248
+
249
+ system('tmux', 'source-file', TMUX_CONF)
250
+ puts "Reloaded #{TMUX_CONF}"
251
+ end
252
+
253
+ add_subcmd(:reset_hooks) do
254
+ NOTIFY_TMUX_HOOKS.each do |hook|
255
+ system('tmux', 'set-hook', '-gu', hook)
256
+ puts "Unset tmux hook: #{hook}"
257
+ end
258
+ end
259
+
260
+ add_subcmd(:load_hooks) do
261
+ system('tmux', 'source-file', TMUX_CONF)
262
+ puts "Sourced #{TMUX_CONF}"
263
+ end
264
+ }.run
243
265
  end
244
266
 
245
- add_subcmd(:update_hooks) do
246
- settings_file = File.join(Dir.home, '.claude', 'settings.json')
267
+ # --- h notify claude ---
247
268
 
248
- unless File.exist?(settings_file)
249
- puts "#{settings_file} not found"
250
- next
269
+ add_subcmd(:claude) do
270
+ require 'json'
271
+
272
+ read_claude_settings = lambda do
273
+ return {} unless File.exist?(CLAUDE_SETTINGS)
274
+ JSON.parse(File.read(CLAUDE_SETTINGS))
275
+ rescue JSON::ParserError
276
+ {}
251
277
  end
252
278
 
253
- require 'json'
254
- settings = JSON.parse(File.read(settings_file))
255
- settings['hooks'] ||= {}
279
+ write_claude_settings = lambda do |settings|
280
+ File.write(CLAUDE_SETTINGS, JSON.pretty_generate(settings))
281
+ end
282
+
283
+ make_child {
284
+ add_subcmd(:setup) do
285
+ settings = read_claude_settings.call
286
+ settings['hooks'] ||= {}
287
+ settings['hooks']['Notification'] = [{ 'hooks' => [{ 'type' => 'command', 'command' => CLAUDE_NOTIFICATION_CMD }] }]
288
+ settings['hooks']['Stop'] = [{ 'hooks' => [{ 'type' => 'command', 'command' => CLAUDE_STOP_CMD }] }]
289
+ write_claude_settings.call(settings)
290
+ puts "Updated #{CLAUDE_SETTINGS}"
291
+ puts " Notification -> h alert + h notify push -t info"
292
+ puts " Stop -> h alert + h notify push -t success"
293
+ end
256
294
 
257
- notification_cmd = %q(MSG=$(cat | jq -r '.message // "Needs input"'); h alert -t 'Claude Code' -m "$MSG" -s k -c "echo switchc -t '$TMUX_PANE' | pbcopy"; h notify push -t info "$MSG")
258
- stop_cmd = %q(h alert -t 'Claude Code' -m 'Work completed' -s b -c "echo switchc -t '$TMUX_PANE' | pbcopy"; h notify push -t success 'Work completed')
295
+ add_subcmd(:add_hooks) do
296
+ settings = read_claude_settings.call
297
+ unless File.exist?(CLAUDE_SETTINGS)
298
+ puts "#{CLAUDE_SETTINGS} not found"
299
+ next
300
+ end
301
+
302
+ settings['hooks'] ||= {}
303
+
304
+ # Inject h notify push into existing commands if not already present,
305
+ # otherwise write fresh hooks
306
+ %w[Notification Stop].each do |event|
307
+ existing_hooks = settings.dig('hooks', event, 0, 'hooks') || []
308
+ if existing_hooks.any? { |h| h['command']&.include?('h notify push') }
309
+ puts "#{event} hook already includes h notify push — skipping"
310
+ next
311
+ end
312
+
313
+ cmd = event == 'Notification' ? CLAUDE_NOTIFICATION_CMD : CLAUDE_STOP_CMD
314
+ settings['hooks'][event] = [{ 'hooks' => [{ 'type' => 'command', 'command' => cmd }] }]
315
+ puts "Set #{event} hook -> h alert + h notify push"
316
+ end
317
+
318
+ write_claude_settings.call(settings)
319
+ end
259
320
 
260
- settings['hooks']['Notification'] = [{ 'hooks' => [{ 'type' => 'command', 'command' => notification_cmd }] }]
261
- settings['hooks']['Stop'] = [{ 'hooks' => [{ 'type' => 'command', 'command' => stop_cmd }] }]
321
+ add_subcmd(:reset_hooks) do
322
+ unless File.exist?(CLAUDE_SETTINGS)
323
+ puts "#{CLAUDE_SETTINGS} not found"
324
+ next
325
+ end
326
+
327
+ settings = read_claude_settings.call
328
+ settings['hooks'] ||= {}
329
+
330
+ %w[Notification Stop].each do |event|
331
+ hooks = settings.dig('hooks', event, 0, 'hooks') || []
332
+ hooks.each do |h|
333
+ next unless h['command']&.include?('h notify push')
334
+ # Strip the h notify push portion from the command
335
+ h['command'] = h['command']
336
+ .split(';')
337
+ .reject { |part| part.strip.start_with?('h notify push') }
338
+ .join(';')
339
+ .strip
340
+ end
341
+ puts "Stripped h notify push from #{event} hook"
342
+ end
343
+
344
+ write_claude_settings.call(settings)
345
+ end
262
346
 
263
- File.write(settings_file, JSON.pretty_generate(settings))
264
- puts "Updated #{settings_file}"
265
- puts " Notification -> h alert + h notify push -t info"
266
- puts " Stop -> h alert + h notify push -t success"
347
+ add_subcmd(:load_hooks) do
348
+ puts "Claude Code settings are loaded automatically on startup."
349
+ puts "Restart claude to pick up changes to #{CLAUDE_SETTINGS}"
350
+ end
351
+ }.run
267
352
  end
268
353
  end
data/bin/h-pr CHANGED
@@ -1071,6 +1071,81 @@ Hiiro.run(*ARGV, plugins: [Pins]) do
1071
1071
  system('gh', 'pr', 'merge', pr_number.to_s, *merge_args)
1072
1072
  end
1073
1073
 
1074
+ add_subcmd(:fix) do |*fix_args|
1075
+ opts = Hiiro::Options.parse(fix_args) do
1076
+ flag(:red, short: :r, desc: 'Fix all PRs with failing checks')
1077
+ flag(:run, short: :R, desc: 'Launch queue tasks immediately after queuing')
1078
+ end
1079
+
1080
+ pinned = pinned_manager.load_pinned
1081
+
1082
+ prs_to_fix = if opts.red
1083
+ failing = pinned.select { |pr| (c = pr['checks']) && c['failed'].to_i > 0 }
1084
+ if failing.empty?
1085
+ puts "No PRs with failing checks"
1086
+ next
1087
+ end
1088
+ failing
1089
+ elsif opts.args.empty?
1090
+ # Default: fuzzy-select from failing PRs, fall back to all tracked
1091
+ failing = pinned.select { |pr| (c = pr['checks']) && c['failed'].to_i > 0 }
1092
+ candidates = failing.empty? ? pinned : failing
1093
+ if candidates.empty?
1094
+ puts "No tracked PRs"
1095
+ next
1096
+ end
1097
+ lines = candidates.each_with_index.each_with_object({}) do |(pr, idx), h|
1098
+ h[pinned_manager.display_pinned(pr, idx)] = pr
1099
+ end
1100
+ selected = fuzzyfind_from_map(lines)
1101
+ next unless selected
1102
+ [selected]
1103
+ else
1104
+ # Resolve each positional arg as a PR ref
1105
+ opts.args.map { |ref|
1106
+ pr_num = resolve_pr.call(ref)
1107
+ next nil unless pr_num
1108
+ pinned.find { |p| p['number'].to_s == pr_num.to_s }
1109
+ }.compact
1110
+ end
1111
+
1112
+ if prs_to_fix.empty?
1113
+ puts "No PRs to fix"
1114
+ next
1115
+ end
1116
+
1117
+ q = Hiiro::Queue.current(self)
1118
+ queued_names = []
1119
+
1120
+ prs_to_fix.each do |pr|
1121
+ task_info = {
1122
+ task_name: pr['task'],
1123
+ tree_name: pr['worktree'],
1124
+ session_name: pr['tmux_session'],
1125
+ }.compact
1126
+ task_info = nil if task_info.empty?
1127
+
1128
+ result = q.add_with_frontmatter("/pr:fix #{pr['number']}", task_info: task_info)
1129
+ if result
1130
+ puts "Queued fix for ##{pr['number']}: #{pr['title']}"
1131
+ queued_names << result[:name]
1132
+ else
1133
+ STDERR.puts "Failed to queue fix for ##{pr['number']}"
1134
+ end
1135
+ end
1136
+
1137
+ unless queued_names.empty?
1138
+ puts
1139
+ puts "Queued #{queued_names.length} fix task(s)."
1140
+ if opts.run
1141
+ puts "Launching..."
1142
+ queued_names.each { |name| q.launch_task(name) }
1143
+ else
1144
+ puts "Run 'h queue run' to start."
1145
+ end
1146
+ end
1147
+ end
1148
+
1074
1149
  add_subcmd(:comment) do |ref = nil|
1075
1150
  pr_number = resolve_pr.call(ref)
1076
1151
  next unless pr_number
data/lib/hiiro/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  class Hiiro
2
- VERSION = "0.1.230"
2
+ VERSION = "0.1.232"
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.230
4
+ version: 0.1.232
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Toyota