portage-cli 0.7.0 → 0.7.4
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 +142 -0
- data/README.md +266 -17
- data/lib/portage/cli/buy.rb +187 -41
- data/lib/portage/cli/doctor.rb +130 -2
- data/lib/portage/cli/find.rb +2 -1
- data/lib/portage/cli/handoff_reconciler/wire_adapters.rb +50 -0
- data/lib/portage/cli/handoff_reconciler.rb +249 -0
- data/lib/portage/cli/handoff_spend_mode.rb +28 -0
- data/lib/portage/cli/handoff_wait_timeout.rb +49 -0
- data/lib/portage/cli/handoff_waiter.rb +103 -0
- data/lib/portage/cli/homepage_fetch.rb +41 -0
- data/lib/portage/cli/install_doctor.rb +239 -0
- data/lib/portage/cli/macos_notifier.rb +37 -0
- data/lib/portage/cli/notifier.rb +6 -3
- data/lib/portage/cli/payment_methods.rb +6 -4
- data/lib/portage/cli/permissive_authenticator.rb +16 -0
- data/lib/portage/cli/proxy_doctor.rb +149 -0
- data/lib/portage/cli/proxy_settings/password_ref.rb +48 -0
- data/lib/portage/cli/proxy_settings.rb +350 -0
- data/lib/portage/cli/reconcile_notifier.rb +65 -0
- data/lib/portage/cli/reconcile_notify.rb +38 -0
- data/lib/portage/cli/search_backends.rb +6 -4
- data/lib/portage/cli/shipping_profile.rb +7 -2
- data/lib/portage/cli/user_agent.rb +39 -0
- data/lib/portage/cli/version.rb +1 -1
- data/lib/portage/cli/webmcp.rb +25 -0
- data/lib/portage/cli/webmcp_checkout_mode.rb +28 -0
- data/lib/portage/cli.rb +249 -15
- metadata +37 -6
data/lib/portage/cli.rb
CHANGED
|
@@ -2,6 +2,7 @@ require "optparse"
|
|
|
2
2
|
require "json"
|
|
3
3
|
|
|
4
4
|
require_relative "cli/version"
|
|
5
|
+
require_relative "cli/user_agent"
|
|
5
6
|
require_relative "cli/shipping_profile"
|
|
6
7
|
require_relative "cli/buyer_context"
|
|
7
8
|
require_relative "cli/catalog_products"
|
|
@@ -10,7 +11,12 @@ require_relative "cli/find"
|
|
|
10
11
|
require_relative "cli/compare"
|
|
11
12
|
require_relative "cli/history"
|
|
12
13
|
require_relative "cli/payment_methods"
|
|
14
|
+
require_relative "cli/proxy_settings"
|
|
13
15
|
require_relative "cli/doctor"
|
|
16
|
+
require_relative "cli/handoff_reconciler"
|
|
17
|
+
require_relative "cli/handoff_waiter"
|
|
18
|
+
require_relative "cli/reconcile_notifier"
|
|
19
|
+
require_relative "cli/reconcile_notify"
|
|
14
20
|
require_relative "cli/generate/adapter"
|
|
15
21
|
require_relative "cli/generate/agent_profile"
|
|
16
22
|
|
|
@@ -25,6 +31,7 @@ module Portage
|
|
|
25
31
|
[--product-id ID] [--yes] [--dry-run]
|
|
26
32
|
[--auto-open|--no-auto-open] [--notify-webhook URL]
|
|
27
33
|
[--decision-backend jev|laya] [--min-confidence N] [--json]
|
|
34
|
+
[--wait [--wait-timeout DURATION|off]]
|
|
28
35
|
portage buy --query "..." [--store URL] [--max-price N] [--limit N] ...
|
|
29
36
|
portage find --query "..." [--max-price N] [--limit N] [--json]
|
|
30
37
|
portage compare <url> --product-id ID [--id VALUE ...] [--results N]
|
|
@@ -43,28 +50,66 @@ module Portage
|
|
|
43
50
|
[--rolling-cap N --rolling-window-seconds N --currency CUR]
|
|
44
51
|
[--velocity-count N --velocity-window-seconds N]
|
|
45
52
|
[--allow HOST ...] [--clear-allowlist]
|
|
53
|
+
portage orders reconcile [--checkout ID] [--json]
|
|
46
54
|
portage doctor [--require FILE] [--adapter CLASS_NAME] [--json]
|
|
47
55
|
portage configure [--require FILE] [--adapter CLASS_NAME] [--json] (alias for doctor)
|
|
48
56
|
portage setup [--require FILE] [--adapter CLASS_NAME] [--json] (alias for doctor)
|
|
49
57
|
portage generate adapter NAME [--dir DIR]
|
|
50
58
|
portage generate agent-profile [--out FILE] [--key-out FILE] [--rotate]
|
|
59
|
+
portage --version
|
|
60
|
+
|
|
61
|
+
proxy flags (buy/find/compare/doctor/payment enroll):
|
|
62
|
+
[--proxy URL] [--proxy-mode forward|gateway] [--proxy-header "Name: value"]
|
|
63
|
+
[--no-proxy HOSTS] [--proxy-route ROUTE=URL|direct] [--proxy-chain URL,URL,...]
|
|
64
|
+
[--proxy-passthrough HEADER] [--proxy-ca FILE] [--no-env-proxy]
|
|
51
65
|
USAGE
|
|
52
66
|
|
|
53
67
|
COMMANDS = { "buy" => :run_buy, "find" => :run_find, "compare" => :run_compare,
|
|
54
68
|
"history" => :run_history, "payment" => :run_payment, "policy" => :run_policy,
|
|
55
|
-
"
|
|
56
|
-
"generate" => :run_generate }.freeze
|
|
69
|
+
"orders" => :run_orders, "doctor" => :run_doctor, "configure" => :run_doctor,
|
|
70
|
+
"setup" => :run_doctor, "generate" => :run_generate }.freeze
|
|
71
|
+
|
|
72
|
+
VERSION_FLAGS = %w[--version -v version].freeze
|
|
57
73
|
|
|
58
74
|
# @param argv [Array<String>]
|
|
59
75
|
# @return [Integer] process exit code
|
|
60
76
|
def self.run(argv)
|
|
61
77
|
command, *rest = argv
|
|
78
|
+
return run_version if VERSION_FLAGS.include?(command)
|
|
62
79
|
return send(COMMANDS[command], rest) if COMMANDS.key?(command)
|
|
63
80
|
|
|
64
81
|
warn USAGE
|
|
65
82
|
1
|
|
66
83
|
end
|
|
67
84
|
|
|
85
|
+
# So a packaged install (Homebrew's `test do` block, `portage doctor`)
|
|
86
|
+
# can confirm which release it's running without hitting the network.
|
|
87
|
+
def self.run_version
|
|
88
|
+
puts VERSION
|
|
89
|
+
0
|
|
90
|
+
end
|
|
91
|
+
private_class_method :run_version
|
|
92
|
+
|
|
93
|
+
# --- proxy (docs/plans/proxy-support.md Phase 2) ---
|
|
94
|
+
|
|
95
|
+
# Resolves this command's `--proxy*` flags/env/config.json into a
|
|
96
|
+
# ProxyConfig and installs it as the process-wide default every
|
|
97
|
+
# Support::Connection.start call (buy/find/compare/payment/doctor alike)
|
|
98
|
+
# already reads unless it's handed its own `proxy:` — see ProxySettings'
|
|
99
|
+
# own comment for why. A bad flag or a malformed/protected-header
|
|
100
|
+
# config.json surfaces here as a clean message, never a raw core
|
|
101
|
+
# exception.
|
|
102
|
+
# @return [ProxySettings, nil] nil on a config error (already reported).
|
|
103
|
+
def self.apply_proxy_settings(proxy_flags)
|
|
104
|
+
settings = ProxySettings.new(flags: proxy_flags || {})
|
|
105
|
+
Portage::Ucp::Support::ProxyConfig.current = settings.resolve
|
|
106
|
+
settings
|
|
107
|
+
rescue ProxySettings::ConfigError => e
|
|
108
|
+
warn "portage: #{e.message}"
|
|
109
|
+
nil
|
|
110
|
+
end
|
|
111
|
+
private_class_method :apply_proxy_settings
|
|
112
|
+
|
|
68
113
|
# --- find ---
|
|
69
114
|
|
|
70
115
|
def self.run_find(argv)
|
|
@@ -72,6 +117,8 @@ module Portage
|
|
|
72
117
|
return 1 unless options
|
|
73
118
|
|
|
74
119
|
json = options.delete(:json)
|
|
120
|
+
return 1 unless apply_proxy_settings(options.delete(:proxy))
|
|
121
|
+
|
|
75
122
|
report = Find.new(**options).call
|
|
76
123
|
record_find(report)
|
|
77
124
|
puts json ? JSON.pretty_generate(report) : format_find(report)
|
|
@@ -98,11 +145,13 @@ module Portage
|
|
|
98
145
|
private_class_method :parse_find_options
|
|
99
146
|
|
|
100
147
|
def self.find_option_parser(opts)
|
|
148
|
+
opts[:proxy] = {}
|
|
101
149
|
OptionParser.new do |parser|
|
|
102
150
|
parser.on("--query QUERY") { |v| opts[:query] = v }
|
|
103
151
|
parser.on("--limit N", Integer) { |v| opts[:limit] = v }
|
|
104
152
|
parser.on("--max-price N", Float) { |v| opts[:max_price] = to_minor_units(v) }
|
|
105
153
|
parser.on("--json") { opts[:json] = true }
|
|
154
|
+
ProxySettings.add_options(parser, opts[:proxy])
|
|
106
155
|
end
|
|
107
156
|
end
|
|
108
157
|
private_class_method :find_option_parser
|
|
@@ -121,6 +170,8 @@ module Portage
|
|
|
121
170
|
|
|
122
171
|
json = options.delete(:json)
|
|
123
172
|
url = options.delete(:url)
|
|
173
|
+
return 1 unless apply_proxy_settings(options.delete(:proxy))
|
|
174
|
+
|
|
124
175
|
report = Compare.new(origin_url: url, **options).call
|
|
125
176
|
# Recorded as a search, not a purchase — compare never checks out. The
|
|
126
177
|
# query string names the compare so `portage history list` doesn't
|
|
@@ -147,12 +198,14 @@ module Portage
|
|
|
147
198
|
private_class_method :parse_compare_options
|
|
148
199
|
|
|
149
200
|
def self.compare_option_parser(opts)
|
|
201
|
+
opts[:proxy] = {}
|
|
150
202
|
OptionParser.new do |parser|
|
|
151
203
|
parser.on("--product-id ID") { |v| opts[:origin_product_id] = v }
|
|
152
204
|
parser.on("--id VALUE") { |v| opts[:identity] << v }
|
|
153
205
|
parser.on("--results N", Integer) { |v| opts[:results] = v }
|
|
154
206
|
parser.on("--max-price N", Float) { |v| opts[:max_price] = to_minor_units(v) }
|
|
155
207
|
parser.on("--json") { opts[:json] = true }
|
|
208
|
+
ProxySettings.add_options(parser, opts[:proxy])
|
|
156
209
|
end
|
|
157
210
|
end
|
|
158
211
|
private_class_method :compare_option_parser
|
|
@@ -162,6 +215,7 @@ module Portage
|
|
|
162
215
|
def self.run_buy(argv)
|
|
163
216
|
parsed = parse_buy_options(argv)
|
|
164
217
|
return 1 unless parsed
|
|
218
|
+
return 1 unless apply_proxy_settings(parsed[:proxy])
|
|
165
219
|
|
|
166
220
|
url = parsed[:buy][:url] || parsed[:store]
|
|
167
221
|
parsed[:confidence_check] = confidence_check(parsed, url)
|
|
@@ -210,11 +264,88 @@ module Portage
|
|
|
210
264
|
options[:product_id] ||= product_id
|
|
211
265
|
report = Buy.new(**options).call
|
|
212
266
|
record_buy(report, options[:query])
|
|
213
|
-
|
|
267
|
+
result = parsed[:wait] ? wait_for_handoff(report, parsed) : nil
|
|
268
|
+
print_buy_report(report, result, parsed[:json])
|
|
214
269
|
report[:checkout] || report[:browse] ? 0 : 1
|
|
215
270
|
end
|
|
216
271
|
private_class_method :execute_buy
|
|
217
272
|
|
|
273
|
+
# docs/plans/handoff-reconcile.md Phase 3 — `portage buy --wait`. A
|
|
274
|
+
# no-op (returns nil) whenever there's nothing to wait on: --dry-run
|
|
275
|
+
# never hands off at all, and a completed/browse-only/dead-end report
|
|
276
|
+
# has no pending shopper record either. Otherwise polls
|
|
277
|
+
# `HandoffReconciler` through `HandoffWaiter` until it settles or its
|
|
278
|
+
# deadline passes.
|
|
279
|
+
#
|
|
280
|
+
# Under --json, every event streams to stdout as it happens (NDJSON) —
|
|
281
|
+
# the initial hand-off, then a line per checkout-status change, then the
|
|
282
|
+
# settle, so a calling agent reading stdout live never has to guess
|
|
283
|
+
# whether it's still waiting. Under plain output, nothing streams here:
|
|
284
|
+
# the forced "terminal" notify channel (see #wait_notifier) prints its
|
|
285
|
+
# own line when it settles, and the final report follows exactly as it
|
|
286
|
+
# does without --wait.
|
|
287
|
+
def self.wait_for_handoff(report, parsed)
|
|
288
|
+
return nil unless report[:handoff] && report[:checkout_id]
|
|
289
|
+
|
|
290
|
+
transaction_log = Portage::Ucp::Support::TransactionLog.new
|
|
291
|
+
record = pending_shopper_record(report[:checkout_id], transaction_log)
|
|
292
|
+
return nil unless record
|
|
293
|
+
|
|
294
|
+
json = parsed[:json]
|
|
295
|
+
emit_ndjson(handoff_event(report)) if json
|
|
296
|
+
reconciler = HandoffReconciler.new(transaction_log: transaction_log, notifier: wait_notifier(json))
|
|
297
|
+
waiter = HandoffWaiter.new(reconciler: reconciler, transaction_log: transaction_log,
|
|
298
|
+
wait_timeout_override: parsed[:wait_timeout])
|
|
299
|
+
waiter.call(record) { |event, result| emit_wait_event(event, result, json) }
|
|
300
|
+
end
|
|
301
|
+
private_class_method :wait_for_handoff
|
|
302
|
+
|
|
303
|
+
def self.pending_shopper_record(checkout_id, transaction_log)
|
|
304
|
+
transaction_log.each_record.find { |r| r["checkout_id"] == checkout_id && r["settled_by"] == "shopper" }
|
|
305
|
+
end
|
|
306
|
+
private_class_method :pending_shopper_record
|
|
307
|
+
|
|
308
|
+
# `terminal` is forced on for a plain-text wait (see ReconcileNotify) so
|
|
309
|
+
# the shopper sees a line the moment it settles even with nothing
|
|
310
|
+
# configured. Never forced under --json: that channel prints plain text,
|
|
311
|
+
# which would corrupt the NDJSON stream this method's caller is also
|
|
312
|
+
# writing to the same stdout.
|
|
313
|
+
def self.wait_notifier(json)
|
|
314
|
+
ReconcileNotifier.new(channels: ReconcileNotify.resolve(extra: json ? [] : ["terminal"]))
|
|
315
|
+
end
|
|
316
|
+
private_class_method :wait_notifier
|
|
317
|
+
|
|
318
|
+
def self.handoff_event(report)
|
|
319
|
+
{ event: "handoff", checkout_id: report[:checkout_id], checkout_url: report[:checkout_url],
|
|
320
|
+
reason: report[:outcome] }
|
|
321
|
+
end
|
|
322
|
+
private_class_method :handoff_event
|
|
323
|
+
|
|
324
|
+
# Only `:settled` ever reaches stdout as `handoff_settled` — a timeout or
|
|
325
|
+
# an interrupted wait leaves `result.settled` false, and that's already
|
|
326
|
+
# visible in the final report object this event stream ends with, so a
|
|
327
|
+
# second, redundant "gave up" event isn't needed.
|
|
328
|
+
def self.emit_wait_event(event, result, json)
|
|
329
|
+
return unless json
|
|
330
|
+
|
|
331
|
+
case event
|
|
332
|
+
when :status then emit_ndjson({ event: "handoff_status", status: result.checkout_status })
|
|
333
|
+
when :settled
|
|
334
|
+
emit_ndjson({ event: "handoff_settled", result: result.status, resolution: result.resolution,
|
|
335
|
+
order_id: result.order_id, amount: result.amount, currency: result.currency }.compact)
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
private_class_method :emit_wait_event
|
|
339
|
+
|
|
340
|
+
def self.emit_ndjson(payload) = puts JSON.generate(payload)
|
|
341
|
+
private_class_method :emit_ndjson
|
|
342
|
+
|
|
343
|
+
def self.print_buy_report(report, result, json)
|
|
344
|
+
report = report.merge(reconcile: result.to_h) if result
|
|
345
|
+
puts json ? JSON.pretty_generate(report) : format_report(report)
|
|
346
|
+
end
|
|
347
|
+
private_class_method :print_buy_report
|
|
348
|
+
|
|
218
349
|
# Built before the buy starts (and before the search, when there's no
|
|
219
350
|
# URL), so a bad --min-confidence, or a bad PORTAGE_MIN_CONFIDENCE with
|
|
220
351
|
# a backend enabled, stops the run up front rather than after a
|
|
@@ -276,7 +407,10 @@ module Portage
|
|
|
276
407
|
buy = { url: url, qty: 1, yes: false, dry_run: false }
|
|
277
408
|
parsed = { buy: buy, find: {}, confidence: {} }
|
|
278
409
|
json = argv.include?("--json")
|
|
279
|
-
|
|
410
|
+
parsed[:proxy] = {}
|
|
411
|
+
parser = buy_option_parser(buy, parsed)
|
|
412
|
+
ProxySettings.add_options(parser, parsed[:proxy])
|
|
413
|
+
parser.parse!(argv)
|
|
280
414
|
buy[:query] ||= ""
|
|
281
415
|
return parsed if url || !buy[:query].strip.empty?
|
|
282
416
|
|
|
@@ -294,9 +428,9 @@ module Portage
|
|
|
294
428
|
parser.on("--product-id ID") { |v| buy[:product_id] = v }
|
|
295
429
|
parser.on("--yes") { buy[:yes] = true }
|
|
296
430
|
parser.on("--dry-run") { buy[:dry_run] = true }
|
|
297
|
-
parser.on("--[no-]auto-open") { |v| buy[:auto_open] = v }
|
|
298
|
-
parser.on("--notify-webhook URL") { |v| buy[:notify_webhook] = v }
|
|
299
431
|
parser.on("--json") { parsed[:json] = true }
|
|
432
|
+
add_handoff_options(parser, buy)
|
|
433
|
+
add_wait_options(parser, parsed)
|
|
300
434
|
add_search_options(parser, buy, parsed)
|
|
301
435
|
add_confidence_options(parser, parsed[:confidence])
|
|
302
436
|
end
|
|
@@ -311,6 +445,22 @@ module Portage
|
|
|
311
445
|
end
|
|
312
446
|
private_class_method :add_confidence_options
|
|
313
447
|
|
|
448
|
+
def self.add_handoff_options(parser, buy)
|
|
449
|
+
parser.on("--[no-]auto-open") { |v| buy[:auto_open] = v }
|
|
450
|
+
parser.on("--notify-webhook URL") { |v| buy[:notify_webhook] = v }
|
|
451
|
+
end
|
|
452
|
+
private_class_method :add_handoff_options
|
|
453
|
+
|
|
454
|
+
# docs/plans/handoff-reconcile.md Phase 3 — `--wait`/`--wait-timeout`
|
|
455
|
+
# land on `parsed`, never on `buy`: they're consumed by
|
|
456
|
+
# `#wait_for_handoff` after `Buy#call` returns, and `Buy.new` has no
|
|
457
|
+
# `wait:`/`wait_timeout:` keyword to accidentally receive them.
|
|
458
|
+
def self.add_wait_options(parser, parsed)
|
|
459
|
+
parser.on("--wait") { parsed[:wait] = true }
|
|
460
|
+
parser.on("--wait-timeout DURATION") { |v| parsed[:wait_timeout] = v }
|
|
461
|
+
end
|
|
462
|
+
private_class_method :add_wait_options
|
|
463
|
+
|
|
314
464
|
# `--query` feeds both halves: it's the store search when there's no URL
|
|
315
465
|
# and the catalog search once a store is settled, so it's registered once
|
|
316
466
|
# here rather than twice on the same parser.
|
|
@@ -318,7 +468,7 @@ module Portage
|
|
|
318
468
|
parser.on("--query QUERY") { |v| parsed[:find][:query] = buy[:query] = v }
|
|
319
469
|
parser.on("--store URL") { |v| parsed[:store] = v }
|
|
320
470
|
parser.on("--limit N", Integer) { |v| parsed[:find][:limit] = v }
|
|
321
|
-
parser.on("--max-price N", Float) { |v| parsed[:find][:max_price] = to_minor_units(v) }
|
|
471
|
+
parser.on("--max-price N", Float) { |v| parsed[:find][:max_price] = buy[:max_price] = to_minor_units(v) }
|
|
322
472
|
end
|
|
323
473
|
private_class_method :add_search_options
|
|
324
474
|
|
|
@@ -455,13 +605,14 @@ module Portage
|
|
|
455
605
|
private_class_method :run_payment_mutate
|
|
456
606
|
|
|
457
607
|
def self.parse_payment_enroll_options(argv)
|
|
458
|
-
opts = { scope_merchants: [] }
|
|
608
|
+
opts = { scope_merchants: [], proxy: {} }
|
|
459
609
|
OptionParser.new do |parser|
|
|
460
610
|
parser.on("--label NAME") { |v| opts[:label] = v }
|
|
461
611
|
parser.on("--json") { opts[:json] = true }
|
|
462
612
|
parser.on("--scope-merchant HOST") { |v| opts[:scope_merchants] << v }
|
|
463
613
|
parser.on("--scope-max-amount N", Integer) { |v| opts[:scope_max_amount] = v }
|
|
464
614
|
parser.on("--scope-currency CUR") { |v| opts[:scope_currency] = v }
|
|
615
|
+
ProxySettings.add_options(parser, opts[:proxy])
|
|
465
616
|
end.parse!(argv)
|
|
466
617
|
opts[:url] = argv.first && !argv.first.start_with?("-") ? argv.shift : nil
|
|
467
618
|
opts
|
|
@@ -486,6 +637,7 @@ module Portage
|
|
|
486
637
|
warn USAGE
|
|
487
638
|
return 1
|
|
488
639
|
end
|
|
640
|
+
return 1 unless apply_proxy_settings(opts[:proxy])
|
|
489
641
|
|
|
490
642
|
result = PaymentMethods.new.enroll(opts[:url], label: opts[:label],
|
|
491
643
|
scope: payment_enroll_scope(opts)) do |setup_url|
|
|
@@ -610,14 +762,86 @@ module Portage
|
|
|
610
762
|
end
|
|
611
763
|
private_class_method :format_payment_enroll
|
|
612
764
|
|
|
765
|
+
# --- orders ---
|
|
766
|
+
|
|
767
|
+
# docs/plans/handoff-reconcile.md Phase 1 — resolves every pending
|
|
768
|
+
# `settled_by: "shopper"` TransactionLog record (or just the one named
|
|
769
|
+
# by `--checkout`) by re-fetching that checkout from the store. Safe to
|
|
770
|
+
# run from cron/launchd: a record that's already terminal, or that
|
|
771
|
+
# doesn't belong to this run (a `--checkout` for a different id, or
|
|
772
|
+
# `settled_by: nil` dispatcher crash-evidence), is a no-op result, not
|
|
773
|
+
# an error, so a scheduled run never needs its own filtering logic.
|
|
774
|
+
def self.run_orders(argv)
|
|
775
|
+
sub = argv.first && !argv.first.start_with?("-") ? argv.shift : nil
|
|
776
|
+
return run_orders_reconcile(argv) if sub == "reconcile"
|
|
777
|
+
|
|
778
|
+
warn USAGE
|
|
779
|
+
1
|
|
780
|
+
end
|
|
781
|
+
private_class_method :run_orders
|
|
782
|
+
|
|
783
|
+
def self.run_orders_reconcile(argv)
|
|
784
|
+
opts = {}
|
|
785
|
+
OptionParser.new do |parser|
|
|
786
|
+
parser.on("--checkout ID") { |v| opts[:checkout] = v }
|
|
787
|
+
parser.on("--json") { opts[:json] = true }
|
|
788
|
+
end.parse!(argv)
|
|
789
|
+
|
|
790
|
+
transaction_log = Portage::Ucp::Support::TransactionLog.new
|
|
791
|
+
reconciler = HandoffReconciler.new(transaction_log: transaction_log)
|
|
792
|
+
results = reconcile_records(opts[:checkout], transaction_log, reconciler)
|
|
793
|
+
|
|
794
|
+
puts opts[:json] ? JSON.pretty_generate(results.map(&:to_h)) : format_reconcile(results)
|
|
795
|
+
0
|
|
796
|
+
end
|
|
797
|
+
private_class_method :run_orders_reconcile
|
|
798
|
+
|
|
799
|
+
def self.reconcile_records(checkout_id, transaction_log, reconciler)
|
|
800
|
+
return reconcile_one_checkout(checkout_id, transaction_log, reconciler) if checkout_id
|
|
801
|
+
|
|
802
|
+
HandoffReconciler.each_pending_shopper_record(transaction_log).map { |record| reconciler.call(record) }
|
|
803
|
+
end
|
|
804
|
+
private_class_method :reconcile_records
|
|
805
|
+
|
|
806
|
+
# `--checkout ID` reconciles the one named record whatever its
|
|
807
|
+
# `settled_by`/status — #call itself still refuses to settle anything
|
|
808
|
+
# that isn't a pending shopper record, this just skips the "iterate
|
|
809
|
+
# every pending record" step when the caller already knows which one.
|
|
810
|
+
def self.reconcile_one_checkout(checkout_id, transaction_log, reconciler)
|
|
811
|
+
key = transaction_log.each_record.find { |r| r["checkout_id"] == checkout_id }&.fetch("idempotency_key", nil)
|
|
812
|
+
return [] unless key
|
|
813
|
+
|
|
814
|
+
[reconciler.call(transaction_log.find(key))]
|
|
815
|
+
end
|
|
816
|
+
private_class_method :reconcile_one_checkout
|
|
817
|
+
|
|
818
|
+
def self.format_reconcile(results)
|
|
819
|
+
return "(nothing to reconcile)" if results.empty?
|
|
820
|
+
|
|
821
|
+
results.map { |r| format_reconcile_result(r) }.join("\n")
|
|
822
|
+
end
|
|
823
|
+
private_class_method :format_reconcile
|
|
824
|
+
|
|
825
|
+
def self.format_reconcile_result(result)
|
|
826
|
+
return "#{result.idempotency_key}: #{result.note}" unless result.settled
|
|
827
|
+
|
|
828
|
+
parts = ["#{result.idempotency_key}: #{result.status}"]
|
|
829
|
+
parts << "resolution: #{result.resolution}" if result.resolution
|
|
830
|
+
parts << "order: #{result.order_id}" if result.order_id
|
|
831
|
+
parts << format_amount(result.amount, result.currency) if result.amount
|
|
832
|
+
parts.join(" — ")
|
|
833
|
+
end
|
|
834
|
+
private_class_method :format_reconcile_result
|
|
835
|
+
|
|
613
836
|
# --- doctor ---
|
|
614
837
|
|
|
615
838
|
def self.parse_doctor_options(argv)
|
|
616
|
-
opts = {}
|
|
839
|
+
opts = { proxy: {} }
|
|
617
840
|
OptionParser.new do |parser|
|
|
618
841
|
parser.on("--require FILE") { |v| opts[:require] = v }
|
|
619
842
|
parser.on("--adapter CLASS_NAME") { |v| opts[:adapter] = v }
|
|
620
843
|
parser.on("--json") { opts[:json] = true }
|
|
844
|
+
ProxySettings.add_options(parser, opts[:proxy])
|
|
621
845
|
end.parse!(argv)
|
|
622
846
|
opts
|
|
623
847
|
end
|
|
@@ -627,17 +851,27 @@ module Portage
|
|
|
627
851
|
opts = parse_doctor_options(argv)
|
|
628
852
|
require File.expand_path(opts[:require]) if opts[:require]
|
|
629
853
|
adapter_class = opts[:adapter] && Object.const_get(opts[:adapter])
|
|
854
|
+
proxy_settings = apply_proxy_settings(opts[:proxy])
|
|
855
|
+
return 1 unless proxy_settings
|
|
630
856
|
|
|
631
|
-
|
|
632
|
-
puts opts[:json] ? JSON.pretty_generate(findings.map(&:to_h)) : format_doctor(findings)
|
|
633
|
-
findings.empty? ? 0 : 1
|
|
857
|
+
report_doctor(Doctor.new(adapter_class: adapter_class, proxy_settings: proxy_settings).call, json: opts[:json])
|
|
634
858
|
end
|
|
635
859
|
private_class_method :run_doctor
|
|
636
860
|
|
|
637
|
-
def self.
|
|
638
|
-
|
|
861
|
+
def self.report_doctor(findings, json:)
|
|
862
|
+
puts json ? JSON.pretty_generate(findings.map(&:to_h)) : format_doctor(findings)
|
|
863
|
+
findings.none?(&:warning?) ? 0 : 1
|
|
864
|
+
end
|
|
865
|
+
private_class_method :report_doctor
|
|
639
866
|
|
|
640
|
-
|
|
867
|
+
# Info findings (install method, Ruby, adapters, PATH) first, then the
|
|
868
|
+
# warnings, which alone decide the exit code.
|
|
869
|
+
def self.format_doctor(findings)
|
|
870
|
+
info, warnings = findings.partition { |f| !f.warning? }
|
|
871
|
+
lines = info.map { |f| "[#{f.check}] #{f.message}" }
|
|
872
|
+
lines << "" unless info.empty?
|
|
873
|
+
lines.concat(warnings.empty? ? ["No issues found."] : warnings.map { |f| "[#{f.check}] #{f.message}" })
|
|
874
|
+
lines.join("\n")
|
|
641
875
|
end
|
|
642
876
|
private_class_method :format_doctor
|
|
643
877
|
|
metadata
CHANGED
|
@@ -1,29 +1,43 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: portage-cli
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.7.
|
|
4
|
+
version: 0.7.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tom Whitbread
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-25 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: irb
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.6'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '1.6'
|
|
13
27
|
- !ruby/object:Gem::Dependency
|
|
14
28
|
name: portage-ucp
|
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
|
16
30
|
requirements:
|
|
17
31
|
- - "~>"
|
|
18
32
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '0.
|
|
33
|
+
version: '0.10'
|
|
20
34
|
type: :runtime
|
|
21
35
|
prerelease: false
|
|
22
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
37
|
requirements:
|
|
24
38
|
- - "~>"
|
|
25
39
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: '0.
|
|
40
|
+
version: '0.10'
|
|
27
41
|
- !ruby/object:Gem::Dependency
|
|
28
42
|
name: portage-ucp-client
|
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -33,7 +47,7 @@ dependencies:
|
|
|
33
47
|
version: '0.6'
|
|
34
48
|
- - ">="
|
|
35
49
|
- !ruby/object:Gem::Version
|
|
36
|
-
version: 0.6.
|
|
50
|
+
version: 0.6.3
|
|
37
51
|
type: :runtime
|
|
38
52
|
prerelease: false
|
|
39
53
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -43,7 +57,7 @@ dependencies:
|
|
|
43
57
|
version: '0.6'
|
|
44
58
|
- - ">="
|
|
45
59
|
- !ruby/object:Gem::Version
|
|
46
|
-
version: 0.6.
|
|
60
|
+
version: 0.6.3
|
|
47
61
|
- !ruby/object:Gem::Dependency
|
|
48
62
|
name: portage-ucp-journal
|
|
49
63
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -154,17 +168,34 @@ files:
|
|
|
154
168
|
- lib/portage/cli/find.rb
|
|
155
169
|
- lib/portage/cli/generate/adapter.rb
|
|
156
170
|
- lib/portage/cli/generate/agent_profile.rb
|
|
171
|
+
- lib/portage/cli/handoff_reconciler.rb
|
|
172
|
+
- lib/portage/cli/handoff_reconciler/wire_adapters.rb
|
|
173
|
+
- lib/portage/cli/handoff_spend_mode.rb
|
|
174
|
+
- lib/portage/cli/handoff_wait_timeout.rb
|
|
175
|
+
- lib/portage/cli/handoff_waiter.rb
|
|
157
176
|
- lib/portage/cli/history.rb
|
|
177
|
+
- lib/portage/cli/homepage_fetch.rb
|
|
178
|
+
- lib/portage/cli/install_doctor.rb
|
|
179
|
+
- lib/portage/cli/macos_notifier.rb
|
|
158
180
|
- lib/portage/cli/notifier.rb
|
|
159
181
|
- lib/portage/cli/payment_methods.rb
|
|
160
182
|
- lib/portage/cli/payment_methods/env_backend.rb
|
|
161
183
|
- lib/portage/cli/payment_methods/keychain_backend.rb
|
|
162
184
|
- lib/portage/cli/payment_methods/secret_service_backend.rb
|
|
185
|
+
- lib/portage/cli/permissive_authenticator.rb
|
|
163
186
|
- lib/portage/cli/probe_cache.rb
|
|
187
|
+
- lib/portage/cli/proxy_doctor.rb
|
|
188
|
+
- lib/portage/cli/proxy_settings.rb
|
|
189
|
+
- lib/portage/cli/proxy_settings/password_ref.rb
|
|
190
|
+
- lib/portage/cli/reconcile_notifier.rb
|
|
191
|
+
- lib/portage/cli/reconcile_notify.rb
|
|
164
192
|
- lib/portage/cli/search_backends.rb
|
|
165
193
|
- lib/portage/cli/setting.rb
|
|
166
194
|
- lib/portage/cli/shipping_profile.rb
|
|
195
|
+
- lib/portage/cli/user_agent.rb
|
|
167
196
|
- lib/portage/cli/version.rb
|
|
197
|
+
- lib/portage/cli/webmcp.rb
|
|
198
|
+
- lib/portage/cli/webmcp_checkout_mode.rb
|
|
168
199
|
homepage: https://github.com/tomtom87/Portage/tree/main/portage-cli
|
|
169
200
|
licenses:
|
|
170
201
|
- MIT
|