portage-cli 0.6.4 → 0.7.3
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 +216 -0
- data/README.md +263 -7
- data/lib/portage/cli/buy.rb +529 -104
- data/lib/portage/cli/checkout_handoff.rb +6 -18
- data/lib/portage/cli/confidence_check.rb +127 -0
- data/lib/portage/cli/decisions.rb +72 -0
- data/lib/portage/cli/doctor.rb +104 -1
- data/lib/portage/cli/find.rb +10 -9
- 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/history.rb +23 -6
- data/lib/portage/cli/homepage_fetch.rb +41 -0
- data/lib/portage/cli/macos_notifier.rb +37 -0
- data/lib/portage/cli/notifier.rb +26 -27
- 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/setting.rb +44 -0
- 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 +367 -27
- metadata +46 -5
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
require_relative "config"
|
|
2
|
+
require_relative "setting"
|
|
3
|
+
|
|
4
|
+
module Portage
|
|
5
|
+
module Cli
|
|
6
|
+
# docs/plans/handoff-reconcile.md Phase 3 — which channels fire when a
|
|
7
|
+
# handoff settles, from `portage buy --wait` or `portage orders
|
|
8
|
+
# reconcile` (see ReconcileNotifier). A comma list, same Setting
|
|
9
|
+
# precedence as every other standing preference here: an explicit
|
|
10
|
+
# `override:` beats PORTAGE_RECONCILE_NOTIFY, which beats config.json's
|
|
11
|
+
# "reconcile_notify". Unset is `webhook` only.
|
|
12
|
+
#
|
|
13
|
+
# "journal" is accepted but drives nothing here: the Phase 1 order
|
|
14
|
+
# snapshot journal write already happens unconditionally in
|
|
15
|
+
# HandoffReconciler#record_journal whenever a completed checkout carries
|
|
16
|
+
# an order. Naming it in `reconcile_notify` documents that as always-on
|
|
17
|
+
# rather than gating a second write path.
|
|
18
|
+
module ReconcileNotify
|
|
19
|
+
ENV_VAR = "PORTAGE_RECONCILE_NOTIFY".freeze
|
|
20
|
+
CONFIG_KEY = "reconcile_notify".freeze
|
|
21
|
+
CHANNELS = %w[webhook journal macos terminal].freeze
|
|
22
|
+
DEFAULT = %w[webhook].freeze
|
|
23
|
+
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
# @param extra [Array<String>] channels to force on regardless of
|
|
27
|
+
# configuration — `portage buy --wait` forces `terminal` in plain
|
|
28
|
+
# (non-`--json`) mode, so the shopper sees a line when it settles
|
|
29
|
+
# even with nothing configured.
|
|
30
|
+
# @return [Array<String>]
|
|
31
|
+
def resolve(override: nil, config: Config.load, extra: [])
|
|
32
|
+
raw = Setting.resolve(override: override, env: ENV_VAR, config: config, config_key: CONFIG_KEY)
|
|
33
|
+
configured = raw.nil? ? DEFAULT : raw.to_s.split(",").map(&:strip)
|
|
34
|
+
((configured & CHANNELS) + extra).uniq
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -2,6 +2,9 @@ require "net/http"
|
|
|
2
2
|
require "uri"
|
|
3
3
|
require "json"
|
|
4
4
|
require "yaml"
|
|
5
|
+
require "portage/ucp"
|
|
6
|
+
require "portage/ucp/support/connection"
|
|
7
|
+
require_relative "user_agent"
|
|
5
8
|
|
|
6
9
|
module Portage
|
|
7
10
|
module Cli
|
|
@@ -18,7 +21,6 @@ module Portage
|
|
|
18
21
|
module SearchBackends
|
|
19
22
|
OPEN_TIMEOUT = 5
|
|
20
23
|
READ_TIMEOUT = 5
|
|
21
|
-
USER_AGENT = "portage-find".freeze
|
|
22
24
|
|
|
23
25
|
# Reference works and marketplaces-of-links that a search backend will
|
|
24
26
|
# happily return for a product query but that are never themselves a UCP
|
|
@@ -45,9 +47,9 @@ module Portage
|
|
|
45
47
|
end
|
|
46
48
|
|
|
47
49
|
def self.request(uri, headers)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
http.get(uri.request_uri,
|
|
50
|
+
Portage::Ucp::Support::Connection.start(uri, route: :search, open_timeout: OPEN_TIMEOUT,
|
|
51
|
+
read_timeout: READ_TIMEOUT) do |http|
|
|
52
|
+
http.get(uri.request_uri, UserAgent.headers.merge(headers))
|
|
51
53
|
end
|
|
52
54
|
end
|
|
53
55
|
private_class_method :request
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module Portage
|
|
2
|
+
module Cli
|
|
3
|
+
# How portage-cli resolves a standing preference, in one place
|
|
4
|
+
# (docs/plans/checkout-handoff-delivery.md open decision #1): a
|
|
5
|
+
# per-invocation override (a flag) beats its PORTAGE_* env var, which
|
|
6
|
+
# beats its ~/.portage/config.json key (Config). CheckoutHandoff,
|
|
7
|
+
# Notifier, Buy and ConfidenceCheck all read their settings through it.
|
|
8
|
+
#
|
|
9
|
+
# nil or a blank string counts as unset at every level, so an exported
|
|
10
|
+
# but empty `PORTAGE_AUTO_OPEN_CHECKOUT=` falls through to config.json
|
|
11
|
+
# rather than silently meaning "off".
|
|
12
|
+
module Setting
|
|
13
|
+
TRUE_VALUES = %w[1 true yes].freeze
|
|
14
|
+
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
# @param override [Object, nil] the flag's value, nil when not passed.
|
|
18
|
+
# @param env [String, nil] the env var's name.
|
|
19
|
+
# @param config [Config, nil]
|
|
20
|
+
# @param config_key [String, nil]
|
|
21
|
+
# @return [Object, nil] the value from the first level that's set.
|
|
22
|
+
def resolve(override: nil, env: nil, config: nil, config_key: nil)
|
|
23
|
+
return override if set?(override)
|
|
24
|
+
|
|
25
|
+
from_env = env && ENV.fetch(env, nil)
|
|
26
|
+
return from_env if set?(from_env)
|
|
27
|
+
|
|
28
|
+
from_config = config_key && config&.get(config_key)
|
|
29
|
+
from_config if set?(from_config)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# The same precedence, read as yes/no: true, or a string in
|
|
33
|
+
# TRUE_VALUES (any case). Anything else, unset included, is no.
|
|
34
|
+
def flag?(**)
|
|
35
|
+
TRUE_VALUES.include?(resolve(**).to_s.strip.downcase)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def set?(value)
|
|
39
|
+
!(value.nil? || (value.is_a?(String) && value.strip.empty?))
|
|
40
|
+
end
|
|
41
|
+
private_class_method :set?
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
require "portage/ucp/client"
|
|
2
|
+
require_relative "version"
|
|
3
|
+
require_relative "config"
|
|
4
|
+
require_relative "setting"
|
|
5
|
+
|
|
6
|
+
module Portage
|
|
7
|
+
module Cli
|
|
8
|
+
# Every request this CLI makes — to a store, a search backend, a
|
|
9
|
+
# payment handler, or a notify webhook — names itself, then the client
|
|
10
|
+
# gem it speaks UCP through, so a merchant reading its logs sees
|
|
11
|
+
# "portage-cli/... portage-ucp-client/... (+https://github.com/...)"
|
|
12
|
+
# rather than Ruby's default and knows where to look.
|
|
13
|
+
#
|
|
14
|
+
# A caller running many agents behind one IP (or one that just wants
|
|
15
|
+
# its own contact info in the string a merchant might grep for) can
|
|
16
|
+
# override it — same Setting precedence as Notifier's webhook URL:
|
|
17
|
+
# PORTAGE_USER_AGENT beats ~/.portage/config.json's "user_agent" key,
|
|
18
|
+
# both of which beat this default. Resolved fresh on every call rather
|
|
19
|
+
# than frozen at load time, so a config.json edit or an ENV change
|
|
20
|
+
# takes effect without restarting whatever's driving this CLI.
|
|
21
|
+
module UserAgent
|
|
22
|
+
DEFAULT = "portage-cli/#{VERSION} #{Portage::Ucp::Client::USER_AGENT}".freeze
|
|
23
|
+
ENV_VAR = "PORTAGE_USER_AGENT".freeze
|
|
24
|
+
CONFIG_KEY = "user_agent".freeze
|
|
25
|
+
|
|
26
|
+
module_function
|
|
27
|
+
|
|
28
|
+
# @param config [Config]
|
|
29
|
+
# @return [String] never blank — falls back to DEFAULT.
|
|
30
|
+
def value(config: Config.load)
|
|
31
|
+
Setting.resolve(env: ENV_VAR, config: config, config_key: CONFIG_KEY) || DEFAULT
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @param config [Config]
|
|
35
|
+
# @return [Hash] a fresh header hash, safe for a caller to #merge into.
|
|
36
|
+
def headers(config: Config.load) = { "User-Agent" => value(config: config) }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
data/lib/portage/cli/version.rb
CHANGED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Portage
|
|
2
|
+
module Cli
|
|
3
|
+
# docs/plans/handoff-reconcile.md Phase 4 — `portage-ucp-webmcp` is
|
|
4
|
+
# optional at runtime, not in portage-cli's gemspec, same posture as
|
|
5
|
+
# `Decisions.available?` for `portage-ucp-decision` and
|
|
6
|
+
# `Resolver.build_adapter` for a platform adapter gem: `require`, then
|
|
7
|
+
# rescue LoadError, so `gem install portage-cli` stays light and only a
|
|
8
|
+
# caller who actually passes `Buy.new(webmcp_bridge:)` ever needs it
|
|
9
|
+
# installed.
|
|
10
|
+
module Webmcp
|
|
11
|
+
# @return [Boolean] whether portage-ucp-webmcp could be loaded.
|
|
12
|
+
# Memoized: `require` runs once per process.
|
|
13
|
+
def self.available?
|
|
14
|
+
return @available unless @available.nil?
|
|
15
|
+
|
|
16
|
+
@available = begin
|
|
17
|
+
require "portage/ucp/webmcp"
|
|
18
|
+
true
|
|
19
|
+
rescue LoadError
|
|
20
|
+
false
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
require_relative "config"
|
|
2
|
+
require_relative "setting"
|
|
3
|
+
|
|
4
|
+
module Portage
|
|
5
|
+
module Cli
|
|
6
|
+
# docs/plans/handoff-reconcile.md Phase 4 — how `Buy`'s optional WebMCP
|
|
7
|
+
# path (see Webmcp, Buy#webmcp_flow) finishes a checkout it built in the
|
|
8
|
+
# browser. Same Setting precedence as HandoffSpendMode: no CLI flag (a
|
|
9
|
+
# WebMCP bridge is an injected collaborator, not something a shell
|
|
10
|
+
# invocation can name), so just PORTAGE_WEBMCP_CHECKOUT_MODE >
|
|
11
|
+
# config.json's "webmcp_checkout_mode". Unset, or anything not in
|
|
12
|
+
# MODES, is `express_stop`, the only mode actually implemented — see
|
|
13
|
+
# Buy#webmcp_token_unsupported_report for why `token` isn't yet.
|
|
14
|
+
module WebmcpCheckoutMode
|
|
15
|
+
ENV_VAR = "PORTAGE_WEBMCP_CHECKOUT_MODE".freeze
|
|
16
|
+
CONFIG_KEY = "webmcp_checkout_mode".freeze
|
|
17
|
+
MODES = %w[express_stop token].freeze
|
|
18
|
+
DEFAULT = "express_stop".freeze
|
|
19
|
+
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
def resolve(override: nil, config: Config.load)
|
|
23
|
+
mode = Setting.resolve(override: override, env: ENV_VAR, config: config, config_key: CONFIG_KEY)
|
|
24
|
+
MODES.include?(mode) ? mode : DEFAULT
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|