portage-cli 0.9.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 +64 -0
- data/README.md +33 -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/buy.rb +3 -24
- data/lib/portage/cli/check.rb +164 -0
- data/lib/portage/cli/check_next_step.rb +51 -0
- 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/handoff_host.rb +33 -0
- 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 +142 -12
- metadata +34 -4
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
require "json"
|
|
2
|
-
require "fileutils"
|
|
3
1
|
require "uri"
|
|
2
|
+
require_relative "database"
|
|
4
3
|
|
|
5
4
|
module Portage
|
|
6
5
|
module Cli
|
|
7
6
|
module Index
|
|
8
|
-
# `~/.portage/index/
|
|
9
|
-
# `
|
|
7
|
+
# The `stores` table of `~/.portage/index/index.sqlite3` (Index::Database;
|
|
8
|
+
# `stores.json` before docs/plans/local-catalogue.md Phase 1) — one
|
|
9
|
+
# entry per merchant origin `portage index build`/`refresh`/`add` has
|
|
10
|
+
# found and verified.
|
|
10
11
|
#
|
|
11
12
|
# This is untrusted, user-owned data, same posture as `stores.yml`
|
|
12
13
|
# before it's tagged: never checked into git (docs/plans/
|
|
@@ -19,7 +20,7 @@ module Portage
|
|
|
19
20
|
# search result (see search_backends_spec.rb/cli_spec.rb for the
|
|
20
21
|
# specs proving both).
|
|
21
22
|
#
|
|
22
|
-
# Entry shape (string keys, JSON
|
|
23
|
+
# Entry shape (string keys, a JSON object per row): origin, platform,
|
|
23
24
|
# ucp_version, capabilities (array of "catalog"/"cart"/"checkout"),
|
|
24
25
|
# webmcp_preset, categories (category id => weight, top 5),
|
|
25
26
|
# sources (array of source names that found it), last_verified
|
|
@@ -27,45 +28,49 @@ module Portage
|
|
|
27
28
|
class Store
|
|
28
29
|
PATH = File.join(Dir.home, ".portage", "index", "stores.json").freeze
|
|
29
30
|
|
|
31
|
+
# @param path [String] where the legacy stores.json lives (or would
|
|
32
|
+
# live) — the database sits beside it, and a stores.json found
|
|
33
|
+
# there is imported on first open.
|
|
30
34
|
def initialize(path: PATH)
|
|
31
|
-
@
|
|
35
|
+
@db = Database.new(path: Database.path_for(path))
|
|
32
36
|
end
|
|
33
37
|
|
|
34
|
-
def all = entries.values
|
|
38
|
+
def all = @db.entries("stores").values
|
|
35
39
|
|
|
36
|
-
def find(origin) =
|
|
40
|
+
def find(origin) = @db.get("stores", origin)
|
|
37
41
|
|
|
38
42
|
# Merges `fields` onto whatever's already there for `origin` (or
|
|
39
43
|
# starts a fresh entry) — so a second source finding the same store
|
|
40
44
|
# adds to its `sources`/`categories` rather than clobbering the
|
|
41
45
|
# first source's findings.
|
|
42
46
|
def upsert(origin, **fields)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
@db.transaction do
|
|
48
|
+
existing = @db.get("stores", origin) || { "origin" => origin }
|
|
49
|
+
existing.merge(fields.transform_keys(&:to_s)).tap { |merged| @db.put("stores", origin, merged) }
|
|
50
|
+
end
|
|
47
51
|
end
|
|
48
52
|
|
|
49
53
|
# @return [Integer] how many entries (0 or 1 — origins are unique)
|
|
50
54
|
# this host's removal actually dropped.
|
|
51
55
|
def remove(host)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
+
@db.transaction do
|
|
57
|
+
doomed = @db.entries("stores").keys.select { |origin| host_of(origin) == host }
|
|
58
|
+
doomed.each { |origin| @db.delete("stores", origin) }
|
|
59
|
+
doomed.length
|
|
60
|
+
end
|
|
56
61
|
end
|
|
57
62
|
|
|
58
63
|
# @return [Integer, nil] seconds since the oldest entry was
|
|
59
64
|
# verified — nil when the index is empty. `doctor` and `index
|
|
60
65
|
# show` use this to say how stale the local index is.
|
|
61
66
|
def oldest_verified_age(now: Time.now)
|
|
62
|
-
timestamps =
|
|
67
|
+
timestamps = all.filter_map { |e| e["last_verified"] }
|
|
63
68
|
return nil if timestamps.empty?
|
|
64
69
|
|
|
65
70
|
now.to_i - timestamps.min
|
|
66
71
|
end
|
|
67
72
|
|
|
68
|
-
def exists? =
|
|
73
|
+
def exists? = @db.exists?
|
|
69
74
|
|
|
70
75
|
private
|
|
71
76
|
|
|
@@ -74,35 +79,6 @@ module Portage
|
|
|
74
79
|
rescue URI::InvalidURIError
|
|
75
80
|
""
|
|
76
81
|
end
|
|
77
|
-
|
|
78
|
-
def entries
|
|
79
|
-
@entries ||= read
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def read
|
|
83
|
-
return {} unless File.readable?(@path)
|
|
84
|
-
|
|
85
|
-
# A business name/title can carry non-ASCII bytes (curly quotes,
|
|
86
|
-
# ®, accents); reading with the process's default external
|
|
87
|
-
# encoding (US-ASCII on a bare-minimal LANG, confirmed live in a
|
|
88
|
-
# sandbox with no locale set) would otherwise raise on the very
|
|
89
|
-
# first non-ASCII byte and get silently swallowed below, quietly
|
|
90
|
-
# dropping the whole file's worth of entries — this was caught by
|
|
91
|
-
# this phase's own live index-build check.
|
|
92
|
-
parsed = JSON.parse(File.read(@path, encoding: "UTF-8"))
|
|
93
|
-
parsed.is_a?(Hash) ? parsed : {}
|
|
94
|
-
rescue StandardError
|
|
95
|
-
{}
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
# An index that can't be written just means this run's findings
|
|
99
|
-
# aren't saved — never a failed build.
|
|
100
|
-
def write
|
|
101
|
-
FileUtils.mkdir_p(File.dirname(@path))
|
|
102
|
-
File.write(@path, JSON.generate(@entries))
|
|
103
|
-
rescue StandardError
|
|
104
|
-
nil
|
|
105
|
-
end
|
|
106
82
|
end
|
|
107
83
|
end
|
|
108
84
|
end
|
data/lib/portage/cli/index.rb
CHANGED
|
@@ -49,6 +49,19 @@ module Portage
|
|
|
49
49
|
|
|
50
50
|
def self.retail_handoff_host?(host) = HandoffOnly.matches_any?(host, RETAIL_HANDOFF_HOSTS)
|
|
51
51
|
|
|
52
|
+
# Adds the UCP Product wire hash to an offer as `product` (docs/plans/
|
|
53
|
+
# local-catalogue.md Phase 3), as the store served it, with `media`
|
|
54
|
+
# cut to the first image. Shared by Find and ShopifyCatalog so both
|
|
55
|
+
# trim the same way. Anything that isn't a non-empty hash leaves the
|
|
56
|
+
# offer as it was: the field is left out, not faked.
|
|
57
|
+
def self.with_product(offer, product)
|
|
58
|
+
return offer unless product.is_a?(Hash) && !product.empty?
|
|
59
|
+
|
|
60
|
+
media = product["media"]
|
|
61
|
+
product = product.merge("media" => media.first(1)) if media.is_a?(Array) && media.length > 1
|
|
62
|
+
offer.merge(product: product)
|
|
63
|
+
end
|
|
64
|
+
|
|
52
65
|
# Shared by every retailer source below: an offer's `store` is the
|
|
53
66
|
# origin of the item URL the API itself returned, same idea as
|
|
54
67
|
# ShopifyCatalog#origin_of — kept as one module method instead of
|
|
@@ -133,8 +146,10 @@ module Portage
|
|
|
133
146
|
return nil unless origin
|
|
134
147
|
|
|
135
148
|
amount, currency = price_of(product)
|
|
136
|
-
|
|
137
|
-
|
|
149
|
+
OfferSources.with_product(
|
|
150
|
+
{ store: origin, source: name, checkout: nil, product_id: variant["id"], title: product["title"],
|
|
151
|
+
amount: amount, currency: currency, url: variant["url"] }, product
|
|
152
|
+
)
|
|
138
153
|
end
|
|
139
154
|
|
|
140
155
|
def origin_of(url)
|
data/lib/portage/cli/version.rb
CHANGED
data/lib/portage/cli.rb
CHANGED
|
@@ -15,6 +15,7 @@ require_relative "cli/browser_profile"
|
|
|
15
15
|
require_relative "cli/buy"
|
|
16
16
|
require_relative "cli/find"
|
|
17
17
|
require_relative "cli/compare"
|
|
18
|
+
require_relative "cli/check"
|
|
18
19
|
require_relative "cli/history"
|
|
19
20
|
require_relative "cli/quotes"
|
|
20
21
|
require_relative "cli/money"
|
|
@@ -56,6 +57,7 @@ module Portage
|
|
|
56
57
|
portage find --query "..." [--max-price N] [--limit N] [--json]
|
|
57
58
|
portage compare <url> --product-id ID [--id VALUE ...] [--results N]
|
|
58
59
|
[--max-price N] [--json]
|
|
60
|
+
portage check <url> [--json]
|
|
59
61
|
portage pick [--search LAST|SEARCH_ID] [--via auto|tty|agent] [--json]
|
|
60
62
|
[--choose REF | --compare REF | --view REF]
|
|
61
63
|
portage approve QUOTE_ID [--via auto|tty|agent] [--relayed-yes | --view] [--json]
|
|
@@ -77,8 +79,9 @@ module Portage
|
|
|
77
79
|
portage orders reconcile [--checkout ID] [--json]
|
|
78
80
|
portage index build [--sources a,b] [--queries FILE] [--dry-run] [--export DIR] [--json]
|
|
79
81
|
portage index refresh [--sources a,b] [--queries FILE] [--dry-run] [--export DIR] [--json]
|
|
80
|
-
portage index show [--stores|--products] [--json]
|
|
81
|
-
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]
|
|
82
85
|
portage index remove <host> [--json]
|
|
83
86
|
portage index sources [--json]
|
|
84
87
|
portage browser import [--browser chrome|edge|brave|arc|firefox|safari] [--profile-root DIR]
|
|
@@ -93,13 +96,13 @@ module Portage
|
|
|
93
96
|
portage generate agent-profile [--out FILE] [--key-out FILE] [--rotate]
|
|
94
97
|
portage --version
|
|
95
98
|
|
|
96
|
-
proxy flags (buy/find/compare/doctor/payment enroll):
|
|
99
|
+
proxy flags (buy/find/compare/check/doctor/payment enroll):
|
|
97
100
|
[--proxy URL] [--proxy-mode forward|gateway] [--proxy-header "Name: value"]
|
|
98
101
|
[--no-proxy HOSTS] [--proxy-route ROUTE=URL|direct] [--proxy-chain URL,URL,...]
|
|
99
102
|
[--proxy-passthrough HEADER] [--proxy-ca FILE] [--no-env-proxy]
|
|
100
103
|
USAGE
|
|
101
104
|
|
|
102
|
-
COMMANDS = { "buy" => :run_buy, "find" => :run_find, "compare" => :run_compare,
|
|
105
|
+
COMMANDS = { "buy" => :run_buy, "find" => :run_find, "compare" => :run_compare, "check" => :run_check,
|
|
103
106
|
"pick" => :run_pick, "approve" => :run_approve,
|
|
104
107
|
"history" => :run_history, "payment" => :run_payment, "policy" => :run_policy,
|
|
105
108
|
"orders" => :run_orders, "index" => :run_index, "browser" => :run_browser,
|
|
@@ -207,6 +210,54 @@ module Portage
|
|
|
207
210
|
def self.to_minor_units(major) = (major * 100).round
|
|
208
211
|
private_class_method :to_minor_units
|
|
209
212
|
|
|
213
|
+
# --- check ---
|
|
214
|
+
|
|
215
|
+
# `portage check <url>` — can Portage buy from this store, and how?
|
|
216
|
+
# Exit 0 for `automated`/`webmcp`, 1 for anything else (and for a bad
|
|
217
|
+
# or missing URL, with the usage on stderr).
|
|
218
|
+
def self.run_check(argv)
|
|
219
|
+
opts = parse_check_options(argv)
|
|
220
|
+
return 1 unless opts
|
|
221
|
+
return 1 unless apply_proxy_settings(opts[:proxy])
|
|
222
|
+
|
|
223
|
+
report = Check.new(opts[:url]).call
|
|
224
|
+
puts opts[:json] ? JSON.pretty_generate(report) : format_check(report)
|
|
225
|
+
Check::USABLE_VERDICTS.include?(report[:verdict]) ? 0 : 1
|
|
226
|
+
end
|
|
227
|
+
private_class_method :run_check
|
|
228
|
+
|
|
229
|
+
def self.parse_check_options(argv)
|
|
230
|
+
opts = { proxy: {} }
|
|
231
|
+
OptionParser.new do |parser|
|
|
232
|
+
parser.on("--json") { opts[:json] = true }
|
|
233
|
+
ProxySettings.add_options(parser, opts[:proxy])
|
|
234
|
+
end.parse!(argv)
|
|
235
|
+
opts[:url] = argv.shift.to_s.strip
|
|
236
|
+
return opts if !opts[:url].empty? && argv.empty? && valid_check_url?(opts[:url])
|
|
237
|
+
|
|
238
|
+
warn USAGE
|
|
239
|
+
nil
|
|
240
|
+
end
|
|
241
|
+
private_class_method :parse_check_options
|
|
242
|
+
|
|
243
|
+
def self.valid_check_url?(url)
|
|
244
|
+
uri = URI.parse(url =~ %r{\Ahttps?://}i ? url : "https://#{url}")
|
|
245
|
+
uri.is_a?(URI::HTTP) && !uri.host.to_s.empty?
|
|
246
|
+
rescue URI::InvalidURIError
|
|
247
|
+
false
|
|
248
|
+
end
|
|
249
|
+
private_class_method :valid_check_url?
|
|
250
|
+
|
|
251
|
+
def self.format_check(report)
|
|
252
|
+
lines = ["#{report[:url]}: #{report[:verdict]}"]
|
|
253
|
+
lines << " platform: #{report[:platform]}" if report[:platform]
|
|
254
|
+
lines << " webmcp: #{report[:webmcp][:status]}#{" (#{report[:webmcp][:reason]})" if report[:webmcp][:reason]}"
|
|
255
|
+
lines << " next: #{report[:next_step]}"
|
|
256
|
+
lines << " index: `#{report[:index_hint]}` adds its catalogue to your local index" if report[:index_hint]
|
|
257
|
+
lines.join("\n")
|
|
258
|
+
end
|
|
259
|
+
private_class_method :format_check
|
|
260
|
+
|
|
210
261
|
# --- compare ---
|
|
211
262
|
|
|
212
263
|
def self.run_compare(argv)
|
|
@@ -1312,6 +1363,7 @@ module Portage
|
|
|
1312
1363
|
"build" => ->(argv) { run_index_build(argv, refresh: false) },
|
|
1313
1364
|
"refresh" => ->(argv) { run_index_build(argv, refresh: true) },
|
|
1314
1365
|
"show" => ->(argv) { run_index_show(argv) },
|
|
1366
|
+
"search" => ->(argv) { run_index_search(argv) },
|
|
1315
1367
|
"add" => ->(argv) { run_index_add(argv) },
|
|
1316
1368
|
"remove" => ->(argv) { run_index_remove(argv) },
|
|
1317
1369
|
"sources" => ->(argv) { run_index_sources(argv) }
|
|
@@ -1370,41 +1422,119 @@ module Portage
|
|
|
1370
1422
|
end
|
|
1371
1423
|
private_class_method :format_index_build
|
|
1372
1424
|
|
|
1425
|
+
INDEX_SHOW_PER_PAGE = 50
|
|
1426
|
+
|
|
1373
1427
|
def self.run_index_show(argv)
|
|
1374
|
-
opts = { kind: nil, json: false }
|
|
1428
|
+
opts = { kind: nil, json: false, page: 1, per_page: INDEX_SHOW_PER_PAGE }
|
|
1375
1429
|
OptionParser.new do |parser|
|
|
1376
1430
|
parser.on("--stores") { opts[:kind] = "stores" }
|
|
1377
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 }
|
|
1378
1434
|
parser.on("--json") { opts[:json] = true }
|
|
1379
1435
|
end.parse!(argv)
|
|
1380
1436
|
|
|
1381
|
-
result =
|
|
1382
|
-
products: opts[:kind] == "stores" ? [] : Index::ProductStore.new.all }
|
|
1437
|
+
result = opts[:kind] == "products" ? index_products_page(opts) : index_show_all(opts[:kind])
|
|
1383
1438
|
puts opts[:json] ? JSON.pretty_generate(result) : format_index_show(result)
|
|
1384
1439
|
0
|
|
1385
1440
|
end
|
|
1386
1441
|
private_class_method :run_index_show
|
|
1387
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
|
+
|
|
1388
1457
|
def self.format_index_show(result)
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
lines << "(none)" if result[:stores].empty?
|
|
1458
|
+
paged = result.key?(:page)
|
|
1459
|
+
lines = paged ? [] : index_store_lines(result[:stores])
|
|
1392
1460
|
lines << "Products:"
|
|
1393
1461
|
result[:products].each { |p| lines << " #{p['title'] || p['key']}" }
|
|
1394
1462
|
lines << "(none)" if result[:products].empty?
|
|
1463
|
+
lines << index_page_line(result) if paged
|
|
1395
1464
|
lines.join("\n")
|
|
1396
1465
|
end
|
|
1397
1466
|
private_class_method :format_index_show
|
|
1398
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
|
+
|
|
1399
1528
|
def self.run_index_add(argv)
|
|
1400
|
-
json = argv.delete("--json")
|
|
1529
|
+
json = !argv.delete("--json").nil?
|
|
1530
|
+
crawl = !argv.delete("--crawl").nil?
|
|
1401
1531
|
url = argv.first && !argv.first.start_with?("-") ? argv.shift : nil
|
|
1402
1532
|
unless url
|
|
1403
1533
|
warn USAGE
|
|
1404
1534
|
return 1
|
|
1405
1535
|
end
|
|
1406
1536
|
|
|
1407
|
-
result = Index::Builder.new.add(url)
|
|
1537
|
+
result = Index::Builder.new.add(url, crawl: crawl)
|
|
1408
1538
|
puts json ? JSON.pretty_generate(result) : result[:message]
|
|
1409
1539
|
result[:added] ? 0 : 1
|
|
1410
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
|
|
@@ -30,14 +30,14 @@ dependencies:
|
|
|
30
30
|
requirements:
|
|
31
31
|
- - "~>"
|
|
32
32
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: '0.
|
|
33
|
+
version: '0.11'
|
|
34
34
|
type: :runtime
|
|
35
35
|
prerelease: false
|
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
37
|
requirements:
|
|
38
38
|
- - "~>"
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
|
-
version: '0.
|
|
40
|
+
version: '0.11'
|
|
41
41
|
- !ruby/object:Gem::Dependency
|
|
42
42
|
name: portage-ucp-client
|
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -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
|
|
@@ -184,8 +200,12 @@ files:
|
|
|
184
200
|
- lib/portage/cli/buy.rb
|
|
185
201
|
- lib/portage/cli/buyer_context.rb
|
|
186
202
|
- lib/portage/cli/catalog_products.rb
|
|
203
|
+
- lib/portage/cli/check.rb
|
|
204
|
+
- lib/portage/cli/check_next_step.rb
|
|
187
205
|
- lib/portage/cli/checkout_handoff.rb
|
|
188
206
|
- lib/portage/cli/classifier.rb
|
|
207
|
+
- lib/portage/cli/classifier/ranking.rb
|
|
208
|
+
- lib/portage/cli/classifier/table.rb
|
|
189
209
|
- lib/portage/cli/compare.rb
|
|
190
210
|
- lib/portage/cli/confidence_check.rb
|
|
191
211
|
- lib/portage/cli/config.rb
|
|
@@ -197,6 +217,7 @@ files:
|
|
|
197
217
|
- lib/portage/cli/generate/adapter.rb
|
|
198
218
|
- lib/portage/cli/generate/agent_profile.rb
|
|
199
219
|
- lib/portage/cli/handoff_agents.rb
|
|
220
|
+
- lib/portage/cli/handoff_host.rb
|
|
200
221
|
- lib/portage/cli/handoff_only.rb
|
|
201
222
|
- lib/portage/cli/handoff_reconciler.rb
|
|
202
223
|
- lib/portage/cli/handoff_reconciler/wire_adapters.rb
|
|
@@ -209,12 +230,21 @@ files:
|
|
|
209
230
|
- lib/portage/cli/human_prompt.rb
|
|
210
231
|
- lib/portage/cli/index.rb
|
|
211
232
|
- lib/portage/cli/index/builder.rb
|
|
233
|
+
- lib/portage/cli/index/database.rb
|
|
234
|
+
- lib/portage/cli/index/entry_product.rb
|
|
212
235
|
- lib/portage/cli/index/exporter.rb
|
|
213
236
|
- lib/portage/cli/index/known_cache.rb
|
|
237
|
+
- lib/portage/cli/index/legacy_import.rb
|
|
214
238
|
- lib/portage/cli/index/product_store.rb
|
|
239
|
+
- lib/portage/cli/index/schema.rb
|
|
240
|
+
- lib/portage/cli/index/search.rb
|
|
215
241
|
- lib/portage/cli/index/sources.rb
|
|
216
242
|
- lib/portage/cli/index/sources/browser.rb
|
|
217
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
|
|
218
248
|
- lib/portage/cli/index/sources/stores_file.rb
|
|
219
249
|
- lib/portage/cli/index/sources/webmcp_sweep.rb
|
|
220
250
|
- lib/portage/cli/index/sources/wikidata.rb
|