hiiro 0.1.342 → 0.1.344

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: 1496be512bebac30ab2a5fc84922bf0eefb05b3ff1632b04a6488142c805b388
4
- data.tar.gz: 215d5c29421e45abd0b8c5fe2984e372d776e5510cab0d71cd178e1c390081fe
3
+ metadata.gz: dfbd49f6a3303a03b9568647851914d4a1c30e7c81a6f9aa8ef5d9808728769d
4
+ data.tar.gz: 1245ec1dcc474ff962d48405c357ac315734c30a2cc573697e5c28ee3b35057f
5
5
  SHA512:
6
- metadata.gz: 3a25d21e45c9bdea4c7ca56a850f6f00b71fb62990a03eb545f0bc379a6881504bf213079ef25f50df99f9a624ae2571f1d3522089f3a837439b363ab8bbbc61
7
- data.tar.gz: a17b9aeccf2740727d1f4f306a4ad446b74cc7cf996f062bb6d8f37eef74660e84162ea9a3edf1542004412e86d2d2b547d81cc1b66493860afe6e018da1c6ad
6
+ metadata.gz: dfaa031a3b826d422c64f8378108278cc845d4ec09a26da1f37fa85da34edb3c326c919d612873c1213331883cead770cd684d688ab521d38ec77dd50645d273
7
+ data.tar.gz: 1d0eb4479c705c13f898da37389b65d60104830d5daae7e9af38dba289cf871feeef60d8b6efd7fe9461cdfa490ba00a6fa53303520f4214b7d28ff0868a3faa
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.343] - 2026-04-12
4
+
5
+ ### Added
6
+ - Task subcommands now fall back to `~/proj/*` directories when no task matches
7
+ - `h task path hiiro` → resolves to `~/proj/hiiro` if no task named "hiiro" exists
8
+ - Works for: `cd`, `path`, `sh`, `branch`, `tree`, `session`, and any subcommand using `-t` flag
9
+ - `FallbackTarget` class duck-types as `Task` for seamless integration
10
+ - Ambiguous project matches print a warning to stderr
11
+
12
+ ### Fixed
13
+ - `Hiiro::Git::Pr.is_link?` is now a class method (was instance method)
14
+
3
15
  ## [0.1.342] - 2026-04-08
4
16
 
5
17
  ### Added
data/lib/hiiro/git/pr.rb CHANGED
@@ -20,7 +20,7 @@ class Hiiro
20
20
  []
21
21
  end
22
22
 
23
- def is_link?(link)
23
+ def self.is_link?(link)
24
24
  temp_link = link.to_s
25
25
  return false unless temp_link.match?('github.com') && temp_link.match?(/pull\/[0-9]+/)
26
26
 
data/lib/hiiro/shell.rb CHANGED
@@ -22,5 +22,119 @@ class Hiiro
22
22
 
23
23
  selected&.chomp
24
24
  end
25
+
26
+ def self.run(*command, **env)
27
+ env = env.transform_keys(&:to_s).transform_values(&:to_s)
28
+ stdout, status = Open3.capture2(env, *command)
29
+ Result.new(stdout, status)
30
+ end
31
+
32
+ def self.run_combined(*command, **env)
33
+ env = env.transform_keys(&:to_s).transform_values(&:to_s)
34
+ output, status = Open3.capture2e(env, *command)
35
+ Result.new(output, status)
36
+ end
37
+
38
+ def self.run3(*command, **env)
39
+ env = env.transform_keys(&:to_s).transform_values(&:to_s)
40
+ stdout, stderr, status = Open3.capture3(env, *command)
41
+ Result.new(stdout, status, stderr: stderr)
42
+ end
43
+
44
+ # Stream stdout live to $stdout as chunks arrive, buffering for Result.
45
+ # stderr passes through to the parent process (not captured).
46
+ #
47
+ # ~/bin/h-tds does this manually with popen2e for Chromatic:
48
+ #
49
+ # Open3.popen2e('unbuffer pnpm chromatic') do |stdin, stdout_err, wait_thr|
50
+ # stdin.close
51
+ # loop do
52
+ # chunk = stdout_err.readpartial(4096)
53
+ # $stdout.write(chunk)
54
+ # @output << chunk
55
+ # rescue EOFError
56
+ # break
57
+ # end
58
+ # @exit_status = wait_thr.value
59
+ # end
60
+ #
61
+ # stream_combined replaces that pattern — stderr merged in, one Result back.
62
+ def self.stream(*command, tee: $stdout, **env)
63
+ env = env.transform_keys(&:to_s).transform_values(&:to_s)
64
+ Open3.popen2(env, *command) do |stdin, stdout, wait_thr|
65
+ stdin.close
66
+ return Result.collect_chunks(stdout, wait_thr, tee: tee)
67
+ end
68
+ end
69
+
70
+ def self.stream_combined(*command, tee: $stdout, **env)
71
+ env = env.transform_keys(&:to_s).transform_values(&:to_s)
72
+ Open3.popen2e(env, *command) do |stdin, stdout_err, wait_thr|
73
+ stdin.close
74
+ return Result.collect_chunks(stdout_err, wait_thr, tee: tee)
75
+ end
76
+ end
77
+ end
78
+
79
+ class Result
80
+ # Matches the full ANSI/VT100 escape sequence spec:
81
+ # cursor movement, erase, colors, SGR — everything a terminal interprets.
82
+ # [^[] catches all single-char Fe sequences (e.g. \eM, \eD); \[...] catches CSI.
83
+ ANSI_PATTERN = /\e(?:\[[0-?]*[ -/]*[@-~]|[^[])/
84
+
85
+ attr_reader :stdout, :stderr, :status
86
+
87
+ def initialize(stdout, status, stderr: nil)
88
+ @stdout = stdout
89
+ @stderr = stderr
90
+ @status = status
91
+ end
92
+
93
+ def success?
94
+ status.success?
95
+ end
96
+
97
+ def failed?
98
+ !success?
99
+ end
100
+
101
+ def exit_code
102
+ status.exitstatus
103
+ end
104
+
105
+ # Replay buffered output to the terminal with ANSI sequences intact —
106
+ # colors, cursor movement, line clears all work as they did live.
107
+ # Returns self so you can chain: result.display.lines
108
+ def display(out: $stdout)
109
+ out.write(stdout)
110
+ out.flush
111
+ self
112
+ end
113
+
114
+ # Strip ANSI escape codes for text processing or filtering.
115
+ # Also strips bare \r (carriage-return-only, used by progress bars).
116
+ def plain_text
117
+ stdout.gsub(ANSI_PATTERN, '').gsub(/\r(?!\n)/, '')
118
+ end
119
+
120
+ # Plain text split into non-empty lines.
121
+ def lines
122
+ plain_text.split("\n").reject(&:empty?)
123
+ end
124
+
125
+ # Factory: reads chunks from an IO (popen handle), optionally tee-ing
126
+ # each chunk to `tee` as it arrives. Used by Shell.stream / stream_combined.
127
+ # Pass tee: nil to capture without printing.
128
+ def self.collect_chunks(io, wait_thr, tee: $stdout)
129
+ output = +""
130
+ loop do
131
+ chunk = io.readpartial(4096)
132
+ tee&.write(chunk)
133
+ output << chunk
134
+ rescue EOFError
135
+ break
136
+ end
137
+ new(output, wait_thr.value)
138
+ end
25
139
  end
26
140
  end
data/lib/hiiro/tasks.rb CHANGED
@@ -63,6 +63,37 @@ class Hiiro
63
63
  Hiiro::Matcher.new(tasks, key).by_prefix(name).first&.item
64
64
  end
65
65
 
66
+ # Resolve a name to a Task or FallbackTarget (~/proj/* directory).
67
+ # Falls back to ~/proj/* prefix match when no task matches.
68
+ def resolve_name(name)
69
+ return nil if name.nil?
70
+
71
+ task = task_by_name(name)
72
+ return task if task
73
+
74
+ proj_dirs = Dir.glob(File.expand_path('~/proj/*/'))
75
+ dir_names = proj_dirs.map { |d| File.basename(d) }
76
+ result = Hiiro::Matcher.by_prefix(dir_names, name)
77
+ if result.one?
78
+ dir_name = result.first.item
79
+ return FallbackTarget.from_project(dir_name, File.expand_path("~/proj/#{dir_name}"))
80
+ elsif result.ambiguous?
81
+ STDERR.puts "Ambiguous project match for '#{name}': #{result.matches.map(&:item).join(', ')}"
82
+ end
83
+
84
+ nil
85
+ end
86
+
87
+ # Resolve the filesystem path for a Task or FallbackTarget.
88
+ def resolve_path(target)
89
+ return nil unless target
90
+
91
+ return target.path if target.is_a?(FallbackTarget)
92
+
93
+ tree = environment.find_tree(target.tree_name)
94
+ tree ? tree.path : File.join(Hiiro::WORK_DIR, target.tree_name)
95
+ end
96
+
66
97
  def task_by_tree(tree_name)
67
98
  environment.task_matcher.resolve(tree_name, :tree_name).resolved&.item
68
99
  end
@@ -499,8 +530,8 @@ class Hiiro
499
530
 
500
531
  def value_for_task(task_name = nil, &block)
501
532
  if task_name
502
- task = task_by_name(task_name)
503
- return block.call(task) if task
533
+ target = resolve_name(task_name)
534
+ return block.call(target) if target
504
535
  end
505
536
 
506
537
  task_list = scope == :subtask ? tasks.sort_by(&:short_name) : environment.all_tasks.sort_by(&:name)
@@ -769,9 +800,9 @@ class Hiiro
769
800
  task = sel.is_a?(Hiiro::Task) ? sel : nil
770
801
  [task, positional]
771
802
  elsif opts.task
772
- [tm.task_by_name(opts.task), positional]
803
+ [tm.resolve_name(opts.task), positional]
773
804
  elsif positional.any?
774
- found = tm.task_by_name(positional.first)
805
+ found = tm.resolve_name(positional.first)
775
806
  found ? [found, positional[1..]] : [tm.current_task, positional]
776
807
  else
777
808
  [tm.current_task, positional]
@@ -1001,8 +1032,7 @@ class Hiiro
1001
1032
  puts "No task found"
1002
1033
  next
1003
1034
  end
1004
- tree = tm.environment.find_tree(task.tree_name)
1005
- tree_path = tree ? tree.path : File.join(Hiiro::WORK_DIR, task.tree_name)
1035
+ tree_path = tm.resolve_path(task)
1006
1036
  app_name = positional[0]
1007
1037
  if app_name.nil?
1008
1038
  tm.send_cd(tree_path)
@@ -1023,8 +1053,7 @@ class Hiiro
1023
1053
  STDERR.puts "No task found"
1024
1054
  next
1025
1055
  end
1026
- tree = tm.environment.find_tree(task.tree_name)
1027
- tree_path = tree ? tree.path : File.join(Hiiro::WORK_DIR, task.tree_name)
1056
+ tree_path = tm.resolve_path(task)
1028
1057
 
1029
1058
  app_name = positional[0]
1030
1059
  globs = positional[1..]
@@ -1099,8 +1128,7 @@ class Hiiro
1099
1128
  puts "Not in a task session (use -t or -f to specify)"
1100
1129
  next
1101
1130
  end
1102
- tree = tm.environment.find_tree(task.tree_name)
1103
- path = tree ? tree.path : File.join(Hiiro::WORK_DIR, task.tree_name)
1131
+ path = tm.resolve_path(task)
1104
1132
 
1105
1133
  if opts.session
1106
1134
  session_name = opts.session
@@ -1446,6 +1474,53 @@ class Hiiro
1446
1474
  end
1447
1475
  end
1448
1476
 
1477
+ class FallbackTarget
1478
+ attr_reader :name, :type
1479
+
1480
+ def self.from_project(name, path)
1481
+ new(name: name, path: path, type: :project)
1482
+ end
1483
+
1484
+ def initialize(name:, path:, type:)
1485
+ @name = name
1486
+ @_path = path
1487
+ @type = type
1488
+ end
1489
+
1490
+ def session_name = name
1491
+ def tree_name = nil
1492
+ def color_index = nil
1493
+ def subtask? = false
1494
+ def top_level? = true
1495
+ def short_name = name
1496
+ def parent_name = nil
1497
+ def tree = nil
1498
+
1499
+ def path
1500
+ @_path
1501
+ end
1502
+
1503
+ def branch
1504
+ return nil unless @_path
1505
+ out = IO.popen(['git', '-C', @_path, 'rev-parse', '--abbrev-ref', 'HEAD'], err: File::NULL, &:read)
1506
+ out&.strip&.then { |b| b.empty? ? nil : b }
1507
+ end
1508
+
1509
+ def display_data(scope: :task, environment:)
1510
+ br = branch
1511
+ {
1512
+ name: name,
1513
+ tree: '(project)',
1514
+ branch: br ? "[#{br}]" : '(none)',
1515
+ session: "(#{name})"
1516
+ }
1517
+ end
1518
+
1519
+ def to_s
1520
+ name
1521
+ end
1522
+ end
1523
+
1449
1524
  class App
1450
1525
  attr_reader :name, :relative_path
1451
1526
 
data/lib/hiiro/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  class Hiiro
2
- VERSION = "0.1.342"
2
+ VERSION = "0.1.344"
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.342
4
+ version: 0.1.344
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Toyota