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
|
@@ -2,19 +2,13 @@ require "json"
|
|
|
2
2
|
require "fileutils"
|
|
3
3
|
require "securerandom"
|
|
4
4
|
require "uri"
|
|
5
|
-
require "net/http"
|
|
6
5
|
require "portage/ucp"
|
|
7
6
|
require "portage/ucp/client"
|
|
8
|
-
require "portage/ucp/support/connection"
|
|
9
7
|
|
|
10
|
-
# Buy::PermissiveAuthenticator (adapter-loopback auth) is used by
|
|
11
|
-
# #adapter_session below — not require_relative'd here to avoid a load
|
|
12
|
-
# cycle (buy.rb will require this file too, for PaymentMethods.default);
|
|
13
|
-
# cli.rb requires "cli/buy" before "cli/payment_methods", so it's already
|
|
14
|
-
# loaded by the time #enroll actually runs.
|
|
15
8
|
require_relative "payment_methods/keychain_backend"
|
|
16
9
|
require_relative "payment_methods/secret_service_backend"
|
|
17
10
|
require_relative "payment_methods/env_backend"
|
|
11
|
+
require_relative "adapter_session"
|
|
18
12
|
require_relative "user_agent"
|
|
19
13
|
require_relative "handoff_only"
|
|
20
14
|
|
|
@@ -220,7 +214,8 @@ module Portage
|
|
|
220
214
|
# Same native-manifest-first, own-store-adapter-fallback discovery as
|
|
221
215
|
# Buy#call — duplicated rather than extracted since Buy's version is
|
|
222
216
|
# entangled with cart/checkout-specific branching this only needs the
|
|
223
|
-
# session object from.
|
|
217
|
+
# session object from. The adapter fallback itself is AdapterSession,
|
|
218
|
+
# shared with HandoffReconciler.
|
|
224
219
|
def discover_session(url)
|
|
225
220
|
native = Portage::Ucp::Client.discover(url, headers: UserAgent.headers)
|
|
226
221
|
native if native
|
|
@@ -228,29 +223,11 @@ module Portage
|
|
|
228
223
|
adapter_session(url)
|
|
229
224
|
end
|
|
230
225
|
|
|
226
|
+
# The homepage fetch stays on the :payment route (forced direct unless
|
|
227
|
+
# a proxy is named for it), redirects included.
|
|
231
228
|
def adapter_session(url)
|
|
232
|
-
uri =
|
|
233
|
-
|
|
234
|
-
platform = body && Portage::Ucp::Resolver.detect_platform(body, headers)
|
|
235
|
-
return nil unless platform
|
|
236
|
-
|
|
237
|
-
env = Portage::Ucp::Resolver.env_for(platform)
|
|
238
|
-
return nil if Portage::Ucp::Resolver.missing_env(platform, env).any?
|
|
239
|
-
|
|
240
|
-
adapter = Portage::Ucp::Resolver.build_adapter(platform, env)
|
|
241
|
-
Portage::Ucp::Client.for_adapter(adapter, authenticator: Buy::PermissiveAuthenticator.new)
|
|
242
|
-
rescue StandardError
|
|
243
|
-
nil
|
|
244
|
-
end
|
|
245
|
-
|
|
246
|
-
def fetch_homepage(uri)
|
|
247
|
-
response = Portage::Ucp::Support::Connection.start(uri, route: :payment, open_timeout: 5,
|
|
248
|
-
read_timeout: 5) do |http|
|
|
249
|
-
http.get(uri.request_uri, UserAgent.headers)
|
|
250
|
-
end
|
|
251
|
-
response.is_a?(Net::HTTPSuccess) ? [response.body, response.to_hash] : [nil, {}]
|
|
252
|
-
rescue StandardError
|
|
253
|
-
[nil, {}]
|
|
229
|
+
uri = normalized_uri(url)
|
|
230
|
+
uri && AdapterSession.call(uri, route: :payment)
|
|
254
231
|
end
|
|
255
232
|
|
|
256
233
|
def payment_enrollment_advertised?(session)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
require_relative "notifier"
|
|
2
2
|
require_relative "macos_notifier"
|
|
3
3
|
require_relative "reconcile_notify"
|
|
4
|
+
require_relative "money"
|
|
4
5
|
|
|
5
6
|
module Portage
|
|
6
7
|
module Cli
|
|
@@ -55,11 +56,7 @@ module Portage
|
|
|
55
56
|
parts.join(" — ")
|
|
56
57
|
end
|
|
57
58
|
|
|
58
|
-
def format_amount(amount, currency)
|
|
59
|
-
return nil unless amount
|
|
60
|
-
|
|
61
|
-
"#{format('%.2f', amount / 100.0)}#{" #{currency}" if currency}"
|
|
62
|
-
end
|
|
59
|
+
def format_amount(amount, currency) = amount && Money.format_amount(amount, currency)
|
|
63
60
|
end
|
|
64
61
|
end
|
|
65
62
|
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
require_relative "../../dot_env"
|
|
2
|
+
|
|
3
|
+
module Portage
|
|
4
|
+
module Cli
|
|
5
|
+
class SetupWizard
|
|
6
|
+
module Steps
|
|
7
|
+
# Shared body of the wizard steps that only prompt for a few env vars
|
|
8
|
+
# and write the answers to ~/.portage/.env (Shipping, SearchKeys,
|
|
9
|
+
# RetailerKeys). A subclass defines #title, #default_yes?, FIELDS
|
|
10
|
+
# ({ "ENV_VAR" => "label" }), #intro and #unchanged_message, and
|
|
11
|
+
# overrides #secret? for fields that must never echo back.
|
|
12
|
+
class EnvKeysStep
|
|
13
|
+
def initialize(prompt:) = @prompt = prompt
|
|
14
|
+
|
|
15
|
+
def call
|
|
16
|
+
@prompt.say(intro)
|
|
17
|
+
assignments = collect
|
|
18
|
+
return @prompt.say(unchanged_message) if assignments.empty?
|
|
19
|
+
|
|
20
|
+
path = DotEnv.update!(assignments)
|
|
21
|
+
@prompt.say("Saved #{assignments.keys.join(', ')} to #{path} (chmod 600).")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def fields = self.class::FIELDS
|
|
27
|
+
def secret?(_var) = false
|
|
28
|
+
|
|
29
|
+
def collect
|
|
30
|
+
fields.each_with_object({}) do |(var, label), assignments|
|
|
31
|
+
hint = ENV.fetch(var, nil).to_s.empty? ? "not set" : "already set"
|
|
32
|
+
answer = secret?(var) ? @prompt.ask_secret(label, hint: hint) : @prompt.ask(label, hint: hint)
|
|
33
|
+
assignments[var] = answer if answer
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
require_relative "
|
|
1
|
+
require_relative "env_keys_step"
|
|
2
2
|
|
|
3
3
|
module Portage
|
|
4
4
|
module Cli
|
|
@@ -12,7 +12,7 @@ module Portage
|
|
|
12
12
|
# setting a key here only makes `find` show more real offers, it
|
|
13
13
|
# never lets `portage buy` complete a purchase at any of these
|
|
14
14
|
# retailers.
|
|
15
|
-
class RetailerKeys
|
|
15
|
+
class RetailerKeys < EnvKeysStep
|
|
16
16
|
FIELDS = {
|
|
17
17
|
"WALMART_AFFILIATE_API_KEY" => "Walmart Affiliate API key",
|
|
18
18
|
"EBAY_BROWSE_ACCESS_TOKEN" => "eBay Browse API access token (Buy It Now search only)",
|
|
@@ -21,33 +21,21 @@ module Portage
|
|
|
21
21
|
"AMAZON_CREATORS_ACCESS_TOKEN" => "Amazon Creators API access token"
|
|
22
22
|
}.freeze
|
|
23
23
|
|
|
24
|
-
def initialize(prompt:) = @prompt = prompt
|
|
25
|
-
|
|
26
24
|
def title = "Retailer offer sources"
|
|
27
25
|
def default_yes? = false
|
|
28
26
|
|
|
29
|
-
def call
|
|
30
|
-
@prompt.say("Optional: official buyer-side APIs for Walmart, eBay (Buy It Now only), Best " \
|
|
31
|
-
"Buy, Etsy and Amazon. Each needs its own key from that retailer's developer " \
|
|
32
|
-
"program — skip any you don't have. None of these ever completes a purchase: " \
|
|
33
|
-
"every offer they return still ends in hand-off, same as Amazon today. Neither " \
|
|
34
|
-
"key is echoed back; Enter keeps whatever's already set.")
|
|
35
|
-
assignments = collect
|
|
36
|
-
return @prompt.say("Left retailer offer source keys unchanged.") if assignments.empty?
|
|
37
|
-
|
|
38
|
-
path = DotEnv.update!(assignments)
|
|
39
|
-
@prompt.say("Saved #{assignments.keys.join(', ')} to #{path} (chmod 600).")
|
|
40
|
-
end
|
|
41
|
-
|
|
42
27
|
private
|
|
43
28
|
|
|
44
|
-
def
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
29
|
+
def intro
|
|
30
|
+
"Optional: official buyer-side APIs for Walmart, eBay (Buy It Now only), Best " \
|
|
31
|
+
"Buy, Etsy and Amazon. Each needs its own key from that retailer's developer " \
|
|
32
|
+
"program — skip any you don't have. None of these ever completes a purchase: " \
|
|
33
|
+
"every offer they return still ends in hand-off, same as Amazon today. Neither " \
|
|
34
|
+
"key is echoed back; Enter keeps whatever's already set."
|
|
50
35
|
end
|
|
36
|
+
|
|
37
|
+
def unchanged_message = "Left retailer offer source keys unchanged."
|
|
38
|
+
def secret?(_var) = true
|
|
51
39
|
end
|
|
52
40
|
end
|
|
53
41
|
end
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
require_relative "
|
|
1
|
+
require_relative "env_keys_step"
|
|
2
2
|
|
|
3
3
|
module Portage
|
|
4
4
|
module Cli
|
|
@@ -10,7 +10,7 @@ module Portage
|
|
|
10
10
|
# with no URL only ever resolves DuckDuckGo's Instant Answer API,
|
|
11
11
|
# which is keyless but only ever matches a specific brand/product
|
|
12
12
|
# name, not an open-ended query ("coffee", "hiking boots").
|
|
13
|
-
class SearchKeys
|
|
13
|
+
class SearchKeys < EnvKeysStep
|
|
14
14
|
FIELDS = {
|
|
15
15
|
"BRAVE_SEARCH_API_KEY" => "Brave Search API key",
|
|
16
16
|
"GOOGLE_CSE_KEY" => "Google Programmable Search API key",
|
|
@@ -21,32 +21,20 @@ module Portage
|
|
|
21
21
|
# only the two API keys are secrets that never echo back.
|
|
22
22
|
SECRET = %w[BRAVE_SEARCH_API_KEY GOOGLE_CSE_KEY].freeze
|
|
23
23
|
|
|
24
|
-
def initialize(prompt:) = @prompt = prompt
|
|
25
|
-
|
|
26
24
|
def title = "Search API keys"
|
|
27
25
|
def default_yes? = true
|
|
28
26
|
|
|
29
|
-
def call
|
|
30
|
-
@prompt.say("DuckDuckGo's free API is already active and needs no key, but only resolves a " \
|
|
31
|
-
"specific brand/product name — set either Brave's key, or both Google fields, for " \
|
|
32
|
-
"real open-ended search. Neither key is echoed back; Enter keeps whatever's " \
|
|
33
|
-
"already set.")
|
|
34
|
-
assignments = collect
|
|
35
|
-
return @prompt.say("Left search API keys unchanged.") if assignments.empty?
|
|
36
|
-
|
|
37
|
-
path = DotEnv.update!(assignments)
|
|
38
|
-
@prompt.say("Saved #{assignments.keys.join(', ')} to #{path} (chmod 600).")
|
|
39
|
-
end
|
|
40
|
-
|
|
41
27
|
private
|
|
42
28
|
|
|
43
|
-
def
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
end
|
|
29
|
+
def intro
|
|
30
|
+
"DuckDuckGo's free API is already active and needs no key, but only resolves a " \
|
|
31
|
+
"specific brand/product name — set either Brave's key, or both Google fields, for " \
|
|
32
|
+
"real open-ended search. Neither key is echoed back; Enter keeps whatever's " \
|
|
33
|
+
"already set."
|
|
49
34
|
end
|
|
35
|
+
|
|
36
|
+
def unchanged_message = "Left search API keys unchanged."
|
|
37
|
+
def secret?(var) = SECRET.include?(var)
|
|
50
38
|
end
|
|
51
39
|
end
|
|
52
40
|
end
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
require_relative "../../shipping_profile"
|
|
2
|
-
require_relative "
|
|
2
|
+
require_relative "env_keys_step"
|
|
3
3
|
|
|
4
4
|
module Portage
|
|
5
5
|
module Cli
|
|
@@ -12,7 +12,7 @@ module Portage
|
|
|
12
12
|
# store prices in no market and can report in-stock items as out of
|
|
13
13
|
# stock (see Doctor#shipping_finding, which this step exists to
|
|
14
14
|
# resolve).
|
|
15
|
-
class Shipping
|
|
15
|
+
class Shipping < EnvKeysStep
|
|
16
16
|
LABELS = {
|
|
17
17
|
street_address: "Street address", extended_address: "Apt/suite (optional)",
|
|
18
18
|
address_locality: "City", address_region: "State/region (optional)",
|
|
@@ -21,29 +21,15 @@ module Portage
|
|
|
21
21
|
phone_number: "Phone (optional)"
|
|
22
22
|
}.freeze
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
FIELDS = ShippingProfile::ENV_VARS.to_h { |key, var| [var, LABELS.fetch(key)] }.freeze
|
|
25
25
|
|
|
26
26
|
def title = "Shipping address"
|
|
27
27
|
def default_yes? = true
|
|
28
28
|
|
|
29
|
-
def call
|
|
30
|
-
@prompt.say("Enter keeps whatever's already set for a field.")
|
|
31
|
-
assignments = collect
|
|
32
|
-
return @prompt.say("Left shipping address unchanged.") if assignments.empty?
|
|
33
|
-
|
|
34
|
-
path = DotEnv.update!(assignments)
|
|
35
|
-
@prompt.say("Saved #{assignments.keys.join(', ')} to #{path} (chmod 600).")
|
|
36
|
-
end
|
|
37
|
-
|
|
38
29
|
private
|
|
39
30
|
|
|
40
|
-
def
|
|
41
|
-
|
|
42
|
-
hint = ENV.fetch(var, nil).to_s.empty? ? "not set" : "already set"
|
|
43
|
-
answer = @prompt.ask(LABELS.fetch(key), hint: hint)
|
|
44
|
-
assignments[var] = answer if answer
|
|
45
|
-
end
|
|
46
|
-
end
|
|
31
|
+
def intro = "Enter keeps whatever's already set for a field."
|
|
32
|
+
def unchanged_message = "Left shipping address unchanged."
|
|
47
33
|
end
|
|
48
34
|
end
|
|
49
35
|
end
|
data/lib/portage/cli/version.rb
CHANGED