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.
@@ -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
- Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https",
49
- open_timeout: OPEN_TIMEOUT, read_timeout: READ_TIMEOUT) do |http|
50
- http.get(uri.request_uri, { "User-Agent" => USER_AGENT }.merge(headers))
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
@@ -1,5 +1,5 @@
1
1
  module Portage
2
2
  module Cli
3
- VERSION = "0.6.4".freeze
3
+ VERSION = "0.7.3".freeze
4
4
  end
5
5
  end
@@ -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