hiiro 0.1.201 → 0.1.203

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: 0161eeaf4f0cbe1052c13d9373ab9ed268394d1895f498fd5b964ba363b017d9
4
- data.tar.gz: 559024c602f5667a1ae23b3d9d917295fe676194c17e24489d3047f16cb1b50d
3
+ metadata.gz: 8c0bb6c6511776861d3adb6a22dbc07343bade8517cdbef483dd6696e2695911
4
+ data.tar.gz: 6367f7818b192365e44b7109d1199583c07f23bd3a08890019b26cc3a0e5dc5b
5
5
  SHA512:
6
- metadata.gz: 85941f5cf69ef053cc6ae8a08ef2b1036313ee8b60413243009cbd85311c2fcb8823b52c11780dd7b4fdf2785027440a86373146673dbb76e9ab2b3de819d773
7
- data.tar.gz: 8cba796136692b6cd26cc372a42d429d422a7a141414939cc383212f4fa9a491b1a94a05cb0383fc2db27d503c4d63ad00be87bd49c63e08b52b5e4458acc3a6
6
+ metadata.gz: 3fe17a443379a9aec58acbd7f9f664cc363c508dbb87014f964ee4c4e1dd12e3c9be3236e7af7d8e96116ada68eb28018be1337da355d8d2a04709a948dd3a04
7
+ data.tar.gz: 8a731d6af3505960bd6683b5b439cb60cc839a54cdc307ebe9decb989e84b23860f7a50d244edb7dbd29857b9e4742548c153c6758d5fa85272b82c55820d2e1
data/bin/h-pr CHANGED
@@ -195,7 +195,17 @@ class PinnedPRManager
195
195
 
196
196
  def load_pinned
197
197
  ensure_file
198
- YAML.load_file(PINNED_FILE) || []
198
+ prs = YAML.load_file(PINNED_FILE) || []
199
+ if prs.any? { |p| !p['slot'] }
200
+ next_slot = prs.map { |p| p['slot'].to_i }.max.to_i
201
+ prs.each do |p|
202
+ next if p['slot']
203
+ next_slot += 1
204
+ p['slot'] = next_slot
205
+ end
206
+ save_pinned(prs)
207
+ end
208
+ prs
199
209
  end
200
210
 
201
211
  def save_pinned(prs)
@@ -215,6 +225,7 @@ class PinnedPRManager
215
225
  existing.merge!(pr_info)
216
226
  existing['updated_at'] = Time.now.iso8601
217
227
  else
228
+ pr_info['slot'] = (pinned.map { |p| p['slot'].to_i }.max.to_i) + 1
218
229
  pr_info['pinned_at'] = Time.now.iso8601
219
230
  pinned << pr_info
220
231
  end
@@ -485,7 +496,7 @@ class PinnedPRManager
485
496
  end
486
497
 
487
498
  def display_pinned(pr, idx = nil)
488
- num = idx ? "#{(idx + 1).to_s.rjust(3)}." : ""
499
+ num = idx ? "#{(pr['slot'] || idx + 1).to_s.rjust(3)}." : ""
489
500
 
490
501
  check_emoji = if pr['checks']
491
502
  c = pr['checks']
@@ -634,7 +645,11 @@ Hiiro.run(*ARGV, plugins: [Pins]) do
634
645
  end
635
646
 
636
647
  if ref.to_s =~ /^\d+$/ && ref.to_i > 0
637
- idx = ref.to_i - 1
648
+ slot = ref.to_i
649
+ by_slot = pinned.find { |p| p['slot'].to_i == slot }
650
+ return by_slot['number'].to_s if by_slot
651
+ # Fall back to 1-based index for unslotted data
652
+ idx = slot - 1
638
653
  return pinned[idx]['number'].to_s if idx < pinned.length
639
654
  end
640
655
 
@@ -688,15 +703,23 @@ Hiiro.run(*ARGV, plugins: [Pins]) do
688
703
  end
689
704
  }
690
705
 
691
- add_subcmd(:link) { |*args|
692
- stdout = `gh pr view`
706
+ add_subcmd(:link) { |ref=nil|
707
+ pr_number = resolve_pr.call(ref)
708
+ next unless pr_number
693
709
 
694
- number = stdout[/number:\s*(\d+)/, 1]
710
+ pinned = pinned_manager.load_pinned
711
+ pr = pinned.find { |p| p['number'].to_s == pr_number.to_s }
695
712
 
696
- if number
697
- print ['https://github.com/instacart/carrot/pull/', number].join
713
+ if pr && pr['url']
714
+ print pr['url']
698
715
  else
699
- puts stdout
716
+ output = `gh pr view #{pr_number} --json url 2>/dev/null`
717
+ info = JSON.parse(output) rescue nil
718
+ if info&.dig('url')
719
+ print info['url']
720
+ else
721
+ STDERR.puts "Could not find URL for PR ##{pr_number}"
722
+ end
700
723
  end
701
724
  }
702
725
 
@@ -1127,6 +1150,68 @@ Hiiro.run(*ARGV, plugins: [Pins]) do
1127
1150
  puts "Added #{added} PR(s) to tracking."
1128
1151
  end
1129
1152
 
1153
+ add_subcmd(:attach) do |ref = nil|
1154
+ pr_number = resolve_pr.call(ref)
1155
+ next unless pr_number
1156
+
1157
+ pinned = pinned_manager.load_pinned
1158
+ pr = pinned.find { |p| p['number'].to_s == pr_number.to_s }
1159
+
1160
+ unless pr
1161
+ puts "PR ##{pr_number} not in pinned list — run 'h pr update' first"
1162
+ next
1163
+ end
1164
+
1165
+ branch = pr['headRefName']
1166
+ task_name = pr['task']
1167
+
1168
+ unless branch
1169
+ puts "PR ##{pr_number} has no branch info"
1170
+ next
1171
+ end
1172
+
1173
+ # Resolve task session and working directory
1174
+ target_session = nil
1175
+ working_dir = Dir.pwd
1176
+
1177
+ if task_name
1178
+ env = Environment.current rescue nil
1179
+ task = env&.find_task(task_name)
1180
+ if task
1181
+ target_session = task.session_name
1182
+ tree = task.tree
1183
+ working_dir = tree.path if tree
1184
+ end
1185
+ end
1186
+
1187
+ unless target_session && system('tmux', 'has-session', '-t', target_session, out: File::NULL, err: File::NULL)
1188
+ puts "Task '#{task_name || '(none)'}' has no active tmux session"
1189
+ next
1190
+ end
1191
+
1192
+ # Write a launcher: commit WIP if dirty, checkout the PR branch, then drop into shell
1193
+ script_path = "/tmp/h-pr-attach-#{pr_number}.sh"
1194
+ File.write(script_path, <<~SH)
1195
+ #!/usr/bin/env bash
1196
+ cd #{Shellwords.shellescape(working_dir)}
1197
+
1198
+ if [ -n "$(git status --porcelain)" ]; then
1199
+ git add -A
1200
+ git commit -m "WIP"
1201
+ echo ""
1202
+ echo "WIP COMMIT: ALL UNCOMMITTED CHANGES HAVE BEEN COMMITTED BEFORE SWITCHING BRANCHES"
1203
+ echo ""
1204
+ fi
1205
+
1206
+ git checkout #{Shellwords.shellescape(branch)}
1207
+ exec #{Shellwords.shellescape(ENV['SHELL'] || 'zsh')}
1208
+ SH
1209
+ FileUtils.chmod(0755, script_path)
1210
+
1211
+ system('tmux', 'new-window', '-t', target_session, '-c', working_dir, script_path)
1212
+ system('tmux', 'switch-client', '-t', target_session)
1213
+ end
1214
+
1130
1215
  add_subcmd(:track) do
1131
1216
  pr_info = pinned_manager.fetch_current_branch_pr
1132
1217
  unless pr_info
data/lib/hiiro/queue.rb CHANGED
@@ -40,6 +40,50 @@ class Hiiro
40
40
  Dir.glob(File.join(dir, '*.md')).sort.map { |f| File.basename(f, '.md') }
41
41
  end
42
42
 
43
+ def tasks_in_sorted(status)
44
+ dir = queue_dirs[status]
45
+ Dir.glob(File.join(dir, '*.md')).map { |f|
46
+ { name: File.basename(f, '.md'), mtime: File.mtime(f) }
47
+ }.sort_by { |t| -t[:mtime].to_i }
48
+ end
49
+
50
+ def format_mtime(mtime)
51
+ now = Time.now
52
+ mtime.year == now.year ? mtime.strftime("%m-%d %H:%M") : mtime.strftime("%Y-%m-%d %H:%M")
53
+ end
54
+
55
+ def list_lines(all: false)
56
+ lines = []
57
+ STATUSES.each do |status|
58
+ tasks = tasks_in_sorted(status.to_sym)
59
+ next if tasks.empty?
60
+
61
+ display = all ? tasks : tasks.first(10)
62
+ display.each do |t|
63
+ ts = format_mtime(t[:mtime])
64
+ line = "%-10s %-12s %s" % [status, ts, t[:name]]
65
+ meta = meta_for(t[:name], status.to_sym)
66
+ if meta && status == 'running'
67
+ started = meta['started_at']
68
+ if started
69
+ elapsed = Time.now - Time.parse(started)
70
+ mins = (elapsed / 60).to_i
71
+ line += " (#{mins}m)"
72
+ end
73
+ line += " [#{meta['tmux_session']}:#{meta['tmux_window']}]" if meta['tmux_session']
74
+ end
75
+ preview = task_preview(t[:name], status.to_sym)
76
+ line += " #{preview}" if preview
77
+ lines << line
78
+ end
79
+
80
+ if !all && tasks.size > 10
81
+ lines << " ... and #{tasks.size - 10} more"
82
+ end
83
+ end
84
+ lines
85
+ end
86
+
43
87
  def all_tasks
44
88
  STATUSES.flat_map do |status|
45
89
  tasks_in(status.to_sym).map { |name| { name: name, status: status } }
@@ -315,54 +359,30 @@ class Hiiro
315
359
  end
316
360
  }
317
361
 
318
- h.add_subcmd(:ls) {
319
- tasks = q.all_tasks
320
- if tasks.empty?
321
- puts "No tasks"
322
- next
323
- end
324
- tasks.each do |t|
325
- line = "%-10s %s" % [t[:status], t[:name]]
326
- meta = q.meta_for(t[:name], t[:status].to_sym)
327
- if meta && t[:status] == 'running'
328
- started = meta['started_at']
329
- if started
330
- elapsed = Time.now - Time.parse(started)
331
- mins = (elapsed / 60).to_i
332
- line += " (#{mins}m)"
333
- end
334
- line += " [#{meta['tmux_session']}:#{meta['tmux_window']}]" if meta['tmux_session']
335
- end
336
- preview = q.task_preview(t[:name], t[:status].to_sym)
337
- line += " #{preview}" if preview
338
- puts line
362
+ list_cmd = proc { |*args|
363
+ opts = Hiiro::Options.parse(args) do
364
+ flag(:all, short: :a, desc: 'Show all tasks without limit; use pager if output exceeds terminal height')
339
365
  end
340
- }
341
-
342
- h.add_subcmd(:list) {
343
- tasks = q.all_tasks
344
- if tasks.empty?
366
+ lines = q.list_lines(all: opts.all)
367
+ if lines.empty?
345
368
  puts "No tasks"
346
369
  next
347
370
  end
348
- tasks.each do |t|
349
- line = "%-10s %s" % [t[:status], t[:name]]
350
- meta = q.meta_for(t[:name], t[:status].to_sym)
351
- if meta && t[:status] == 'running'
352
- started = meta['started_at']
353
- if started
354
- elapsed = Time.now - Time.parse(started)
355
- mins = (elapsed / 60).to_i
356
- line += " (#{mins}m)"
357
- end
358
- line += " [#{meta['tmux_session']}:#{meta['tmux_window']}]" if meta['tmux_session']
371
+ if opts.all
372
+ terminal_lines = ENV['LINES']&.to_i || 24
373
+ if lines.size > terminal_lines
374
+ IO.popen(ENV['PAGER'] || 'less', 'w') { |io| io.puts lines }
375
+ else
376
+ puts lines
359
377
  end
360
- preview = q.task_preview(t[:name], t[:status].to_sym)
361
- line += " #{preview}" if preview
362
- puts line
378
+ else
379
+ puts lines
363
380
  end
364
381
  }
365
382
 
383
+ h.add_subcmd(:ls, &list_cmd)
384
+ h.add_subcmd(:list, &list_cmd)
385
+
366
386
  h.add_subcmd(:status) {
367
387
  tasks = q.all_tasks
368
388
  if tasks.empty?
data/lib/hiiro/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  class Hiiro
2
- VERSION = "0.1.201"
2
+ VERSION = "0.1.203"
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.201
4
+ version: 0.1.203
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Toyota