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