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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +52 -0
- data/README.md +20 -4
- data/known-stores/categories.yml +6841 -18
- data/known-stores/category-stoplist.yml +40 -0
- data/known-stores/category-synonyms.yml +15 -0
- data/lib/portage/cli/browser_import/categorize.rb +5 -2
- data/lib/portage/cli/check.rb +11 -1
- data/lib/portage/cli/classifier/ranking.rb +135 -0
- data/lib/portage/cli/classifier/table.rb +63 -0
- data/lib/portage/cli/classifier.rb +23 -44
- data/lib/portage/cli/doctor.rb +16 -6
- data/lib/portage/cli/find.rb +5 -3
- data/lib/portage/cli/index/builder.rb +49 -12
- data/lib/portage/cli/index/database.rb +150 -0
- data/lib/portage/cli/index/entry_product.rb +37 -0
- data/lib/portage/cli/index/legacy_import.rb +54 -0
- data/lib/portage/cli/index/product_store.rb +73 -35
- data/lib/portage/cli/index/schema.rb +70 -0
- data/lib/portage/cli/index/search.rb +73 -0
- data/lib/portage/cli/index/sources/storefront_products/mapper.rb +127 -0
- data/lib/portage/cli/index/sources/storefront_products/pages.rb +114 -0
- data/lib/portage/cli/index/sources/storefront_products/robots.rb +70 -0
- data/lib/portage/cli/index/sources/storefront_products.rb +147 -0
- data/lib/portage/cli/index/sources.rb +5 -2
- data/lib/portage/cli/index/store.rb +23 -47
- data/lib/portage/cli/index.rb +1 -0
- data/lib/portage/cli/offer_sources.rb +17 -2
- data/lib/portage/cli/version.rb +1 -1
- data/lib/portage/cli.rb +91 -10
- metadata +29 -2
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
|
|
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 =
|
|
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
|
-
|
|
1439
|
-
|
|
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")
|
|
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.
|
|
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-
|
|
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
|