portage-cli 0.11.0 → 0.13.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 +98 -1
- data/README.md +88 -18
- data/lib/portage/cli/adapter_session.rb +34 -0
- data/lib/portage/cli/browser_import/importer.rb +1 -1
- data/lib/portage/cli/browser_import/plist_xml.rb +5 -12
- data/lib/portage/cli/browser_import/sqlite.rb +19 -25
- data/lib/portage/cli/browser_profile/cdp.rb +0 -9
- data/lib/portage/cli/buy.rb +217 -39
- data/lib/portage/cli/compare.rb +4 -3
- data/lib/portage/cli/confidence_check.rb +27 -7
- data/lib/portage/cli/confidence_state.rb +109 -0
- data/lib/portage/cli/find.rb +66 -9
- data/lib/portage/cli/generate/agent_profile.rb +1 -1
- data/lib/portage/cli/handoff_agents.rb +7 -21
- data/lib/portage/cli/handoff_reconciler.rb +2 -22
- data/lib/portage/cli/homepage_fetch.rb +5 -3
- data/lib/portage/cli/index/builder.rb +2 -11
- data/lib/portage/cli/index/known_cache.rb +9 -2
- data/lib/portage/cli/index/sources/stores_file.rb +2 -11
- data/lib/portage/cli/index/sources/wikidata.rb +2 -11
- data/lib/portage/cli/install_doctor.rb +2 -1
- data/lib/portage/cli/notifier.rb +16 -12
- data/lib/portage/cli/offer_sources.rb +3 -14
- data/lib/portage/cli/payment_methods.rb +7 -30
- data/lib/portage/cli/reconcile_notifier.rb +2 -5
- data/lib/portage/cli/setup_wizard/steps/env_keys_step.rb +40 -0
- data/lib/portage/cli/setup_wizard/steps/retailer_keys.rb +11 -23
- data/lib/portage/cli/setup_wizard/steps/search_keys.rb +10 -22
- data/lib/portage/cli/setup_wizard/steps/shipping.rb +5 -19
- data/lib/portage/cli/version.rb +1 -1
- data/lib/portage/cli.rb +25 -173
- metadata +5 -3
- data/lib/portage/cli/known_stores_url.rb +0 -15
data/lib/portage/cli.rb
CHANGED
|
@@ -55,6 +55,7 @@ module Portage
|
|
|
55
55
|
portage buy --quote QUOTE_ID --yes [--json] ...
|
|
56
56
|
portage buy --query "..." [--store URL] [--max-price N] [--limit N] ...
|
|
57
57
|
portage find --query "..." [--max-price N] [--limit N] [--json]
|
|
58
|
+
portage find --store URL --query "..." [--max-price N] [--json] (one store, live, read-only)
|
|
58
59
|
portage compare <url> --product-id ID [--id VALUE ...] [--results N]
|
|
59
60
|
[--max-price N] [--json]
|
|
60
61
|
portage check <url> [--json]
|
|
@@ -128,7 +129,6 @@ module Portage
|
|
|
128
129
|
puts VERSION
|
|
129
130
|
0
|
|
130
131
|
end
|
|
131
|
-
private_class_method :run_version
|
|
132
132
|
|
|
133
133
|
# --- proxy (docs/plans/proxy-support.md Phase 2) ---
|
|
134
134
|
|
|
@@ -148,7 +148,6 @@ module Portage
|
|
|
148
148
|
warn "portage: #{e.message}"
|
|
149
149
|
nil
|
|
150
150
|
end
|
|
151
|
-
private_class_method :apply_proxy_settings
|
|
152
151
|
|
|
153
152
|
# --- find ---
|
|
154
153
|
|
|
@@ -164,21 +163,18 @@ module Portage
|
|
|
164
163
|
puts json ? JSON.pretty_generate(report) : format_find(report)
|
|
165
164
|
report[:offers].any? ? 0 : 1
|
|
166
165
|
end
|
|
167
|
-
private_class_method :run_find
|
|
168
166
|
|
|
169
167
|
# @return [Hash, nil] the saved search entry.
|
|
170
168
|
def self.record_find(report)
|
|
171
169
|
History.new.record_search(query: report[:query], offer_count: report[:offers].length,
|
|
172
170
|
message: report[:message], offers: report[:offers])
|
|
173
171
|
end
|
|
174
|
-
private_class_method :record_find
|
|
175
172
|
|
|
176
173
|
# docs/plans/human-pick-and-approve.md Phase 2: the saved search's id,
|
|
177
174
|
# for `portage pick --search`. Additive; absent when nothing was saved.
|
|
178
175
|
def self.with_search_id(report, entry)
|
|
179
176
|
entry.is_a?(Hash) && entry["search_id"] ? report.merge(search_id: entry["search_id"]) : report
|
|
180
177
|
end
|
|
181
|
-
private_class_method :with_search_id
|
|
182
178
|
|
|
183
179
|
def self.parse_find_options(argv)
|
|
184
180
|
opts = {}
|
|
@@ -188,27 +184,38 @@ module Portage
|
|
|
188
184
|
return nil
|
|
189
185
|
end
|
|
190
186
|
|
|
191
|
-
opts
|
|
187
|
+
valid_find_store?(opts) ? opts : nil
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# `--store` must be an http(s) URL (a bare host is read as https); anything
|
|
191
|
+
# else is a usage error rather than a silent no-op search.
|
|
192
|
+
def self.valid_find_store?(opts)
|
|
193
|
+
return true unless opts.key?(:store)
|
|
194
|
+
return true if Find.store_origin(opts[:store])
|
|
195
|
+
|
|
196
|
+
warn "portage: --store needs an http(s) store URL, got #{opts[:store].inspect}."
|
|
197
|
+
warn USAGE
|
|
198
|
+
false
|
|
192
199
|
end
|
|
193
|
-
private_class_method :parse_find_options
|
|
194
200
|
|
|
195
201
|
def self.find_option_parser(opts)
|
|
196
202
|
opts[:proxy] = {}
|
|
197
203
|
OptionParser.new do |parser|
|
|
198
204
|
parser.on("--query QUERY") { |v| opts[:query] = v }
|
|
205
|
+
parser.on("--store URL", "Search only this store's catalogue, live and read-only (http/https)") do |v|
|
|
206
|
+
opts[:store] = v
|
|
207
|
+
end
|
|
199
208
|
parser.on("--limit N", Integer) { |v| opts[:limit] = v }
|
|
200
209
|
parser.on("--max-price N", Float) { |v| opts[:max_price] = to_minor_units(v) }
|
|
201
210
|
parser.on("--json") { opts[:json] = true }
|
|
202
211
|
ProxySettings.add_options(parser, opts[:proxy])
|
|
203
212
|
end
|
|
204
213
|
end
|
|
205
|
-
private_class_method :find_option_parser
|
|
206
214
|
|
|
207
215
|
# `--max-price 400` means 400 of whatever the offer is priced in, and the
|
|
208
216
|
# comparison happens per-offer in that offer's own currency — no FX
|
|
209
217
|
# conversion, and no attempt to handle zero-decimal currencies like JPY.
|
|
210
218
|
def self.to_minor_units(major) = (major * 100).round
|
|
211
|
-
private_class_method :to_minor_units
|
|
212
219
|
|
|
213
220
|
# --- check ---
|
|
214
221
|
|
|
@@ -224,7 +231,6 @@ module Portage
|
|
|
224
231
|
puts opts[:json] ? JSON.pretty_generate(report) : format_check(report)
|
|
225
232
|
Check::USABLE_VERDICTS.include?(report[:verdict]) ? 0 : 1
|
|
226
233
|
end
|
|
227
|
-
private_class_method :run_check
|
|
228
234
|
|
|
229
235
|
def self.parse_check_options(argv)
|
|
230
236
|
opts = { proxy: {} }
|
|
@@ -238,7 +244,6 @@ module Portage
|
|
|
238
244
|
warn USAGE
|
|
239
245
|
nil
|
|
240
246
|
end
|
|
241
|
-
private_class_method :parse_check_options
|
|
242
247
|
|
|
243
248
|
def self.valid_check_url?(url)
|
|
244
249
|
uri = URI.parse(url =~ %r{\Ahttps?://}i ? url : "https://#{url}")
|
|
@@ -246,7 +251,6 @@ module Portage
|
|
|
246
251
|
rescue URI::InvalidURIError
|
|
247
252
|
false
|
|
248
253
|
end
|
|
249
|
-
private_class_method :valid_check_url?
|
|
250
254
|
|
|
251
255
|
def self.format_check(report)
|
|
252
256
|
lines = ["#{report[:url]}: #{report[:verdict]}"]
|
|
@@ -256,7 +260,6 @@ module Portage
|
|
|
256
260
|
lines << " index: `#{report[:index_hint]}` adds its catalogue to your local index" if report[:index_hint]
|
|
257
261
|
lines.join("\n")
|
|
258
262
|
end
|
|
259
|
-
private_class_method :format_check
|
|
260
263
|
|
|
261
264
|
# --- compare ---
|
|
262
265
|
|
|
@@ -273,7 +276,6 @@ module Portage
|
|
|
273
276
|
puts json ? JSON.pretty_generate(report) : format_compare(report)
|
|
274
277
|
report[:offers].any? ? 0 : 1
|
|
275
278
|
end
|
|
276
|
-
private_class_method :run_compare
|
|
277
279
|
|
|
278
280
|
# Recorded as a search, not a purchase — compare never checks out. The
|
|
279
281
|
# query string names the compare so `portage history list` doesn't
|
|
@@ -287,7 +289,6 @@ module Portage
|
|
|
287
289
|
History.new.record_search(query: "compare: #{url} (product #{product_id})", offer_count: offers.length,
|
|
288
290
|
message: report[:message], offers: offers)
|
|
289
291
|
end
|
|
290
|
-
private_class_method :record_compare
|
|
291
292
|
|
|
292
293
|
def self.parse_compare_options(argv)
|
|
293
294
|
url = argv.first && !argv.first.start_with?("-") ? argv.shift : nil
|
|
@@ -301,7 +302,6 @@ module Portage
|
|
|
301
302
|
opts[:url] = url
|
|
302
303
|
opts
|
|
303
304
|
end
|
|
304
|
-
private_class_method :parse_compare_options
|
|
305
305
|
|
|
306
306
|
def self.compare_option_parser(opts)
|
|
307
307
|
opts[:proxy] = {}
|
|
@@ -314,7 +314,6 @@ module Portage
|
|
|
314
314
|
ProxySettings.add_options(parser, opts[:proxy])
|
|
315
315
|
end
|
|
316
316
|
end
|
|
317
|
-
private_class_method :compare_option_parser
|
|
318
317
|
|
|
319
318
|
# --- buy ---
|
|
320
319
|
|
|
@@ -332,7 +331,6 @@ module Portage
|
|
|
332
331
|
|
|
333
332
|
dispatch_buy(parsed, url)
|
|
334
333
|
end
|
|
335
|
-
private_class_method :run_buy
|
|
336
334
|
|
|
337
335
|
# A saved quote or offer names its own store; otherwise a url does, and
|
|
338
336
|
# with neither the search picks one.
|
|
@@ -343,7 +341,6 @@ module Portage
|
|
|
343
341
|
|
|
344
342
|
buy_from_search(parsed)
|
|
345
343
|
end
|
|
346
|
-
private_class_method :dispatch_buy
|
|
347
344
|
|
|
348
345
|
# Built and validated up front, same posture as #confidence_check — an
|
|
349
346
|
# unknown --handoff-target/PORTAGE_HANDOFF_TARGET/config.json value is a
|
|
@@ -355,7 +352,6 @@ module Portage
|
|
|
355
352
|
rescue ArgumentError => e
|
|
356
353
|
invalid_buy_option(e.message, url: url, json: parsed[:json])
|
|
357
354
|
end
|
|
358
|
-
private_class_method :handoff_target
|
|
359
355
|
|
|
360
356
|
# `portage buy` with no URL: search first, then buy from the store the
|
|
361
357
|
# caller picks. `--yes` alone deliberately isn't enough to get here —
|
|
@@ -373,7 +369,6 @@ module Portage
|
|
|
373
369
|
parsed[:page] = { title: offer[:title], url: offer[:url] }
|
|
374
370
|
execute_buy(parsed, offer[:store], product_id: offer[:product_id])
|
|
375
371
|
end
|
|
376
|
-
private_class_method :buy_from_search
|
|
377
372
|
|
|
378
373
|
# `--offer REF`: the store, product and query come from the saved
|
|
379
374
|
# `find` that produced the ref, as if they'd been passed as flags.
|
|
@@ -390,7 +385,6 @@ module Portage
|
|
|
390
385
|
parsed[:buy][:query] = offer["query"].to_s
|
|
391
386
|
execute_buy(parsed, offer["store"], product_id: offer["product_id"])
|
|
392
387
|
end
|
|
393
|
-
private_class_method :buy_from_offer
|
|
394
388
|
|
|
395
389
|
# `--quote QUOTE_ID`: buys what a `--dry-run` showed. Buy is handed the
|
|
396
390
|
# quoted total as a cap and refuses (`quote_changed`) if the real
|
|
@@ -414,10 +408,10 @@ module Portage
|
|
|
414
408
|
parsed[:quote_record] = quote
|
|
415
409
|
buy = parsed[:buy]
|
|
416
410
|
buy.merge!(qty: quote["qty"], product_id: quote["product_id"], query: quote["query"].to_s)
|
|
417
|
-
buy.merge!(quote_total: quote["total"], quote_currency: quote["currency"]
|
|
411
|
+
buy.merge!(quote_total: quote["total"], quote_currency: quote["currency"], quote_store: quote["store"],
|
|
412
|
+
quote_title: quote["title"])
|
|
418
413
|
execute_buy(parsed, quote["store"])
|
|
419
414
|
end
|
|
420
|
-
private_class_method :buy_from_quote
|
|
421
415
|
|
|
422
416
|
def self.usable_quote(parsed)
|
|
423
417
|
quote = Quotes.new.find(parsed[:quote])
|
|
@@ -430,7 +424,6 @@ module Portage
|
|
|
430
424
|
invalid_buy_option("Quote #{parsed[:quote]} has already been used — dry-run again for a new one.",
|
|
431
425
|
url: quote["store"], json: parsed[:json], outcome: "quote_used")
|
|
432
426
|
end
|
|
433
|
-
private_class_method :usable_quote
|
|
434
427
|
|
|
435
428
|
def self.refuse_unapproved_quote(parsed, quote, level)
|
|
436
429
|
summary = Approve.summary(quote)
|
|
@@ -439,7 +432,6 @@ module Portage
|
|
|
439
432
|
print_buy_report(report, nil, parsed[:json])
|
|
440
433
|
buy_exit_code(report)
|
|
441
434
|
end
|
|
442
|
-
private_class_method :refuse_unapproved_quote
|
|
443
435
|
|
|
444
436
|
# With a terminal (and no --json) the person picks there, through the
|
|
445
437
|
# same HumanPrompt numbered pick `portage pick` uses; otherwise the
|
|
@@ -453,7 +445,6 @@ module Portage
|
|
|
453
445
|
|
|
454
446
|
prompt_for_offer(prompt, report)
|
|
455
447
|
end
|
|
456
|
-
private_class_method :pick_offer
|
|
457
448
|
|
|
458
449
|
def self.prompt_for_offer(prompt, report)
|
|
459
450
|
prompt.say(report[:message].to_s)
|
|
@@ -461,7 +452,6 @@ module Portage
|
|
|
461
452
|
index = prompt.choose("Pick one to buy", choices, view: OfferChoice.method(:view_message))
|
|
462
453
|
index && report[:offers][index]
|
|
463
454
|
end
|
|
464
|
-
private_class_method :prompt_for_offer
|
|
465
455
|
|
|
466
456
|
# docs/plans/human-pick-and-approve.md Phase 2: under
|
|
467
457
|
# `--require-approval any|person` a real `--yes` run with no approved
|
|
@@ -481,7 +471,6 @@ module Portage
|
|
|
481
471
|
print_buy_report(report, result, parsed[:json])
|
|
482
472
|
buy_exit_code(report)
|
|
483
473
|
end
|
|
484
|
-
private_class_method :execute_buy
|
|
485
474
|
|
|
486
475
|
def self.buy_options(parsed, url, product_id: nil)
|
|
487
476
|
options = parsed[:buy].merge(url: url, confidence_check: parsed[:confidence_check],
|
|
@@ -490,23 +479,19 @@ module Portage
|
|
|
490
479
|
options[:webmcp_bridge] = profile_webmcp_bridge(url) if parsed[:handoff_target].profile?
|
|
491
480
|
options
|
|
492
481
|
end
|
|
493
|
-
private_class_method :buy_options
|
|
494
482
|
|
|
495
483
|
# `needs_approval` exits 0 like Buy's own `needs_confirmation`: the run
|
|
496
484
|
# did what it could and is waiting on the person.
|
|
497
485
|
def self.buy_exit_code(report)
|
|
498
486
|
report[:outcome] == "needs_approval" || report[:checkout] || report[:browse] ? 0 : 1
|
|
499
487
|
end
|
|
500
|
-
private_class_method :buy_exit_code
|
|
501
488
|
|
|
502
489
|
def self.real_run?(parsed) = parsed[:buy][:yes] && !parsed[:buy][:dry_run]
|
|
503
|
-
private_class_method :real_run?
|
|
504
490
|
|
|
505
491
|
# An approved quote already passed ApprovalPolicy in #buy_from_quote.
|
|
506
492
|
def self.approval_gate?(parsed)
|
|
507
493
|
real_run?(parsed) && !parsed[:quote_record] && ApprovalPolicy.level != "off"
|
|
508
494
|
end
|
|
509
|
-
private_class_method :approval_gate?
|
|
510
495
|
|
|
511
496
|
# A gated run that got as far as a priced checkout saved a quote; one
|
|
512
497
|
# that didn't (no match, a dead end) is reported as it is — nothing
|
|
@@ -518,7 +503,6 @@ module Portage
|
|
|
518
503
|
summary = Approve.summary(quote)
|
|
519
504
|
report.merge(Approve.needs_approval(summary, message: approval_message(summary, ApprovalPolicy.level)))
|
|
520
505
|
end
|
|
521
|
-
private_class_method :needs_approval_report
|
|
522
506
|
|
|
523
507
|
def self.approval_message(summary, level)
|
|
524
508
|
id = summary[:quote_id]
|
|
@@ -530,7 +514,6 @@ module Portage
|
|
|
530
514
|
how = level == "person" ? "`portage approve #{id} --via tty` at a terminal" : "`portage approve #{id}`"
|
|
531
515
|
"Nothing was bought. #{held} Approve it with #{how}, then run `portage buy --quote #{id} --yes`."
|
|
532
516
|
end
|
|
533
|
-
private_class_method :approval_message
|
|
534
517
|
|
|
535
518
|
# A dry run saves a quote and reports its `quote_id`. A run of a saved
|
|
536
519
|
# quote spends it once it purchases or hands off; any other outcome
|
|
@@ -549,7 +532,6 @@ module Portage
|
|
|
549
532
|
**quote_page(report, parsed))
|
|
550
533
|
saved ? report.merge(quote_id: saved["quote_id"]) : report
|
|
551
534
|
end
|
|
552
|
-
private_class_method :settle_quote
|
|
553
535
|
|
|
554
536
|
# What `portage approve` shows (docs/plans/human-pick-and-approve.md
|
|
555
537
|
# Phase 2): the title of what's in the checkout, else the picked
|
|
@@ -558,7 +540,6 @@ module Portage
|
|
|
558
540
|
page = parsed[:page] || {}
|
|
559
541
|
{ title: Array(report[:items]).first&.dig(:title) || page[:title], url: page[:url] }
|
|
560
542
|
end
|
|
561
|
-
private_class_method :quote_page
|
|
562
543
|
|
|
563
544
|
# docs/plans/buy-skill-and-local-browser.md Phase 6: `--handoff-target
|
|
564
545
|
# profile` gives `portage buy` a browser of its own — the Portage
|
|
@@ -587,7 +568,6 @@ module Portage
|
|
|
587
568
|
rescue StandardError
|
|
588
569
|
nil
|
|
589
570
|
end
|
|
590
|
-
private_class_method :profile_webmcp_bridge
|
|
591
571
|
|
|
592
572
|
# Same "bare host gets an https:// prefix" normalization Buy#initialize
|
|
593
573
|
# applies to the same `url` — done again here since this runs before
|
|
@@ -598,7 +578,6 @@ module Portage
|
|
|
598
578
|
raw = url.to_s.strip
|
|
599
579
|
raw =~ %r{\Ahttps?://}i ? raw : "https://#{raw}"
|
|
600
580
|
end
|
|
601
|
-
private_class_method :absolute_url
|
|
602
581
|
|
|
603
582
|
# An existing tab already on this store's host, so a shopper who's
|
|
604
583
|
# mid-session there isn't yanked to a fresh one; otherwise a brand new
|
|
@@ -609,14 +588,12 @@ module Portage
|
|
|
609
588
|
.find { |t| t["type"] == "page" && same_host?(t["url"], host) }
|
|
610
589
|
existing || BrowserProfile::Cdp.new_tab(port: profile.port, url: url)
|
|
611
590
|
end
|
|
612
|
-
private_class_method :browser_profile_target
|
|
613
591
|
|
|
614
592
|
def self.same_host?(url, host)
|
|
615
593
|
URI(url.to_s).host == host
|
|
616
594
|
rescue URI::InvalidURIError
|
|
617
595
|
false
|
|
618
596
|
end
|
|
619
|
-
private_class_method :same_host?
|
|
620
597
|
|
|
621
598
|
# docs/plans/handoff-reconcile.md Phase 3 — `portage buy --wait`. A
|
|
622
599
|
# no-op (returns nil) whenever there's nothing to wait on: --dry-run
|
|
@@ -646,12 +623,10 @@ module Portage
|
|
|
646
623
|
wait_timeout_override: parsed[:wait_timeout])
|
|
647
624
|
waiter.call(record) { |event, result| emit_wait_event(event, result, json) }
|
|
648
625
|
end
|
|
649
|
-
private_class_method :wait_for_handoff
|
|
650
626
|
|
|
651
627
|
def self.pending_shopper_record(checkout_id, transaction_log)
|
|
652
628
|
transaction_log.each_record.find { |r| r["checkout_id"] == checkout_id && r["settled_by"] == "shopper" }
|
|
653
629
|
end
|
|
654
|
-
private_class_method :pending_shopper_record
|
|
655
630
|
|
|
656
631
|
# `terminal` is forced on for a plain-text wait (see ReconcileNotify) so
|
|
657
632
|
# the shopper sees a line the moment it settles even with nothing
|
|
@@ -661,13 +636,11 @@ module Portage
|
|
|
661
636
|
def self.wait_notifier(json)
|
|
662
637
|
ReconcileNotifier.new(channels: ReconcileNotify.resolve(extra: json ? [] : ["terminal"]))
|
|
663
638
|
end
|
|
664
|
-
private_class_method :wait_notifier
|
|
665
639
|
|
|
666
640
|
def self.handoff_event(report)
|
|
667
641
|
{ event: "handoff", checkout_id: report[:checkout_id], checkout_url: report[:checkout_url],
|
|
668
642
|
reason: report[:outcome] }
|
|
669
643
|
end
|
|
670
|
-
private_class_method :handoff_event
|
|
671
644
|
|
|
672
645
|
# Only `:settled` ever reaches stdout as `handoff_settled` — a timeout or
|
|
673
646
|
# an interrupted wait leaves `result.settled` false, and that's already
|
|
@@ -683,16 +656,13 @@ module Portage
|
|
|
683
656
|
order_id: result.order_id, amount: result.amount, currency: result.currency }.compact)
|
|
684
657
|
end
|
|
685
658
|
end
|
|
686
|
-
private_class_method :emit_wait_event
|
|
687
659
|
|
|
688
660
|
def self.emit_ndjson(payload) = puts JSON.generate(payload)
|
|
689
|
-
private_class_method :emit_ndjson
|
|
690
661
|
|
|
691
662
|
def self.print_buy_report(report, result, json)
|
|
692
663
|
report = report.merge(reconcile: result.to_h) if result
|
|
693
664
|
puts json ? JSON.pretty_generate(report) : format_report(report)
|
|
694
665
|
end
|
|
695
|
-
private_class_method :print_buy_report
|
|
696
666
|
|
|
697
667
|
# Built before the buy starts (and before the search, when there's no
|
|
698
668
|
# URL), so a bad --min-confidence, or a bad PORTAGE_MIN_CONFIDENCE with
|
|
@@ -703,7 +673,6 @@ module Portage
|
|
|
703
673
|
rescue ArgumentError => e
|
|
704
674
|
invalid_buy_option(e.message, url: url, json: parsed[:json])
|
|
705
675
|
end
|
|
706
|
-
private_class_method :confidence_check
|
|
707
676
|
|
|
708
677
|
# A buy refused before it started. Under --json that's a report like
|
|
709
678
|
# any other, with outcome `invalid_option`, so an agent loop reading
|
|
@@ -719,7 +688,6 @@ module Portage
|
|
|
719
688
|
outcome: outcome, browse: false, checkout: false, message: message)
|
|
720
689
|
nil
|
|
721
690
|
end
|
|
722
|
-
private_class_method :invalid_buy_option
|
|
723
691
|
|
|
724
692
|
# A buy that created a checkout is a purchase entry, whatever its
|
|
725
693
|
# outcome. One that never got that far (no match, browse-only, dead end,
|
|
@@ -739,12 +707,10 @@ module Portage
|
|
|
739
707
|
items: Array(report[:items]).map { |item| item.transform_keys(&:to_s) }, message: report[:message]
|
|
740
708
|
)
|
|
741
709
|
end
|
|
742
|
-
private_class_method :record_buy
|
|
743
710
|
|
|
744
711
|
def self.report_total(report)
|
|
745
712
|
Portage::Ucp::Support::Totals.amount(report[:totals])
|
|
746
713
|
end
|
|
747
|
-
private_class_method :report_total
|
|
748
714
|
|
|
749
715
|
# A flag OptionParser can't read (`--min-confidence high`, `--qty two`,
|
|
750
716
|
# an unknown flag) is refused the same way as an out-of-range
|
|
@@ -768,12 +734,10 @@ module Portage
|
|
|
768
734
|
rescue OptionParser::ParseError => e
|
|
769
735
|
invalid_buy_option(e.message, url: url, json: json)
|
|
770
736
|
end
|
|
771
|
-
private_class_method :parse_buy_options
|
|
772
737
|
|
|
773
738
|
def self.buy_target?(url, buy, parsed)
|
|
774
739
|
url || !buy[:query].strip.empty? || parsed[:offer] || parsed[:quote]
|
|
775
740
|
end
|
|
776
|
-
private_class_method :buy_target?
|
|
777
741
|
|
|
778
742
|
# Bare arg is normally the store URL (`portage buy <url> --query "..."`),
|
|
779
743
|
# but `portage buy "coffee"` — no --query, and "coffee" doesn't look like
|
|
@@ -788,10 +752,8 @@ module Portage
|
|
|
788
752
|
parsed[:find][:query] = buy[:query] = url
|
|
789
753
|
nil
|
|
790
754
|
end
|
|
791
|
-
private_class_method :reinterpret_bare_query
|
|
792
755
|
|
|
793
756
|
def self.url_like?(text) = text.match?(%r{\A[a-z][a-z0-9+.-]*://}i) || text.include?(".")
|
|
794
|
-
private_class_method :url_like?
|
|
795
757
|
|
|
796
758
|
def self.buy_option_parser(buy, parsed)
|
|
797
759
|
OptionParser.new do |parser|
|
|
@@ -808,7 +770,6 @@ module Portage
|
|
|
808
770
|
add_confidence_options(parser, parsed[:confidence])
|
|
809
771
|
end
|
|
810
772
|
end
|
|
811
|
-
private_class_method :buy_option_parser
|
|
812
773
|
|
|
813
774
|
# The opt-in confidence gate in front of a `--yes` completion (see
|
|
814
775
|
# ConfidenceCheck) — both default to their PORTAGE_* env vars.
|
|
@@ -816,14 +777,12 @@ module Portage
|
|
|
816
777
|
parser.on("--decision-backend NAME") { |v| confidence[:backend] = v }
|
|
817
778
|
parser.on("--min-confidence N", Float) { |v| confidence[:threshold] = v }
|
|
818
779
|
end
|
|
819
|
-
private_class_method :add_confidence_options
|
|
820
780
|
|
|
821
781
|
def self.add_handoff_options(parser, buy)
|
|
822
782
|
parser.on("--[no-]auto-open") { |v| buy[:auto_open] = v }
|
|
823
783
|
parser.on("--notify-webhook URL") { |v| buy[:notify_webhook] = v }
|
|
824
784
|
parser.on("--handoff-target TARGET") { |v| buy[:handoff_target] = v }
|
|
825
785
|
end
|
|
826
|
-
private_class_method :add_handoff_options
|
|
827
786
|
|
|
828
787
|
# docs/plans/handoff-reconcile.md Phase 3 — `--wait`/`--wait-timeout`
|
|
829
788
|
# land on `parsed`, never on `buy`: they're consumed by
|
|
@@ -833,7 +792,6 @@ module Portage
|
|
|
833
792
|
parser.on("--wait") { parsed[:wait] = true }
|
|
834
793
|
parser.on("--wait-timeout DURATION") { |v| parsed[:wait_timeout] = v }
|
|
835
794
|
end
|
|
836
|
-
private_class_method :add_wait_options
|
|
837
795
|
|
|
838
796
|
# `--query` feeds both halves: it's the store search when there's no URL
|
|
839
797
|
# and the catalog search once a store is settled, so it's registered once
|
|
@@ -846,7 +804,6 @@ module Portage
|
|
|
846
804
|
parser.on("--limit N", Integer) { |v| parsed[:find][:limit] = v }
|
|
847
805
|
parser.on("--max-price N", Float) { |v| parsed[:find][:max_price] = buy[:max_price] = to_minor_units(v) }
|
|
848
806
|
end
|
|
849
|
-
private_class_method :add_search_options
|
|
850
807
|
|
|
851
808
|
# --- pick / approve (docs/plans/human-pick-and-approve.md Phase 2) ---
|
|
852
809
|
|
|
@@ -868,7 +825,6 @@ module Portage
|
|
|
868
825
|
**opts.slice(:search, :choose, :view, :compare))
|
|
869
826
|
print_prompt_result(pick.call, opts[:json])
|
|
870
827
|
end
|
|
871
|
-
private_class_method :run_pick
|
|
872
828
|
|
|
873
829
|
def self.run_approve(argv)
|
|
874
830
|
quote_id = argv.first && !argv.first.start_with?("-") ? argv.shift : nil
|
|
@@ -883,7 +839,6 @@ module Portage
|
|
|
883
839
|
**opts.slice(:relayed_yes, :view))
|
|
884
840
|
print_prompt_result(approve.call, opts[:json])
|
|
885
841
|
end
|
|
886
|
-
private_class_method :run_approve
|
|
887
842
|
|
|
888
843
|
# `--via`/`--json` for both commands, plus whatever the block adds.
|
|
889
844
|
# @return [Hash, nil] nil on a bad flag (already reported).
|
|
@@ -900,13 +855,11 @@ module Portage
|
|
|
900
855
|
prompt_usage(json, e.message)
|
|
901
856
|
nil
|
|
902
857
|
end
|
|
903
|
-
private_class_method :parse_prompt_options
|
|
904
858
|
|
|
905
859
|
def self.prompt_usage(json, message)
|
|
906
860
|
json ? puts(JSON.pretty_generate(outcome: "invalid_option", message: message)) : warn("#{message}\n#{USAGE}")
|
|
907
861
|
1
|
|
908
862
|
end
|
|
909
|
-
private_class_method :prompt_usage
|
|
910
863
|
|
|
911
864
|
# Pick's "Compare an offer across stores": the same Compare run
|
|
912
865
|
# `portage compare` does, from the saved offer's store and product.
|
|
@@ -915,13 +868,11 @@ module Portage
|
|
|
915
868
|
|
|
916
869
|
Compare.new(origin_url: offer["store"], origin_product_id: offer["product_id"]).call
|
|
917
870
|
end
|
|
918
|
-
private_class_method :compare_offer
|
|
919
871
|
|
|
920
872
|
def self.print_prompt_result(result, json)
|
|
921
873
|
puts json ? JSON.pretty_generate(result) : format_prompt_result(result)
|
|
922
874
|
PROMPT_OK_OUTCOMES.include?(result[:outcome]) ? 0 : 1
|
|
923
875
|
end
|
|
924
|
-
private_class_method :print_prompt_result
|
|
925
876
|
|
|
926
877
|
def self.format_prompt_result(result)
|
|
927
878
|
lines = ["[#{result[:outcome]}] #{result[:message]}"]
|
|
@@ -929,12 +880,10 @@ module Portage
|
|
|
929
880
|
lines << " #{approval_line(result[:summary])}" if result[:summary]
|
|
930
881
|
lines.join("\n")
|
|
931
882
|
end
|
|
932
|
-
private_class_method :format_prompt_result
|
|
933
883
|
|
|
934
884
|
def self.choice_line(choice)
|
|
935
885
|
[choice[:label], choice[:url], ("ref #{choice[:ref]}" if choice[:ref])].compact.join(" — ")
|
|
936
886
|
end
|
|
937
|
-
private_class_method :choice_line
|
|
938
887
|
|
|
939
888
|
# --- history ---
|
|
940
889
|
|
|
@@ -948,7 +897,6 @@ module Portage
|
|
|
948
897
|
1
|
|
949
898
|
end
|
|
950
899
|
end
|
|
951
|
-
private_class_method :run_history
|
|
952
900
|
|
|
953
901
|
def self.run_history_list(argv)
|
|
954
902
|
opts = { limit: History::MAX_ENTRIES }
|
|
@@ -961,7 +909,6 @@ module Portage
|
|
|
961
909
|
puts json ? JSON.pretty_generate(result) : format_history(result)
|
|
962
910
|
0
|
|
963
911
|
end
|
|
964
|
-
private_class_method :run_history_list
|
|
965
912
|
|
|
966
913
|
def self.run_history_clear(argv)
|
|
967
914
|
opts = {}
|
|
@@ -970,7 +917,6 @@ module Portage
|
|
|
970
917
|
puts "Cleared #{opts[:kind] || 'purchase and search'} history."
|
|
971
918
|
0
|
|
972
919
|
end
|
|
973
|
-
private_class_method :run_history_clear
|
|
974
920
|
|
|
975
921
|
def self.history_option_parser(opts)
|
|
976
922
|
OptionParser.new do |parser|
|
|
@@ -980,7 +926,6 @@ module Portage
|
|
|
980
926
|
parser.on("--json") { opts[:json] = true }
|
|
981
927
|
end
|
|
982
928
|
end
|
|
983
|
-
private_class_method :history_option_parser
|
|
984
929
|
|
|
985
930
|
def self.format_history(result)
|
|
986
931
|
lines = ["Purchases:"]
|
|
@@ -991,7 +936,6 @@ module Portage
|
|
|
991
936
|
lines << "(none)" if result[:searches].empty?
|
|
992
937
|
lines.join("\n")
|
|
993
938
|
end
|
|
994
|
-
private_class_method :format_history
|
|
995
939
|
|
|
996
940
|
# `outcome` first, since it's what the entry is for. Entries recorded
|
|
997
941
|
# before `outcome` existed fall back to their checkout_status/message.
|
|
@@ -1000,11 +944,10 @@ module Portage
|
|
|
1000
944
|
[
|
|
1001
945
|
"#{Time.at(entry['at'])} — #{entry['outcome'] || entry['checkout_status'] || entry['message']}",
|
|
1002
946
|
"#{entry['url']} (#{entry['query']})", (items unless items.empty?),
|
|
1003
|
-
(format_amount(entry["total"], entry["currency"]) if entry["total"]),
|
|
947
|
+
(Money.format_amount(entry["total"], entry["currency"]) if entry["total"]),
|
|
1004
948
|
(entry["checkout_url"] unless entry["outcome"] == "purchased")
|
|
1005
949
|
].compact.join(" — ")
|
|
1006
950
|
end
|
|
1007
|
-
private_class_method :history_purchase_line
|
|
1008
951
|
|
|
1009
952
|
# Takes a report's symbol-keyed item or a history entry's string-keyed
|
|
1010
953
|
# one.
|
|
@@ -1012,13 +955,11 @@ module Portage
|
|
|
1012
955
|
item = item.transform_keys(&:to_s)
|
|
1013
956
|
"#{item['title'] || item['id']} x#{item['quantity']}"
|
|
1014
957
|
end
|
|
1015
|
-
private_class_method :item_label
|
|
1016
958
|
|
|
1017
959
|
def self.history_search_line(entry)
|
|
1018
960
|
where = entry["url"] ? " at #{entry['url']}" : ""
|
|
1019
961
|
"#{Time.at(entry['at'])} — \"#{entry['query']}\"#{where} — #{entry['offer_count']} result(s)"
|
|
1020
962
|
end
|
|
1021
|
-
private_class_method :history_search_line
|
|
1022
963
|
|
|
1023
964
|
# --- payment ---
|
|
1024
965
|
|
|
@@ -1038,7 +979,6 @@ module Portage
|
|
|
1038
979
|
warn USAGE
|
|
1039
980
|
1
|
|
1040
981
|
end
|
|
1041
|
-
private_class_method :run_payment
|
|
1042
982
|
|
|
1043
983
|
def self.run_payment_list(argv)
|
|
1044
984
|
json = false
|
|
@@ -1047,7 +987,6 @@ module Portage
|
|
|
1047
987
|
puts json ? JSON.pretty_generate(methods) : format_payment_list(methods)
|
|
1048
988
|
0
|
|
1049
989
|
end
|
|
1050
|
-
private_class_method :run_payment_list
|
|
1051
990
|
|
|
1052
991
|
# Shared by set-default/remove/freeze/revoke — each takes exactly one
|
|
1053
992
|
# `<id>` positional arg and reports the (now-updated) entry, or fails
|
|
@@ -1066,7 +1005,6 @@ module Portage
|
|
|
1066
1005
|
warn "No payment method enrolled with id #{id}."
|
|
1067
1006
|
1
|
|
1068
1007
|
end
|
|
1069
|
-
private_class_method :run_payment_mutate
|
|
1070
1008
|
|
|
1071
1009
|
def self.parse_payment_enroll_options(argv)
|
|
1072
1010
|
opts = { scope_merchants: [], proxy: {} }
|
|
@@ -1081,7 +1019,6 @@ module Portage
|
|
|
1081
1019
|
opts[:url] = argv.first && !argv.first.start_with?("-") ? argv.shift : nil
|
|
1082
1020
|
opts
|
|
1083
1021
|
end
|
|
1084
|
-
private_class_method :parse_payment_enroll_options
|
|
1085
1022
|
|
|
1086
1023
|
# nil (not `{}`) when no --scope-* flag was given at all, so
|
|
1087
1024
|
# PaymentMethods#enroll's `scope:` default of no-scope-written stays the
|
|
@@ -1093,7 +1030,6 @@ module Portage
|
|
|
1093
1030
|
{ merchants: opts[:scope_merchants], max_amount: opts[:scope_max_amount], currency: opts[:scope_currency] }
|
|
1094
1031
|
.compact
|
|
1095
1032
|
end
|
|
1096
|
-
private_class_method :payment_enroll_scope
|
|
1097
1033
|
|
|
1098
1034
|
def self.run_payment_enroll(argv)
|
|
1099
1035
|
opts = parse_payment_enroll_options(argv)
|
|
@@ -1113,7 +1049,6 @@ module Portage
|
|
|
1113
1049
|
warn e.message
|
|
1114
1050
|
1
|
|
1115
1051
|
end
|
|
1116
|
-
private_class_method :run_payment_enroll
|
|
1117
1052
|
|
|
1118
1053
|
# --- policy ---
|
|
1119
1054
|
|
|
@@ -1127,7 +1062,6 @@ module Portage
|
|
|
1127
1062
|
1
|
|
1128
1063
|
end
|
|
1129
1064
|
end
|
|
1130
|
-
private_class_method :run_policy
|
|
1131
1065
|
|
|
1132
1066
|
# `require_approval` is always shown at its effective value, the
|
|
1133
1067
|
# default included, so "what does `--yes` need right now" is never a
|
|
@@ -1140,7 +1074,6 @@ module Portage
|
|
|
1140
1074
|
puts json ? JSON.pretty_generate(effective) : format_policy(policy)
|
|
1141
1075
|
0
|
|
1142
1076
|
end
|
|
1143
|
-
private_class_method :run_policy_show
|
|
1144
1077
|
|
|
1145
1078
|
def self.format_policy(policy)
|
|
1146
1079
|
spending = policy.to_h.except(ApprovalPolicy::KEY)
|
|
@@ -1148,7 +1081,6 @@ module Portage
|
|
|
1148
1081
|
default = " (default)" unless ApprovalPolicy.configured?(policy)
|
|
1149
1082
|
"#{body}\nrequire_approval: #{ApprovalPolicy.level(policy)}#{default}"
|
|
1150
1083
|
end
|
|
1151
|
-
private_class_method :format_policy
|
|
1152
1084
|
|
|
1153
1085
|
def self.parse_policy_set_options(argv)
|
|
1154
1086
|
opts = { allow: [] }
|
|
@@ -1162,7 +1094,6 @@ module Portage
|
|
|
1162
1094
|
end.parse!(argv)
|
|
1163
1095
|
opts
|
|
1164
1096
|
end
|
|
1165
|
-
private_class_method :parse_policy_set_options
|
|
1166
1097
|
|
|
1167
1098
|
def self.add_policy_cap_options(parser, opts)
|
|
1168
1099
|
parser.on("--per-transaction-cap N", Integer) { |v| opts[:per_transaction_cap] = v }
|
|
@@ -1170,7 +1101,6 @@ module Portage
|
|
|
1170
1101
|
parser.on("--rolling-window-seconds N", Integer) { |v| opts[:rolling_window_seconds] = v }
|
|
1171
1102
|
parser.on("--currency CUR") { |v| opts[:currency] = v }
|
|
1172
1103
|
end
|
|
1173
|
-
private_class_method :add_policy_cap_options
|
|
1174
1104
|
|
|
1175
1105
|
# Each `--*` group is applied independently and only when its required
|
|
1176
1106
|
# fields are present — `portage policy set --allow shop.example.com`
|
|
@@ -1194,7 +1124,6 @@ module Portage
|
|
|
1194
1124
|
warn "#{e.message}\n#{USAGE}"
|
|
1195
1125
|
1
|
|
1196
1126
|
end
|
|
1197
|
-
private_class_method :run_policy_set
|
|
1198
1127
|
|
|
1199
1128
|
# docs/plans/human-pick-and-approve.md Phase 2: raising the level (or
|
|
1200
1129
|
# setting the same one) needs nothing; lowering it (person -> any/off,
|
|
@@ -1211,7 +1140,6 @@ module Portage
|
|
|
1211
1140
|
policy.set(ApprovalPolicy::KEY, level)
|
|
1212
1141
|
true
|
|
1213
1142
|
end
|
|
1214
|
-
private_class_method :require_approval_applied?
|
|
1215
1143
|
|
|
1216
1144
|
def self.confirm_lowering(current, level)
|
|
1217
1145
|
prompt = HumanPrompt.new(via: "tty")
|
|
@@ -1224,14 +1152,12 @@ module Portage
|
|
|
1224
1152
|
"terminal here — nothing changed. Run it yourself from a terminal."
|
|
1225
1153
|
false
|
|
1226
1154
|
end
|
|
1227
|
-
private_class_method :confirm_lowering
|
|
1228
1155
|
|
|
1229
1156
|
def self.lowering_effect(level)
|
|
1230
1157
|
return "`portage buy --yes` would then buy without anyone approving the total." if level == "off"
|
|
1231
1158
|
|
|
1232
1159
|
"An agent relaying your yes would then be enough to buy."
|
|
1233
1160
|
end
|
|
1234
|
-
private_class_method :lowering_effect
|
|
1235
1161
|
|
|
1236
1162
|
def self.set_policy_cap(policy, opts)
|
|
1237
1163
|
if opts[:per_transaction_cap]
|
|
@@ -1243,14 +1169,12 @@ module Portage
|
|
|
1243
1169
|
policy.set("rolling_cap", { "amount" => opts[:rolling_cap], "currency" => require_currency!(opts),
|
|
1244
1170
|
"window_seconds" => opts[:rolling_window_seconds] })
|
|
1245
1171
|
end
|
|
1246
|
-
private_class_method :set_policy_cap
|
|
1247
1172
|
|
|
1248
1173
|
def self.set_policy_velocity(policy, opts)
|
|
1249
1174
|
return unless opts[:velocity_count] && opts[:velocity_window_seconds]
|
|
1250
1175
|
|
|
1251
1176
|
policy.set("velocity", { "count" => opts[:velocity_count], "window_seconds" => opts[:velocity_window_seconds] })
|
|
1252
1177
|
end
|
|
1253
|
-
private_class_method :set_policy_velocity
|
|
1254
1178
|
|
|
1255
1179
|
def self.set_policy_allowlist(policy, opts)
|
|
1256
1180
|
return policy.set("merchant_allowlist", []) if opts[:clear_allowlist]
|
|
@@ -1258,19 +1182,16 @@ module Portage
|
|
|
1258
1182
|
|
|
1259
1183
|
policy.set("merchant_allowlist", (policy.merchant_allowlist + opts[:allow]).uniq)
|
|
1260
1184
|
end
|
|
1261
|
-
private_class_method :set_policy_allowlist
|
|
1262
1185
|
|
|
1263
1186
|
def self.require_currency!(opts)
|
|
1264
1187
|
opts[:currency] || raise(ArgumentError, "--currency is required alongside a cap")
|
|
1265
1188
|
end
|
|
1266
|
-
private_class_method :require_currency!
|
|
1267
1189
|
|
|
1268
1190
|
def self.format_payment_list(methods)
|
|
1269
1191
|
return "(no payment methods enrolled)" if methods.empty?
|
|
1270
1192
|
|
|
1271
1193
|
methods.map { |m| "#{m['label']} (#{m['id']}) — default: #{m['default']}, frozen: #{m['frozen']}" }.join("\n")
|
|
1272
1194
|
end
|
|
1273
|
-
private_class_method :format_payment_list
|
|
1274
1195
|
|
|
1275
1196
|
def self.format_payment_enroll(result)
|
|
1276
1197
|
case result[:status]
|
|
@@ -1284,7 +1205,6 @@ module Portage
|
|
|
1284
1205
|
else "This store doesn't support payment enrollment."
|
|
1285
1206
|
end
|
|
1286
1207
|
end
|
|
1287
|
-
private_class_method :format_payment_enroll
|
|
1288
1208
|
|
|
1289
1209
|
# --- orders ---
|
|
1290
1210
|
|
|
@@ -1302,7 +1222,6 @@ module Portage
|
|
|
1302
1222
|
warn USAGE
|
|
1303
1223
|
1
|
|
1304
1224
|
end
|
|
1305
|
-
private_class_method :run_orders
|
|
1306
1225
|
|
|
1307
1226
|
def self.run_orders_reconcile(argv)
|
|
1308
1227
|
opts = {}
|
|
@@ -1318,14 +1237,12 @@ module Portage
|
|
|
1318
1237
|
puts opts[:json] ? JSON.pretty_generate(results.map(&:to_h)) : format_reconcile(results)
|
|
1319
1238
|
0
|
|
1320
1239
|
end
|
|
1321
|
-
private_class_method :run_orders_reconcile
|
|
1322
1240
|
|
|
1323
1241
|
def self.reconcile_records(checkout_id, transaction_log, reconciler)
|
|
1324
1242
|
return reconcile_one_checkout(checkout_id, transaction_log, reconciler) if checkout_id
|
|
1325
1243
|
|
|
1326
1244
|
HandoffReconciler.each_pending_shopper_record(transaction_log).map { |record| reconciler.call(record) }
|
|
1327
1245
|
end
|
|
1328
|
-
private_class_method :reconcile_records
|
|
1329
1246
|
|
|
1330
1247
|
# `--checkout ID` reconciles the one named record whatever its
|
|
1331
1248
|
# `settled_by`/status — #call itself still refuses to settle anything
|
|
@@ -1337,14 +1254,12 @@ module Portage
|
|
|
1337
1254
|
|
|
1338
1255
|
[reconciler.call(transaction_log.find(key))]
|
|
1339
1256
|
end
|
|
1340
|
-
private_class_method :reconcile_one_checkout
|
|
1341
1257
|
|
|
1342
1258
|
def self.format_reconcile(results)
|
|
1343
1259
|
return "(nothing to reconcile)" if results.empty?
|
|
1344
1260
|
|
|
1345
1261
|
results.map { |r| format_reconcile_result(r) }.join("\n")
|
|
1346
1262
|
end
|
|
1347
|
-
private_class_method :format_reconcile
|
|
1348
1263
|
|
|
1349
1264
|
def self.format_reconcile_result(result)
|
|
1350
1265
|
return "#{result.idempotency_key}: #{result.note}" unless result.settled
|
|
@@ -1352,10 +1267,9 @@ module Portage
|
|
|
1352
1267
|
parts = ["#{result.idempotency_key}: #{result.status}"]
|
|
1353
1268
|
parts << "resolution: #{result.resolution}" if result.resolution
|
|
1354
1269
|
parts << "order: #{result.order_id}" if result.order_id
|
|
1355
|
-
parts << format_amount(result.amount, result.currency) if result.amount
|
|
1270
|
+
parts << Money.format_amount(result.amount, result.currency) if result.amount
|
|
1356
1271
|
parts.join(" — ")
|
|
1357
1272
|
end
|
|
1358
|
-
private_class_method :format_reconcile_result
|
|
1359
1273
|
|
|
1360
1274
|
# --- index (docs/plans/buy-skill-and-local-browser.md Phase 2b) ---
|
|
1361
1275
|
|
|
@@ -1376,7 +1290,6 @@ module Portage
|
|
|
1376
1290
|
warn USAGE
|
|
1377
1291
|
1
|
|
1378
1292
|
end
|
|
1379
|
-
private_class_method :run_index
|
|
1380
1293
|
|
|
1381
1294
|
def self.parse_index_build_options(argv)
|
|
1382
1295
|
opts = { sources: nil, queries: nil, dry_run: false, json: false, export: nil }
|
|
@@ -1390,7 +1303,6 @@ module Portage
|
|
|
1390
1303
|
opts[:queries] &&= File.readlines(opts[:queries]).map(&:strip).reject(&:empty?)
|
|
1391
1304
|
opts
|
|
1392
1305
|
end
|
|
1393
|
-
private_class_method :parse_index_build_options
|
|
1394
1306
|
|
|
1395
1307
|
def self.run_index_build(argv, refresh:)
|
|
1396
1308
|
opts = parse_index_build_options(argv)
|
|
@@ -1403,10 +1315,8 @@ module Portage
|
|
|
1403
1315
|
puts opts[:json] ? JSON.pretty_generate(result) : format_index_build(result)
|
|
1404
1316
|
0
|
|
1405
1317
|
end
|
|
1406
|
-
private_class_method :run_index_build
|
|
1407
1318
|
|
|
1408
1319
|
def self.index_sources(names) = names ? Index::Sources.by_name(names) : nil
|
|
1409
|
-
private_class_method :index_sources
|
|
1410
1320
|
|
|
1411
1321
|
def self.format_index_build(result)
|
|
1412
1322
|
lines = ["Ran #{result[:sources_run].join(', ')} — #{result[:candidates]} candidate(s)."]
|
|
@@ -1420,7 +1330,6 @@ module Portage
|
|
|
1420
1330
|
end
|
|
1421
1331
|
lines.join("\n")
|
|
1422
1332
|
end
|
|
1423
|
-
private_class_method :format_index_build
|
|
1424
1333
|
|
|
1425
1334
|
INDEX_SHOW_PER_PAGE = 50
|
|
1426
1335
|
|
|
@@ -1438,12 +1347,10 @@ module Portage
|
|
|
1438
1347
|
puts opts[:json] ? JSON.pretty_generate(result) : format_index_show(result)
|
|
1439
1348
|
0
|
|
1440
1349
|
end
|
|
1441
|
-
private_class_method :run_index_show
|
|
1442
1350
|
|
|
1443
1351
|
def self.index_show_all(kind)
|
|
1444
1352
|
{ stores: kind == "products" ? [] : Index::Store.new.all, products: kind == "stores" ? [] : Index::ProductStore.new.all }
|
|
1445
1353
|
end
|
|
1446
|
-
private_class_method :index_show_all
|
|
1447
1354
|
|
|
1448
1355
|
# `--products` pages (docs/plans/local-catalogue.md Phase 2): a crawled
|
|
1449
1356
|
# index can hold thousands.
|
|
@@ -1452,7 +1359,6 @@ module Portage
|
|
|
1452
1359
|
{ stores: [], products: products.page(opts[:page], per_page: opts[:per_page]), page: opts[:page],
|
|
1453
1360
|
per_page: opts[:per_page], products_total: products.count }
|
|
1454
1361
|
end
|
|
1455
|
-
private_class_method :index_products_page
|
|
1456
1362
|
|
|
1457
1363
|
def self.format_index_show(result)
|
|
1458
1364
|
paged = result.key?(:page)
|
|
@@ -1463,13 +1369,11 @@ module Portage
|
|
|
1463
1369
|
lines << index_page_line(result) if paged
|
|
1464
1370
|
lines.join("\n")
|
|
1465
1371
|
end
|
|
1466
|
-
private_class_method :format_index_show
|
|
1467
1372
|
|
|
1468
1373
|
def self.index_store_lines(stores)
|
|
1469
1374
|
lines = ["Stores:"] + stores.map { |s| " #{s['origin']} (#{Array(s['sources']).join(', ')})" }
|
|
1470
1375
|
stores.empty? ? lines + ["(none)"] : lines
|
|
1471
1376
|
end
|
|
1472
|
-
private_class_method :index_store_lines
|
|
1473
1377
|
|
|
1474
1378
|
def self.index_page_line(result)
|
|
1475
1379
|
first = ((result[:page] - 1) * result[:per_page]) + 1
|
|
@@ -1479,7 +1383,6 @@ module Portage
|
|
|
1479
1383
|
"Showing #{first}-#{last} of #{result[:products_total]}. Next: --page #{result[:page] + 1}" \
|
|
1480
1384
|
"#{" --per-page #{result[:per_page]}" unless result[:per_page] == INDEX_SHOW_PER_PAGE}"
|
|
1481
1385
|
end
|
|
1482
|
-
private_class_method :index_page_line
|
|
1483
1386
|
|
|
1484
1387
|
# `portage index search` (docs/plans/local-catalogue.md Phase 2): local
|
|
1485
1388
|
# index entries only, no request. Exits 1 with no hits.
|
|
@@ -1498,7 +1401,6 @@ module Portage
|
|
|
1498
1401
|
puts opts[:json] ? JSON.pretty_generate(result) : format_index_search(result)
|
|
1499
1402
|
result[:products].empty? ? 1 : 0
|
|
1500
1403
|
end
|
|
1501
|
-
private_class_method :run_index_search
|
|
1502
1404
|
|
|
1503
1405
|
def self.index_search(query, opts)
|
|
1504
1406
|
products = Index::ProductStore.new
|
|
@@ -1506,24 +1408,22 @@ module Portage
|
|
|
1506
1408
|
hits = products.search(query, **filters).map { |entry| entry.merge("product" => Index::EntryProduct.wire(entry)) }
|
|
1507
1409
|
{ query: query, engine: products.search_engine, live: false, filters: filters, products: hits }
|
|
1508
1410
|
end
|
|
1509
|
-
private_class_method :index_search
|
|
1510
1411
|
|
|
1511
1412
|
def self.format_index_search(result)
|
|
1512
1413
|
return "No index products match \"#{result[:query]}\"." if result[:products].empty?
|
|
1513
1414
|
|
|
1514
1415
|
lines = result[:products].map { |p| index_search_line(p) }
|
|
1515
|
-
lines << "(From the local index, not live: check the price and stock with
|
|
1416
|
+
lines << "(From the local index, not live: check the price and stock with " \
|
|
1417
|
+
"`portage find --store URL --query ...`.)"
|
|
1516
1418
|
lines << "(FTS5 isn't available in this SQLite, so this was a plain text match.)" if result[:engine] == "like"
|
|
1517
1419
|
lines.join("\n")
|
|
1518
1420
|
end
|
|
1519
|
-
private_class_method :format_index_search
|
|
1520
1421
|
|
|
1521
1422
|
def self.index_search_line(product)
|
|
1522
1423
|
hosts = Array(product["stores"]).filter_map { |s| URI.parse(s["origin"].to_s).host }.join(", ")
|
|
1523
1424
|
["#{product['title']}#{" — #{product['brand']}" if product['brand']}", ("(#{hosts})" unless hosts.empty?),
|
|
1524
1425
|
product["url"]].compact.join(" ")
|
|
1525
1426
|
end
|
|
1526
|
-
private_class_method :index_search_line
|
|
1527
1427
|
|
|
1528
1428
|
def self.run_index_add(argv)
|
|
1529
1429
|
json = !argv.delete("--json").nil?
|
|
@@ -1538,7 +1438,6 @@ module Portage
|
|
|
1538
1438
|
puts json ? JSON.pretty_generate(result) : result[:message]
|
|
1539
1439
|
result[:added] ? 0 : 1
|
|
1540
1440
|
end
|
|
1541
|
-
private_class_method :run_index_add
|
|
1542
1441
|
|
|
1543
1442
|
def self.run_index_remove(argv)
|
|
1544
1443
|
json = argv.delete("--json") ? true : false
|
|
@@ -1552,7 +1451,6 @@ module Portage
|
|
|
1552
1451
|
puts json ? JSON.pretty_generate(result) : result[:message]
|
|
1553
1452
|
result[:removed] ? 0 : 1
|
|
1554
1453
|
end
|
|
1555
|
-
private_class_method :run_index_remove
|
|
1556
1454
|
|
|
1557
1455
|
def self.run_index_sources(argv)
|
|
1558
1456
|
json = argv.delete("--json") ? true : false
|
|
@@ -1560,12 +1458,10 @@ module Portage
|
|
|
1560
1458
|
puts json ? JSON.pretty_generate(sources) : format_index_sources(sources)
|
|
1561
1459
|
0
|
|
1562
1460
|
end
|
|
1563
|
-
private_class_method :run_index_sources
|
|
1564
1461
|
|
|
1565
1462
|
def self.format_index_sources(sources)
|
|
1566
1463
|
sources.map { |s| "#{s[:name]}: #{s[:description]}#{" (#{s[:path]})" if s[:path]}" }.join("\n")
|
|
1567
1464
|
end
|
|
1568
|
-
private_class_method :format_index_sources
|
|
1569
1465
|
|
|
1570
1466
|
# --- browser (docs/plans/buy-skill-and-local-browser.md Phase 3) ---
|
|
1571
1467
|
|
|
@@ -1579,7 +1475,6 @@ module Portage
|
|
|
1579
1475
|
warn USAGE
|
|
1580
1476
|
1
|
|
1581
1477
|
end
|
|
1582
|
-
private_class_method :run_browser
|
|
1583
1478
|
|
|
1584
1479
|
def self.parse_browser_import_options(argv)
|
|
1585
1480
|
opts = { browser: nil, root: nil, history_days: BrowserImport::Importer::DEFAULT_HISTORY_DAYS,
|
|
@@ -1588,7 +1483,6 @@ module Portage
|
|
|
1588
1483
|
browser_import_option_parser(opts).parse!(argv)
|
|
1589
1484
|
opts
|
|
1590
1485
|
end
|
|
1591
|
-
private_class_method :parse_browser_import_options
|
|
1592
1486
|
|
|
1593
1487
|
def self.browser_import_option_parser(opts)
|
|
1594
1488
|
OptionParser.new do |parser|
|
|
@@ -1602,7 +1496,6 @@ module Portage
|
|
|
1602
1496
|
end
|
|
1603
1497
|
end
|
|
1604
1498
|
end
|
|
1605
|
-
private_class_method :browser_import_option_parser
|
|
1606
1499
|
|
|
1607
1500
|
# Reads, reduces and probes (BrowserImport::Importer#plan), shows the
|
|
1608
1501
|
# list, and saves only through BrowserImport::Confirm's gate: `--yes`,
|
|
@@ -1625,7 +1518,6 @@ module Portage
|
|
|
1625
1518
|
warn "#{e.message}\n#{USAGE}"
|
|
1626
1519
|
1
|
|
1627
1520
|
end
|
|
1628
|
-
private_class_method :run_browser_import
|
|
1629
1521
|
|
|
1630
1522
|
# The real HandoffOnly list (docs/plans/buy-skill-and-local-browser.md
|
|
1631
1523
|
# Phase 5) into Importer's own injectable `handoff_only_hosts:` seam
|
|
@@ -1635,7 +1527,6 @@ module Portage
|
|
|
1635
1527
|
def self.browser_importer
|
|
1636
1528
|
BrowserImport::Importer.new(handoff_only_hosts: HandoffOnly.new.hosts)
|
|
1637
1529
|
end
|
|
1638
|
-
private_class_method :browser_importer
|
|
1639
1530
|
|
|
1640
1531
|
def self.browser_import_options(opts)
|
|
1641
1532
|
browser = opts[:browser] || BrowserImport::Profiles.detect || "chrome"
|
|
@@ -1645,7 +1536,6 @@ module Portage
|
|
|
1645
1536
|
max_probes: opts[:max_probes], exclude: opts[:exclude]
|
|
1646
1537
|
)
|
|
1647
1538
|
end
|
|
1648
|
-
private_class_method :browser_import_options
|
|
1649
1539
|
|
|
1650
1540
|
BROWSER_IMPORT_MESSAGES = {
|
|
1651
1541
|
dry_run: "Dry run — nothing saved.",
|
|
@@ -1665,14 +1555,12 @@ module Portage
|
|
|
1665
1555
|
end
|
|
1666
1556
|
plan[:error] ? 1 : 0
|
|
1667
1557
|
end
|
|
1668
|
-
private_class_method :report_browser_import
|
|
1669
1558
|
|
|
1670
1559
|
def self.browser_import_message(decision, saved)
|
|
1671
1560
|
return "Saved #{saved[:stores]} store(s) and #{saved[:products]} product(s) to your local index." if saved
|
|
1672
1561
|
|
|
1673
1562
|
BROWSER_IMPORT_MESSAGES.fetch(decision, "Nothing saved.")
|
|
1674
1563
|
end
|
|
1675
|
-
private_class_method :browser_import_message
|
|
1676
1564
|
|
|
1677
1565
|
def self.format_browser_import(plan)
|
|
1678
1566
|
lines = browser_import_counts(plan)
|
|
@@ -1682,7 +1570,6 @@ module Portage
|
|
|
1682
1570
|
lines << "#{plan[:products].length} product page(s) to keep." if plan[:products].any?
|
|
1683
1571
|
lines.join("\n")
|
|
1684
1572
|
end
|
|
1685
|
-
private_class_method :format_browser_import
|
|
1686
1573
|
|
|
1687
1574
|
def self.browser_import_counts(plan)
|
|
1688
1575
|
skipped = plan[:skipped].map { |reason, n| "#{n} #{reason}" }.join(", ")
|
|
@@ -1693,14 +1580,12 @@ module Portage
|
|
|
1693
1580
|
lines << "Hit the #{plan[:probed]}-probe cap; #{plan[:unprobed]} domain(s) left unprobed." if plan[:capped]
|
|
1694
1581
|
lines
|
|
1695
1582
|
end
|
|
1696
|
-
private_class_method :browser_import_counts
|
|
1697
1583
|
|
|
1698
1584
|
def self.browser_import_line(entry)
|
|
1699
1585
|
categories = entry[:category_names].empty? ? "uncategorised" : entry[:category_names].join(", ")
|
|
1700
1586
|
"#{entry[:domain]} — #{entry[:verdict]} — #{categories} " \
|
|
1701
1587
|
"(#{entry[:sources].join(', ')}, #{entry[:visits]} visit(s))"
|
|
1702
1588
|
end
|
|
1703
|
-
private_class_method :browser_import_line
|
|
1704
1589
|
|
|
1705
1590
|
# --- browser profile (docs/plans/buy-skill-and-local-browser.md Phase 6) ---
|
|
1706
1591
|
|
|
@@ -1716,13 +1601,11 @@ module Portage
|
|
|
1716
1601
|
|
|
1717
1602
|
BROWSER_PROFILE_SUBCOMMANDS[sub].call(argv)
|
|
1718
1603
|
end
|
|
1719
|
-
private_class_method :run_browser_profile
|
|
1720
1604
|
|
|
1721
1605
|
def self.browser_profile_usage
|
|
1722
1606
|
warn USAGE
|
|
1723
1607
|
1
|
|
1724
1608
|
end
|
|
1725
|
-
private_class_method :browser_profile_usage
|
|
1726
1609
|
|
|
1727
1610
|
def self.parse_browser_profile_options(argv)
|
|
1728
1611
|
opts = { browser: nil, port: BrowserProfile::Profile::DEFAULT_PORT, url: nil, json: false }
|
|
@@ -1734,7 +1617,6 @@ module Portage
|
|
|
1734
1617
|
end.parse!(argv)
|
|
1735
1618
|
opts
|
|
1736
1619
|
end
|
|
1737
|
-
private_class_method :parse_browser_profile_options
|
|
1738
1620
|
|
|
1739
1621
|
# `--browser` names the exact browser; without it, the first Chromium
|
|
1740
1622
|
# family browser BrowserImport::Profiles finds installed, falling back
|
|
@@ -1744,7 +1626,6 @@ module Portage
|
|
|
1744
1626
|
browser = opts[:browser] || BrowserProfile::Browsers.detect || "chrome"
|
|
1745
1627
|
BrowserProfile::Profile.new(browser: browser, port: opts[:port])
|
|
1746
1628
|
end
|
|
1747
|
-
private_class_method :browser_profile_for
|
|
1748
1629
|
|
|
1749
1630
|
def self.run_browser_profile_init(argv)
|
|
1750
1631
|
opts = parse_browser_profile_options(argv)
|
|
@@ -1755,7 +1636,6 @@ module Portage
|
|
|
1755
1636
|
warn "#{e.message}\n#{USAGE}"
|
|
1756
1637
|
1
|
|
1757
1638
|
end
|
|
1758
|
-
private_class_method :run_browser_profile_init
|
|
1759
1639
|
|
|
1760
1640
|
# Launches the profile (if it isn't already running on its own port)
|
|
1761
1641
|
# and either opens a new tab at --url or attaches to the first
|
|
@@ -1773,7 +1653,6 @@ module Portage
|
|
|
1773
1653
|
warn "#{e.message}\n#{USAGE}"
|
|
1774
1654
|
1
|
|
1775
1655
|
end
|
|
1776
|
-
private_class_method :run_browser_profile_open
|
|
1777
1656
|
|
|
1778
1657
|
def self.run_browser_profile_status(argv)
|
|
1779
1658
|
opts = parse_browser_profile_options(argv)
|
|
@@ -1784,20 +1663,17 @@ module Portage
|
|
|
1784
1663
|
warn "#{e.message}\n#{USAGE}"
|
|
1785
1664
|
1
|
|
1786
1665
|
end
|
|
1787
|
-
private_class_method :run_browser_profile_status
|
|
1788
1666
|
|
|
1789
1667
|
def self.format_browser_profile_open(result)
|
|
1790
1668
|
tab = result.dig(:target, "url")
|
|
1791
1669
|
"#{result[:browser]} profile is open (port #{result[:port]}, #{result[:dir]})#{" — tab: #{tab}" if tab}."
|
|
1792
1670
|
end
|
|
1793
|
-
private_class_method :format_browser_profile_open
|
|
1794
1671
|
|
|
1795
1672
|
def self.format_browser_profile_status(result)
|
|
1796
1673
|
return "#{result[:browser]} profile (#{result[:dir]}) isn't running." unless result[:running]
|
|
1797
1674
|
|
|
1798
1675
|
"#{result[:browser]} profile is running on port #{result[:port]} (#{result[:dir]})."
|
|
1799
1676
|
end
|
|
1800
|
-
private_class_method :format_browser_profile_status
|
|
1801
1677
|
|
|
1802
1678
|
def self.report_browser_profile_error(error, json)
|
|
1803
1679
|
if json
|
|
@@ -1807,7 +1683,6 @@ module Portage
|
|
|
1807
1683
|
end
|
|
1808
1684
|
1
|
|
1809
1685
|
end
|
|
1810
|
-
private_class_method :report_browser_profile_error
|
|
1811
1686
|
|
|
1812
1687
|
# --- doctor ---
|
|
1813
1688
|
|
|
@@ -1821,7 +1696,6 @@ module Portage
|
|
|
1821
1696
|
end.parse!(argv)
|
|
1822
1697
|
opts
|
|
1823
1698
|
end
|
|
1824
|
-
private_class_method :parse_doctor_options
|
|
1825
1699
|
|
|
1826
1700
|
# `wizard: :force` is `portage setup`, always offering the wizard on a
|
|
1827
1701
|
# TTY; `:auto` is `doctor`/`configure`, which only offers it when
|
|
@@ -1841,10 +1715,8 @@ module Portage
|
|
|
1841
1715
|
|
|
1842
1716
|
report_doctor(doctor.call, json: opts[:json])
|
|
1843
1717
|
end
|
|
1844
|
-
private_class_method :run_doctor
|
|
1845
1718
|
|
|
1846
1719
|
def self.run_setup(argv) = run_doctor(argv, wizard: :force)
|
|
1847
|
-
private_class_method :run_setup
|
|
1848
1720
|
|
|
1849
1721
|
# --json or no TTY on stdin always stays today's read-only report,
|
|
1850
1722
|
# whichever command name was used — a piped/CI/agent run never blocks
|
|
@@ -1855,18 +1727,15 @@ module Portage
|
|
|
1855
1727
|
|
|
1856
1728
|
doctor.nothing_configured?
|
|
1857
1729
|
end
|
|
1858
|
-
private_class_method :run_wizard?
|
|
1859
1730
|
|
|
1860
1731
|
def self.run_setup_wizard
|
|
1861
1732
|
SetupWizard.new.call
|
|
1862
1733
|
end
|
|
1863
|
-
private_class_method :run_setup_wizard
|
|
1864
1734
|
|
|
1865
1735
|
def self.report_doctor(findings, json:)
|
|
1866
1736
|
puts json ? JSON.pretty_generate(findings.map(&:to_h)) : format_doctor(findings)
|
|
1867
1737
|
findings.none?(&:warning?) ? 0 : 1
|
|
1868
1738
|
end
|
|
1869
|
-
private_class_method :report_doctor
|
|
1870
1739
|
|
|
1871
1740
|
# Info findings (install method, Ruby, adapters, PATH) first, then the
|
|
1872
1741
|
# warnings, which alone decide the exit code.
|
|
@@ -1877,7 +1746,6 @@ module Portage
|
|
|
1877
1746
|
lines.concat(warnings.empty? ? ["No issues found."] : warnings.map { |f| "[#{f.check}] #{f.message}" })
|
|
1878
1747
|
lines.join("\n")
|
|
1879
1748
|
end
|
|
1880
|
-
private_class_method :format_doctor
|
|
1881
1749
|
|
|
1882
1750
|
# --- generate ---
|
|
1883
1751
|
|
|
@@ -1891,7 +1759,6 @@ module Portage
|
|
|
1891
1759
|
1
|
|
1892
1760
|
end
|
|
1893
1761
|
end
|
|
1894
|
-
private_class_method :run_generate
|
|
1895
1762
|
|
|
1896
1763
|
def self.run_generate_adapter(rest)
|
|
1897
1764
|
name, *rest = rest
|
|
@@ -1906,7 +1773,6 @@ module Portage
|
|
|
1906
1773
|
puts "Scaffolded #{path}/"
|
|
1907
1774
|
0
|
|
1908
1775
|
end
|
|
1909
|
-
private_class_method :run_generate_adapter
|
|
1910
1776
|
|
|
1911
1777
|
def self.run_generate_agent_profile(rest)
|
|
1912
1778
|
out = "agent-profile.json"
|
|
@@ -1926,7 +1792,6 @@ module Portage
|
|
|
1926
1792
|
"`bundle exec rake agent_profile:purge` from the repo root — see docs/agent-profile.md."
|
|
1927
1793
|
0
|
|
1928
1794
|
end
|
|
1929
|
-
private_class_method :run_generate_agent_profile
|
|
1930
1795
|
|
|
1931
1796
|
# --- output ---
|
|
1932
1797
|
|
|
@@ -1942,48 +1807,41 @@ module Portage
|
|
|
1942
1807
|
lines.concat(format_decisions(report[:decisions])) if report[:decisions]&.any?
|
|
1943
1808
|
lines.join("\n")
|
|
1944
1809
|
end
|
|
1945
|
-
private_class_method :format_report
|
|
1946
1810
|
|
|
1947
1811
|
def self.format_quote(report)
|
|
1948
1812
|
lines = report[:quote_id] ? [" quote: #{report[:quote_id]}"] : []
|
|
1949
1813
|
lines << " approve: #{approval_line(report[:summary])}" if report[:summary]
|
|
1950
1814
|
lines
|
|
1951
1815
|
end
|
|
1952
|
-
private_class_method :format_quote
|
|
1953
1816
|
|
|
1954
1817
|
def self.approval_line(summary)
|
|
1955
1818
|
[Approve.describe(summary), summary[:url]].compact.join(" — ")
|
|
1956
1819
|
end
|
|
1957
|
-
private_class_method :approval_line
|
|
1958
1820
|
|
|
1959
1821
|
# What the checkout holds, as opposed to the search results above it,
|
|
1960
1822
|
# and where it differs from the request.
|
|
1961
1823
|
def self.format_checkout(report)
|
|
1962
1824
|
lines = Array(report[:items]).map { |item| " in checkout: #{item_label(item)}" }
|
|
1963
1825
|
total = report_total(report)
|
|
1964
|
-
lines << " total: #{format_amount(total, report[:currency])}" if total
|
|
1826
|
+
lines << " total: #{Money.format_amount(total, report[:currency])}" if total
|
|
1965
1827
|
lines + Array(report[:warnings]).map { |w| " warning: #{w}" }
|
|
1966
1828
|
end
|
|
1967
|
-
private_class_method :format_checkout
|
|
1968
1829
|
|
|
1969
1830
|
def self.format_decisions(decisions)
|
|
1970
1831
|
decisions.map do |name, verdict|
|
|
1971
1832
|
" decision #{name}: #{verdict.compact.map { |key, value| "#{key}=#{value}" }.join(' ')}"
|
|
1972
1833
|
end
|
|
1973
1834
|
end
|
|
1974
|
-
private_class_method :format_decisions
|
|
1975
1835
|
|
|
1976
1836
|
def self.format_handoff(handoff)
|
|
1977
1837
|
lines = [" opened in browser: #{handoff[:opened]}", " notified: #{handoff[:notified]}"]
|
|
1978
1838
|
lines << " notify error: #{handoff[:notify_error]}" if handoff[:notify_error]
|
|
1979
1839
|
lines
|
|
1980
1840
|
end
|
|
1981
|
-
private_class_method :format_handoff
|
|
1982
1841
|
|
|
1983
1842
|
def self.product_line(product)
|
|
1984
1843
|
product.respond_to?(:title) ? "#{product.id}: #{product.title}" : "#{product['id']}: #{product['title']}"
|
|
1985
1844
|
end
|
|
1986
|
-
private_class_method :product_line
|
|
1987
1845
|
|
|
1988
1846
|
def self.format_find(report)
|
|
1989
1847
|
lines = [report[:message].to_s]
|
|
@@ -1991,7 +1849,6 @@ module Portage
|
|
|
1991
1849
|
lines << " search: #{report[:search_id]} (portage pick --search #{report[:search_id]})" if report[:search_id]
|
|
1992
1850
|
lines.join("\n")
|
|
1993
1851
|
end
|
|
1994
|
-
private_class_method :format_find
|
|
1995
1852
|
|
|
1996
1853
|
def self.offer_line(offer)
|
|
1997
1854
|
parts = ["#{offer[:store]} — #{offer[:title]} (#{offer[:product_id]})", format_price(offer)]
|
|
@@ -1999,7 +1856,6 @@ module Portage
|
|
|
1999
1856
|
parts << "ref #{offer[:offer_ref]}" if offer[:offer_ref]
|
|
2000
1857
|
parts.join(" — ")
|
|
2001
1858
|
end
|
|
2002
|
-
private_class_method :offer_line
|
|
2003
1859
|
|
|
2004
1860
|
def self.format_compare(report)
|
|
2005
1861
|
lines = [report[:message].to_s]
|
|
@@ -2007,7 +1863,6 @@ module Portage
|
|
|
2007
1863
|
lines << " search: #{report[:search_id]} (portage pick --search #{report[:search_id]})" if report[:search_id]
|
|
2008
1864
|
lines.join("\n")
|
|
2009
1865
|
end
|
|
2010
|
-
private_class_method :format_compare
|
|
2011
1866
|
|
|
2012
1867
|
def self.compare_offer_line(offer)
|
|
2013
1868
|
parts = ["[#{offer[:match]}] #{offer[:store]} — #{offer[:title]} (#{offer[:product_id]})", format_price(offer)]
|
|
@@ -2015,16 +1870,13 @@ module Portage
|
|
|
2015
1870
|
parts << "ref #{offer[:offer_ref]}" if offer[:offer_ref]
|
|
2016
1871
|
parts.join(" — ")
|
|
2017
1872
|
end
|
|
2018
|
-
private_class_method :compare_offer_line
|
|
2019
1873
|
|
|
2020
1874
|
def self.format_price(offer)
|
|
2021
1875
|
return "price n/a" unless offer[:amount]
|
|
2022
1876
|
|
|
2023
|
-
format_amount(offer[:amount], offer[:currency])
|
|
1877
|
+
Money.format_amount(offer[:amount], offer[:currency])
|
|
2024
1878
|
end
|
|
2025
|
-
private_class_method :format_price
|
|
2026
1879
|
|
|
2027
|
-
|
|
2028
|
-
private_class_method :format_amount
|
|
1880
|
+
private_class_method(*(singleton_methods(false) - [:run]))
|
|
2029
1881
|
end
|
|
2030
1882
|
end
|