hiiro 0.1.56 → 0.1.57

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: 0db0a8fd9d54e542cb2b5951197e2250a1d199a66278b31bab5c003b50d8f39f
4
- data.tar.gz: bee3a32c0dc9e68379aee3963bb11b15041a9cd21715379dbbc086bff59eba17
3
+ metadata.gz: 7f55785ca9b54838e9b0e0eefe8e1e829f85ad1b1d9a1ed6b961305b847f1828
4
+ data.tar.gz: cda5ec5bdc86b4210e4870d88bca437c9cc7dbac3857ae3e264a66ba0b054675
5
5
  SHA512:
6
- metadata.gz: b59060b23668b9f6fc2818884628dccae6a95e84a5554d9666b9ce79d9bfc45f231c486039121c6a33cc8e97b144e1579cbd637131d8ca5cd0f196bfcdb4e9ff
7
- data.tar.gz: 5918af1c725ce23f22a17d273def7f656118c1a54f9fd13ee7fe0be708e82f8c8146c7a4b4c7860482c5fb85a5c8a86910c5412062a96d9c37ce9901b527bee7
6
+ metadata.gz: 8d8e719178da0355245154216264edb4c292fb5ccc62dc561e8529bf7b67f4a94d85a5db3516e1406f5cf7464f61454dc3c07f7dc26530a44a895577b7082c12
7
+ data.tar.gz: 28a570aba442cb90419feaf3edd4ef4135773fc2f971401df942bb09617f17c7466c3ba32dbfc2edd1849895d973cdada07f80b8edd2932685e8f21bc00f29d4
data/Rakefile CHANGED
@@ -1,3 +1,10 @@
1
1
  require "bundler/gem_tasks"
2
+ require "rake/testtask"
2
3
 
3
- task default: :build
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task default: :test
data/bin/h-plugin CHANGED
@@ -24,7 +24,7 @@ o.add_subcmd(:edit) { |*args|
24
24
  system(ENV['EDITOR'] || 'safe_nvim', __FILE__)
25
25
  else
26
26
  pm = Hiiro::PrefixMatcher.new(plugin_files) { |f| File.basename(f) }
27
- plugins = args.flat_map { |arg| pm.find_all(arg) }.uniq
27
+ plugins = args.flat_map { |arg| pm.find_all(arg).matches.map(&:item) }.uniq
28
28
 
29
29
  if plugins.none?
30
30
  puts "No matching plugins found for: #{args.map(&:inspect).join(' ')}"
data/exe/h CHANGED
@@ -4,7 +4,7 @@ require "hiiro"
4
4
  require "fileutils"
5
5
  require "pry"
6
6
 
7
- Hiiro.run(*ARGV, cwd: Dir.pwd, plugins: [:Tasks]) do
7
+ Hiiro.run(*ARGV, cwd: Dir.pwd, plugins: [Tasks]) do
8
8
  add_subcommand(:version) { |*args|
9
9
  puts Hiiro::VERSION
10
10
  }
data/hiiro.gemspec CHANGED
@@ -27,4 +27,7 @@ Gem::Specification.new do |spec|
27
27
  spec.require_paths = ["lib"]
28
28
 
29
29
  spec.add_dependency "pry", "~> 0.14"
30
+
31
+ spec.add_development_dependency "minitest", "~> 5.0"
32
+ spec.add_development_dependency "rake", "~> 13.0"
30
33
  end
@@ -1,5 +1,136 @@
1
1
  class Hiiro
2
2
  class PrefixMatcher
3
+ class Item
4
+ attr_reader :item, :extracted_item, :key, :block
5
+
6
+ def initialize(item:, extracted_item:, key: nil, block: nil)
7
+ @item = item
8
+ @extracted_item = extracted_item
9
+ @key = key
10
+ @block = block
11
+ end
12
+ end
13
+
14
+ class Result
15
+ attr_reader :matcher, :all_items, :key, :block, :prefix
16
+
17
+ def initialize(matcher:, all_items:, prefix:, key: nil, block: nil)
18
+ @matcher = matcher
19
+ @all_items = all_items
20
+ @prefix = prefix
21
+ @key = key
22
+ @block = block
23
+ end
24
+
25
+ def matches
26
+ @matches ||= all_items.select { |item| item.extracted_item.to_s.start_with?(prefix.to_s) }
27
+ end
28
+
29
+ def count
30
+ matches.count
31
+ end
32
+
33
+ def ambiguous?
34
+ count > 1
35
+ end
36
+
37
+ def exact_match
38
+ all_items.find { |item| item.extracted_item == prefix }
39
+ end
40
+
41
+ def match
42
+ one? ? matches.first : nil
43
+ end
44
+
45
+ def match?
46
+ matches.any?
47
+ end
48
+
49
+ def exact?
50
+ !exact_match.nil?
51
+ end
52
+
53
+ def one?
54
+ count == 1
55
+ end
56
+
57
+ # Returns item for resolve semantics: exact match, or single match, otherwise nil
58
+ def resolved
59
+ exact_match || match
60
+ end
61
+
62
+ # Returns the first matching item (for find semantics)
63
+ def first
64
+ matches.first
65
+ end
66
+ end
67
+
68
+ class PathResult
69
+ attr_reader :matcher, :all_items, :key, :block, :prefix
70
+
71
+ def initialize(matcher:, all_items:, prefix:, key: nil, block: nil)
72
+ @matcher = matcher
73
+ @all_items = all_items
74
+ @prefix = prefix
75
+ @key = key
76
+ @block = block
77
+ end
78
+
79
+ def matches
80
+ @matches ||= begin
81
+ prefixes = prefix.to_s.split('/')
82
+
83
+ items_with_paths = all_items.map { |item|
84
+ [item, item.extracted_item.to_s.split('/')]
85
+ }
86
+
87
+ prefixes.each_with_index do |seg, i|
88
+ items_with_paths = items_with_paths.select { |_, path| path[i]&.start_with?(seg) }
89
+ end
90
+
91
+ items_with_paths.map(&:first)
92
+ end
93
+ end
94
+
95
+ def count
96
+ matches.count
97
+ end
98
+
99
+ def ambiguous?
100
+ count > 1
101
+ end
102
+
103
+ def exact_match
104
+ all_items.find { |item| item.extracted_item == prefix }
105
+ end
106
+
107
+ def match
108
+ one? ? matches.first : nil
109
+ end
110
+
111
+ def match?
112
+ matches.any?
113
+ end
114
+
115
+ def exact?
116
+ !exact_match.nil?
117
+ end
118
+
119
+ def one?
120
+ count == 1
121
+ end
122
+
123
+ # Returns item for resolve semantics: exact match, or single match, otherwise nil
124
+ def resolved
125
+ exact_match || match
126
+ end
127
+
128
+ # Returns the first matching item (for find semantics)
129
+ def first
130
+ matches.first
131
+ end
132
+ end
133
+
3
134
  class << self
4
135
  def find(items, prefix, key: nil, &block)
5
136
  new(items, key, &block).find(prefix)
@@ -34,69 +165,69 @@ class Hiiro
34
165
  @block = block
35
166
  end
36
167
 
37
- def items(key = nil, &block)
38
- if key.nil? && !block_given?
39
- @items ||= original_items.map { |item| extract(item, @key, &@block) }
40
- else
41
- original_items.map { |item| extract(item, key, &block) }
42
- end
168
+ def all_items(key = nil, &block)
169
+ use_key = key.nil? && !block_given? ? @key : key
170
+ use_block = key.nil? && !block_given? ? @block : block
171
+
172
+ @all_items_cache ||= {}
173
+ cache_key = [use_key, use_block].hash
174
+
175
+ @all_items_cache[cache_key] ||= original_items.map { |item|
176
+ Item.new(
177
+ item: item,
178
+ extracted_item: extract(item, use_key, &use_block),
179
+ key: use_key,
180
+ block: use_block
181
+ )
182
+ }
43
183
  end
44
184
 
45
- def extracted_items(key = nil, &block)
46
- original_items.zip(items(key, &block))
185
+ def search(prefix, key = nil, &block)
186
+ Result.new(
187
+ matcher: self,
188
+ all_items: all_items(key, &block),
189
+ prefix: prefix,
190
+ key: key || @key,
191
+ block: block || @block
192
+ )
47
193
  end
48
194
 
49
195
  def find(prefix, key = nil, &block)
50
- extracted_items(key, &block).find { |_, extracted| matches?(extracted, prefix) }&.first
196
+ search(prefix, key, &block)
51
197
  end
52
198
 
53
199
  def find_all(prefix, key = nil, &block)
54
- extracted_items(key, &block).select { |_, extracted| matches?(extracted, prefix) }.map(&:first)
200
+ search(prefix, key, &block)
55
201
  end
56
202
 
57
203
  def resolve(prefix, key = nil, &block)
58
- pairs = extracted_items(key, &block)
59
-
60
- exact = pairs.find { |_, extracted| extracted == prefix }
61
- return exact.first if exact
62
-
63
- matches = pairs.select { |_, extracted| matches?(extracted, prefix) }
64
- matches.one? ? matches.first.first : nil
204
+ search(prefix, key, &block)
65
205
  end
66
206
 
67
207
  def find_path(prefix, key = nil, &block)
68
- matching_path_pairs(prefix, key, &block).first&.first
208
+ search_path(prefix, key, &block)
69
209
  end
70
210
 
71
211
  def find_all_paths(prefix, key = nil, &block)
72
- matching_path_pairs(prefix, key, &block).map(&:first)
212
+ search_path(prefix, key, &block)
73
213
  end
74
214
 
75
215
  def resolve_path(prefix, key = nil, &block)
76
- matches = matching_path_pairs(prefix, key, &block)
77
- return nil if matches.empty?
78
- return matches.first.first if matches.one?
216
+ search_path(prefix, key, &block)
217
+ end
79
218
 
80
- exact = matches.find { |_, path| path == prefix }
81
- exact&.first
219
+ def search_path(prefix, key = nil, &block)
220
+ PathResult.new(
221
+ matcher: self,
222
+ all_items: all_items(key, &block),
223
+ prefix: prefix,
224
+ key: key || @key,
225
+ block: block || @block
226
+ )
82
227
  end
83
228
 
84
229
  private
85
230
 
86
- def matching_path_pairs(prefix, key = nil, &block)
87
- prefixes = prefix.to_s.split('/')
88
-
89
- pairs = extracted_items(key, &block).map { |item, extracted|
90
- [item, extracted.to_s.split('/')]
91
- }
92
-
93
- prefixes.each_with_index do |seg, i|
94
- pairs = pairs.select { |_, path| path[i]&.start_with?(seg) }
95
- end
96
-
97
- pairs.map { |item, path| [item, path.join('/')] }
98
- end
99
-
100
231
  def matches?(item, prefix)
101
232
  item.to_s.start_with?(prefix.to_s)
102
233
  end
data/lib/hiiro/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  class Hiiro
2
- VERSION = "0.1.56"
2
+ VERSION = "0.1.57"
3
3
  end
data/plugins/pins.rb CHANGED
@@ -53,23 +53,27 @@ module Pins
53
53
  value
54
54
  end
55
55
 
56
- def find(partial)
56
+ def search(partial)
57
57
  Hiiro::PrefixMatcher.find(pins.keys.map(&:to_s), partial)
58
58
  end
59
59
 
60
+ def find(partial)
61
+ search(partial).match&.item
62
+ end
63
+
60
64
  def find_all(partial)
61
- Hiiro::PrefixMatcher.find_all(pins.keys.map(&:to_s), partial)
65
+ search(partial).matches.map(&:item)
62
66
  end
63
67
 
64
68
  def remove(name)
65
- all_matches = find_all(name)
69
+ result = search(name)
66
70
 
67
- if all_matches.count > 1
68
- puts "Unable to remove pin. Multiple matches: #{all_matches.inspect}"
71
+ if result.ambiguous?
72
+ puts "Unable to remove pin. Multiple matches: #{result.matches.map(&:item).inspect}"
69
73
  return
70
74
  end
71
75
 
72
- pin_name = all_matches.first
76
+ pin_name = result.match&.item
73
77
 
74
78
  pins.delete(pin_name.to_s)
75
79
  end
data/plugins/tasks.rb CHANGED
@@ -209,33 +209,33 @@ class Environment
209
209
  # Try path-based matching first (handles "parent/child" patterns)
210
210
  if abbreviated.include?('/')
211
211
  result = task_matcher.resolve_path(abbreviated)
212
- return result if result
212
+ return result.resolved&.item if result.match?
213
213
 
214
214
  # "main" refers to the parent task itself
215
215
  parent_prefix, child_prefix = abbreviated.split('/', 2)
216
216
  if 'main'.start_with?(child_prefix)
217
- return task_matcher.find(parent_prefix)
217
+ return task_matcher.find(parent_prefix).first&.item
218
218
  end
219
219
 
220
220
  nil
221
221
  else
222
- task_matcher.find(abbreviated)
222
+ task_matcher.find(abbreviated).first&.item
223
223
  end
224
224
  end
225
225
 
226
226
  def find_tree(abbreviated)
227
227
  return nil if abbreviated.nil?
228
- tree_matcher.find(abbreviated)
228
+ tree_matcher.find(abbreviated).first&.item
229
229
  end
230
230
 
231
231
  def find_session(abbreviated)
232
232
  return nil if abbreviated.nil?
233
- session_matcher.find(abbreviated)
233
+ session_matcher.find(abbreviated).first&.item
234
234
  end
235
235
 
236
236
  def find_app(abbreviated)
237
237
  return nil if abbreviated.nil?
238
- app_matcher.find(abbreviated)
238
+ app_matcher.find(abbreviated).first&.item
239
239
  end
240
240
  end
241
241
 
@@ -277,15 +277,15 @@ class TaskManager
277
277
  return slash_lookup(name) if name.include?('/')
278
278
 
279
279
  key = (scope == :subtask) ? :short_name : :name
280
- Hiiro::PrefixMatcher.new(tasks, key).find(name)
280
+ Hiiro::PrefixMatcher.new(tasks, key).find(name).first&.item
281
281
  end
282
282
 
283
283
  def task_by_tree(tree_name)
284
- environment.task_matcher.resolve(tree_name, :tree_name)
284
+ environment.task_matcher.resolve(tree_name, :tree_name).resolved&.item
285
285
  end
286
286
 
287
287
  def task_by_session(session_name)
288
- environment.task_matcher.resolve(session_name, :session_name)
288
+ environment.task_matcher.resolve(session_name, :session_name).resolved&.item
289
289
  end
290
290
 
291
291
  def current_task
@@ -549,19 +549,19 @@ class TaskManager
549
549
  return
550
550
  end
551
551
 
552
- matches = environment.app_matcher.find_all(app_name)
552
+ result = environment.app_matcher.find_all(app_name)
553
553
 
554
- case matches.count
554
+ case result.count
555
555
  when 0
556
556
  puts "ERROR: No matches found"
557
557
  puts
558
558
  puts "Possible Apps:"
559
559
  environment.all_apps.each { |a| puts format(" %-20s => %s", a.name, a.relative_path) }
560
560
  when 1
561
- print matches.first.resolve(tree_root)
561
+ print result.first.item.resolve(tree_root)
562
562
  else
563
563
  puts "Multiple matches found:"
564
- matches.each { |a| puts format(" %-20s => %s", a.name, a.relative_path) }
564
+ result.matches.each { |m| puts format(" %-20s => %s", m.item.name, m.item.relative_path) }
565
565
  end
566
566
  end
567
567
 
@@ -623,9 +623,9 @@ class TaskManager
623
623
  tree = environment.find_tree(task.tree_name)
624
624
  tree_root = tree ? tree.path : File.join(WORK_DIR, task.tree_name)
625
625
 
626
- matches = environment.app_matcher.find_all(app_name)
626
+ result = environment.app_matcher.find_all(app_name)
627
627
 
628
- case matches.count
628
+ case result.count
629
629
  when 0
630
630
  # Fallback: directory discovery
631
631
  exact = File.join(tree_root, app_name)
@@ -638,15 +638,15 @@ class TaskManager
638
638
  list_apps
639
639
  nil
640
640
  when 1
641
- app = matches.first
641
+ app = result.first.item
642
642
  [app.name, app.resolve(tree_root)]
643
643
  else
644
- exact = matches.find { |a| a.name == app_name }
644
+ exact = result.matches.find { |m| m.item.name == app_name }
645
645
  if exact
646
- [exact.name, exact.resolve(tree_root)]
646
+ [exact.item.name, exact.item.resolve(tree_root)]
647
647
  else
648
648
  puts "ERROR: '#{app_name}' matches multiple apps:"
649
- matches.each { |a| puts " #{a.name}" }
649
+ result.matches.each { |m| puts " #{m.item.name}" }
650
650
  nil
651
651
  end
652
652
  end
@@ -696,7 +696,7 @@ class TaskManager
696
696
  data = load_tasks
697
697
  data['tasks'] ||= []
698
698
  data['tasks'].reject! { |t| t['name'] == task.name }
699
- data['tasks'] << task.to_h
699
+ data['tasks'] << task.to_h.transform_keys(&:to_s)
700
700
  save_tasks(data)
701
701
  end
702
702
 
data/script/test ADDED
@@ -0,0 +1,4 @@
1
+ #!/bin/bash
2
+
3
+ bundle exec rake test "$@"
4
+
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.56
4
+ version: 0.1.57
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Toyota
@@ -24,6 +24,34 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0.14'
27
+ - !ruby/object:Gem::Dependency
28
+ name: minitest
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '5.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '5.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '13.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '13.0'
27
55
  description: Build multi-command CLI tools with subcommand dispatch, abbreviation
28
56
  matching, and a plugin system. Similar to git or docker command structure.
29
57
  email:
@@ -92,6 +120,7 @@ files:
92
120
  - script/install
93
121
  - script/publish
94
122
  - script/sync
123
+ - script/test
95
124
  - script/update
96
125
  homepage: https://github.com/unixsuperhero/hiiro
97
126
  licenses: