portage-cli 0.10.0 → 0.11.0

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.
data/lib/portage/cli.rb CHANGED
@@ -79,8 +79,9 @@ module Portage
79
79
  portage orders reconcile [--checkout ID] [--json]
80
80
  portage index build [--sources a,b] [--queries FILE] [--dry-run] [--export DIR] [--json]
81
81
  portage index refresh [--sources a,b] [--queries FILE] [--dry-run] [--export DIR] [--json]
82
- portage index show [--stores|--products] [--json]
83
- portage index add <url> [--json]
82
+ portage index show [--stores|--products [--page N] [--per-page N]] [--json]
83
+ portage index search QUERY [--category ID] [--store HOST] [--limit N] [--json]
84
+ portage index add <url> [--crawl] [--json]
84
85
  portage index remove <host> [--json]
85
86
  portage index sources [--json]
86
87
  portage browser import [--browser chrome|edge|brave|arc|firefox|safari] [--profile-root DIR]
@@ -252,6 +253,7 @@ module Portage
252
253
  lines << " platform: #{report[:platform]}" if report[:platform]
253
254
  lines << " webmcp: #{report[:webmcp][:status]}#{" (#{report[:webmcp][:reason]})" if report[:webmcp][:reason]}"
254
255
  lines << " next: #{report[:next_step]}"
256
+ lines << " index: `#{report[:index_hint]}` adds its catalogue to your local index" if report[:index_hint]
255
257
  lines.join("\n")
256
258
  end
257
259
  private_class_method :format_check
@@ -1361,6 +1363,7 @@ module Portage
1361
1363
  "build" => ->(argv) { run_index_build(argv, refresh: false) },
1362
1364
  "refresh" => ->(argv) { run_index_build(argv, refresh: true) },
1363
1365
  "show" => ->(argv) { run_index_show(argv) },
1366
+ "search" => ->(argv) { run_index_search(argv) },
1364
1367
  "add" => ->(argv) { run_index_add(argv) },
1365
1368
  "remove" => ->(argv) { run_index_remove(argv) },
1366
1369
  "sources" => ->(argv) { run_index_sources(argv) }
@@ -1419,41 +1422,119 @@ module Portage
1419
1422
  end
1420
1423
  private_class_method :format_index_build
1421
1424
 
1425
+ INDEX_SHOW_PER_PAGE = 50
1426
+
1422
1427
  def self.run_index_show(argv)
1423
- opts = { kind: nil, json: false }
1428
+ opts = { kind: nil, json: false, page: 1, per_page: INDEX_SHOW_PER_PAGE }
1424
1429
  OptionParser.new do |parser|
1425
1430
  parser.on("--stores") { opts[:kind] = "stores" }
1426
1431
  parser.on("--products") { opts[:kind] = "products" }
1432
+ parser.on("--page N", Integer) { |v| opts[:page] = [v, 1].max }
1433
+ parser.on("--per-page N", Integer) { |v| opts[:per_page] = [v, 1].max }
1427
1434
  parser.on("--json") { opts[:json] = true }
1428
1435
  end.parse!(argv)
1429
1436
 
1430
- result = { stores: opts[:kind] == "products" ? [] : Index::Store.new.all,
1431
- products: opts[:kind] == "stores" ? [] : Index::ProductStore.new.all }
1437
+ result = opts[:kind] == "products" ? index_products_page(opts) : index_show_all(opts[:kind])
1432
1438
  puts opts[:json] ? JSON.pretty_generate(result) : format_index_show(result)
1433
1439
  0
1434
1440
  end
1435
1441
  private_class_method :run_index_show
1436
1442
 
1443
+ def self.index_show_all(kind)
1444
+ { stores: kind == "products" ? [] : Index::Store.new.all, products: kind == "stores" ? [] : Index::ProductStore.new.all }
1445
+ end
1446
+ private_class_method :index_show_all
1447
+
1448
+ # `--products` pages (docs/plans/local-catalogue.md Phase 2): a crawled
1449
+ # index can hold thousands.
1450
+ def self.index_products_page(opts)
1451
+ products = Index::ProductStore.new
1452
+ { stores: [], products: products.page(opts[:page], per_page: opts[:per_page]), page: opts[:page],
1453
+ per_page: opts[:per_page], products_total: products.count }
1454
+ end
1455
+ private_class_method :index_products_page
1456
+
1437
1457
  def self.format_index_show(result)
1438
- lines = ["Stores:"]
1439
- result[:stores].each { |s| lines << " #{s['origin']} (#{Array(s['sources']).join(', ')})" }
1440
- lines << "(none)" if result[:stores].empty?
1458
+ paged = result.key?(:page)
1459
+ lines = paged ? [] : index_store_lines(result[:stores])
1441
1460
  lines << "Products:"
1442
1461
  result[:products].each { |p| lines << " #{p['title'] || p['key']}" }
1443
1462
  lines << "(none)" if result[:products].empty?
1463
+ lines << index_page_line(result) if paged
1444
1464
  lines.join("\n")
1445
1465
  end
1446
1466
  private_class_method :format_index_show
1447
1467
 
1468
+ def self.index_store_lines(stores)
1469
+ lines = ["Stores:"] + stores.map { |s| " #{s['origin']} (#{Array(s['sources']).join(', ')})" }
1470
+ stores.empty? ? lines + ["(none)"] : lines
1471
+ end
1472
+ private_class_method :index_store_lines
1473
+
1474
+ def self.index_page_line(result)
1475
+ first = ((result[:page] - 1) * result[:per_page]) + 1
1476
+ last = first + result[:products].length - 1
1477
+ return "Page #{result[:page]} is past the end (#{result[:products_total]} product(s))." if last < first
1478
+
1479
+ "Showing #{first}-#{last} of #{result[:products_total]}. Next: --page #{result[:page] + 1}" \
1480
+ "#{" --per-page #{result[:per_page]}" unless result[:per_page] == INDEX_SHOW_PER_PAGE}"
1481
+ end
1482
+ private_class_method :index_page_line
1483
+
1484
+ # `portage index search` (docs/plans/local-catalogue.md Phase 2): local
1485
+ # index entries only, no request. Exits 1 with no hits.
1486
+ def self.run_index_search(argv)
1487
+ opts = { category: nil, store: nil, limit: 20, json: false }
1488
+ OptionParser.new do |parser|
1489
+ parser.on("--category ID") { |v| opts[:category] = v }
1490
+ parser.on("--store HOST") { |v| opts[:store] = v }
1491
+ parser.on("--limit N", Integer) { |v| opts[:limit] = [v, 1].max }
1492
+ parser.on("--json") { opts[:json] = true }
1493
+ end.parse!(argv)
1494
+ query = argv.join(" ").strip
1495
+ return (warn USAGE) || 1 if query.empty?
1496
+
1497
+ result = index_search(query, opts)
1498
+ puts opts[:json] ? JSON.pretty_generate(result) : format_index_search(result)
1499
+ result[:products].empty? ? 1 : 0
1500
+ end
1501
+ private_class_method :run_index_search
1502
+
1503
+ def self.index_search(query, opts)
1504
+ products = Index::ProductStore.new
1505
+ filters = opts.slice(:category, :store, :limit)
1506
+ hits = products.search(query, **filters).map { |entry| entry.merge("product" => Index::EntryProduct.wire(entry)) }
1507
+ { query: query, engine: products.search_engine, live: false, filters: filters, products: hits }
1508
+ end
1509
+ private_class_method :index_search
1510
+
1511
+ def self.format_index_search(result)
1512
+ return "No index products match \"#{result[:query]}\"." if result[:products].empty?
1513
+
1514
+ lines = result[:products].map { |p| index_search_line(p) }
1515
+ lines << "(From the local index, not live: check the price and stock with `portage find --store`.)"
1516
+ lines << "(FTS5 isn't available in this SQLite, so this was a plain text match.)" if result[:engine] == "like"
1517
+ lines.join("\n")
1518
+ end
1519
+ private_class_method :format_index_search
1520
+
1521
+ def self.index_search_line(product)
1522
+ hosts = Array(product["stores"]).filter_map { |s| URI.parse(s["origin"].to_s).host }.join(", ")
1523
+ ["#{product['title']}#{" — #{product['brand']}" if product['brand']}", ("(#{hosts})" unless hosts.empty?),
1524
+ product["url"]].compact.join(" ")
1525
+ end
1526
+ private_class_method :index_search_line
1527
+
1448
1528
  def self.run_index_add(argv)
1449
- json = argv.delete("--json") ? true : false
1529
+ json = !argv.delete("--json").nil?
1530
+ crawl = !argv.delete("--crawl").nil?
1450
1531
  url = argv.first && !argv.first.start_with?("-") ? argv.shift : nil
1451
1532
  unless url
1452
1533
  warn USAGE
1453
1534
  return 1
1454
1535
  end
1455
1536
 
1456
- result = Index::Builder.new.add(url)
1537
+ result = Index::Builder.new.add(url, crawl: crawl)
1457
1538
  puts json ? JSON.pretty_generate(result) : result[:message]
1458
1539
  result[:added] ? 0 : 1
1459
1540
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: portage-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.0
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tom Whitbread
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-09-29 00:00:00.000000000 Z
11
+ date: 2026-09-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: irb
@@ -72,6 +72,20 @@ dependencies:
72
72
  - - "~>"
73
73
  - !ruby/object:Gem::Version
74
74
  version: '0.1'
75
+ - !ruby/object:Gem::Dependency
76
+ name: sqlite3
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '2.9'
82
+ type: :runtime
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '2.9'
75
89
  - !ruby/object:Gem::Dependency
76
90
  name: rspec
77
91
  requirement: !ruby/object:Gem::Requirement
@@ -155,6 +169,8 @@ files:
155
169
  - exe/portage
156
170
  - exe/portage-console
157
171
  - known-stores/categories.yml
172
+ - known-stores/category-stoplist.yml
173
+ - known-stores/category-synonyms.yml
158
174
  - lib/portage/cli.rb
159
175
  - lib/portage/cli/agent_profile_url.rb
160
176
  - lib/portage/cli/approval_policy.rb
@@ -188,6 +204,8 @@ files:
188
204
  - lib/portage/cli/check_next_step.rb
189
205
  - lib/portage/cli/checkout_handoff.rb
190
206
  - lib/portage/cli/classifier.rb
207
+ - lib/portage/cli/classifier/ranking.rb
208
+ - lib/portage/cli/classifier/table.rb
191
209
  - lib/portage/cli/compare.rb
192
210
  - lib/portage/cli/confidence_check.rb
193
211
  - lib/portage/cli/config.rb
@@ -212,12 +230,21 @@ files:
212
230
  - lib/portage/cli/human_prompt.rb
213
231
  - lib/portage/cli/index.rb
214
232
  - lib/portage/cli/index/builder.rb
233
+ - lib/portage/cli/index/database.rb
234
+ - lib/portage/cli/index/entry_product.rb
215
235
  - lib/portage/cli/index/exporter.rb
216
236
  - lib/portage/cli/index/known_cache.rb
237
+ - lib/portage/cli/index/legacy_import.rb
217
238
  - lib/portage/cli/index/product_store.rb
239
+ - lib/portage/cli/index/schema.rb
240
+ - lib/portage/cli/index/search.rb
218
241
  - lib/portage/cli/index/sources.rb
219
242
  - lib/portage/cli/index/sources/browser.rb
220
243
  - lib/portage/cli/index/sources/shopify_catalog.rb
244
+ - lib/portage/cli/index/sources/storefront_products.rb
245
+ - lib/portage/cli/index/sources/storefront_products/mapper.rb
246
+ - lib/portage/cli/index/sources/storefront_products/pages.rb
247
+ - lib/portage/cli/index/sources/storefront_products/robots.rb
221
248
  - lib/portage/cli/index/sources/stores_file.rb
222
249
  - lib/portage/cli/index/sources/webmcp_sweep.rb
223
250
  - lib/portage/cli/index/sources/wikidata.rb