portage-cli 0.9.0 → 0.10.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 +12 -0
- data/README.md +13 -0
- data/lib/portage/cli/buy.rb +3 -24
- data/lib/portage/cli/check.rb +154 -0
- data/lib/portage/cli/check_next_step.rb +51 -0
- data/lib/portage/cli/handoff_host.rb +33 -0
- data/lib/portage/cli/version.rb +1 -1
- data/lib/portage/cli.rb +51 -2
- metadata +6 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c506344f3b17a2ea4199f84efaf70d658b0debec2fe03d167b847c58617e800b
|
|
4
|
+
data.tar.gz: 20c00c246e59fa9059611f7bb54380140295eada2c5deb457a11ec94309c1b04
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9a3eff64f57ab81774a2a52034232a4667227aaff13aa7279d26dd41cf52faeafeea691711755fb41fd2f3a0a67f77d144d248d48c2631779a49c10bec9f2882
|
|
7
|
+
data.tar.gz: ade0a2ba64ba928ec1272a8902858bad1f79897f5059d428528b8978e4045047ee1a0e6e2579dc2f8a468fa73b0c3491cc66c48f2bc1a18951600e05bd2a25e1
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,18 @@ pre-1.0, so APIs may still shift between minor versions.
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.10.0] - 2026-09-29
|
|
10
|
+
|
|
11
|
+
- **`portage check <url> [--json]`.** Reports whether Portage can buy from a store and
|
|
12
|
+
how: `verdict` (`automated`, `webmcp`, `handoff`, `unsupported`), `next_step`, and the
|
|
13
|
+
detail behind them (native UCP, platform, adapter install and missing env, hand-off-only,
|
|
14
|
+
WebMCP status). Wraps `Portage::Ucp::Check` and follows the precedence `buy` uses.
|
|
15
|
+
Plain GETs only, hand-off-only hosts are not contacted, and WebMCP is read only from an
|
|
16
|
+
already-open Portage profile tab. Exits `0` for `automated` and `webmcp`. The hand-off-only
|
|
17
|
+
test moved into a shared `HandoffHost` so `buy` and `check` can't disagree.
|
|
18
|
+
Follows a homepage `<link rel="ucp">` manifest pointer through `portage-ucp` 0.11.0,
|
|
19
|
+
which this release now requires (`~> 0.11`).
|
|
20
|
+
|
|
9
21
|
## [0.9.0] - 2026-09-29
|
|
10
22
|
|
|
11
23
|
- **Human pick and approve** (`docs/plans/human-pick-and-approve.md`, Phases 1-3).
|
data/README.md
CHANGED
|
@@ -133,6 +133,7 @@ portage buy --query "..." [--store URL] [--max-price N] [--limit N] ...
|
|
|
133
133
|
portage find --query "..." [--max-price N] [--limit N] [--json]
|
|
134
134
|
portage compare <url> --product-id ID [--id VALUE ...] [--results N]
|
|
135
135
|
[--max-price N] [--json]
|
|
136
|
+
portage check <url> [--json]
|
|
136
137
|
portage pick [--search LAST|SEARCH_ID] [--via auto|tty|agent] [--json]
|
|
137
138
|
[--choose REF | --compare REF | --view REF]
|
|
138
139
|
portage approve QUOTE_ID [--via auto|tty|agent] [--relayed-yes | --view] [--json]
|
|
@@ -218,6 +219,18 @@ used (an unreadable value, an out-of-range threshold) stops the buy before
|
|
|
218
219
|
anything runs: on stderr normally, or as a JSON report with `outcome:
|
|
219
220
|
"invalid_option"` under `--json`.
|
|
220
221
|
|
|
222
|
+
### Check
|
|
223
|
+
|
|
224
|
+
`portage check <url> [--json]` answers "can Portage buy from this store, and how?".
|
|
225
|
+
It checks for a native `/.well-known/ucp` manifest, detects the platform, notes
|
|
226
|
+
whether its adapter gem is installed and which env vars are missing, and looks for
|
|
227
|
+
WebMCP tools, using the same hand-off-only rules as `buy`. `verdict` is
|
|
228
|
+
`automated`, `webmcp`, `handoff` or `unsupported`, with a plain-English
|
|
229
|
+
`next_step`. Exits `0` for `automated` and `webmcp`. It sends plain GETs only, and
|
|
230
|
+
never contacts a hand-off-only host. WebMCP is read only from a tab your Portage
|
|
231
|
+
browser profile already has open on the store; it never launches a browser.
|
|
232
|
+
Takes the `--proxy*` flags.
|
|
233
|
+
|
|
221
234
|
### Compare
|
|
222
235
|
|
|
223
236
|
`portage compare <url> --product-id ID` finds other stores selling the same
|
data/lib/portage/cli/buy.rb
CHANGED
|
@@ -13,6 +13,7 @@ require_relative "checkout_handoff"
|
|
|
13
13
|
require_relative "money"
|
|
14
14
|
require_relative "notifier"
|
|
15
15
|
require_relative "handoff_only"
|
|
16
|
+
require_relative "handoff_host"
|
|
16
17
|
require_relative "offer_sources"
|
|
17
18
|
require_relative "handoff_target"
|
|
18
19
|
require_relative "handoff_agents"
|
|
@@ -189,32 +190,10 @@ module Portage
|
|
|
189
190
|
# "handoff_only_hosts" is the user's own list (see HandoffOnly).
|
|
190
191
|
def handoff_only?
|
|
191
192
|
@handoff_only ||= HandoffOnly.new
|
|
192
|
-
|
|
193
|
-
return true if OfferSources.retail_handoff_host?(@uri.host)
|
|
194
|
-
|
|
195
|
-
etsy_buyer_host?
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
# Phase 7 (docs/plans/buy-skill-and-local-browser.md):
|
|
199
|
-
# portage-ucp-etsy is a *seller*-side adapter — a shop owner with
|
|
200
|
-
# their own ETSY_* credentials set still reaches #adapter_flow
|
|
201
|
-
# unchanged below, the same "only your own store" rule every other
|
|
202
|
-
# adapter already gets (see the class comment at the top of this
|
|
203
|
-
# file). An ordinary buyer with no Etsy credentials of their own
|
|
204
|
-
# gets routed to hand-off here instead of a homepage fetch +
|
|
205
|
-
# platform detection that would just be scraping a stranger's
|
|
206
|
-
# listing — the buyer-side offer OfferSources::EtsyListings hands
|
|
207
|
-
# `find` has nothing this process can check out anyway.
|
|
208
|
-
def etsy_buyer_host?
|
|
209
|
-
HandoffOnly.matches_any?(@uri.host, %w[etsy.com]) && !etsy_adapter_configured?
|
|
193
|
+
HandoffHost.restricted?(@uri.host, handoff_only: @handoff_only)
|
|
210
194
|
end
|
|
211
195
|
|
|
212
|
-
def
|
|
213
|
-
platform = Portage::Ucp::Resolver::PLATFORMS.find { |p| p.name == "Etsy" }
|
|
214
|
-
return false unless platform
|
|
215
|
-
|
|
216
|
-
Portage::Ucp::Resolver.missing_env(platform, Portage::Ucp::Resolver.env_for(platform)).empty?
|
|
217
|
-
end
|
|
196
|
+
def etsy_buyer_host? = HandoffHost.etsy_buyer_host?(@uri.host)
|
|
218
197
|
|
|
219
198
|
# No checkout was ever built — there's nothing to browse or complete,
|
|
220
199
|
# just a link and why. `checkout_url` is built, never fetched: the
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
require "uri"
|
|
2
|
+
require "portage/ucp"
|
|
3
|
+
require_relative "handoff_only"
|
|
4
|
+
require_relative "handoff_host"
|
|
5
|
+
require_relative "check_next_step"
|
|
6
|
+
require_relative "webmcp"
|
|
7
|
+
require_relative "browser_profile"
|
|
8
|
+
|
|
9
|
+
module Portage
|
|
10
|
+
module Cli
|
|
11
|
+
# `portage check <url>` — "can Portage buy from this store, and how?"
|
|
12
|
+
#
|
|
13
|
+
# Wraps Portage::Ucp::Check (native `/.well-known/ucp`, platform
|
|
14
|
+
# detection, live adapter probe) and adds what only the CLI knows, so
|
|
15
|
+
# the answer matches what `portage buy` would actually do: the
|
|
16
|
+
# hand-off-only list (HandoffHost, shared with Buy), whether the adapter
|
|
17
|
+
# gem is installed, and whether the page exposes WebMCP tools.
|
|
18
|
+
#
|
|
19
|
+
# Precedence mirrors Buy#call: hand-off-only host, then native UCP, then
|
|
20
|
+
# WebMCP, then a platform adapter, then nothing. A hand-off-only host
|
|
21
|
+
# short-circuits before any request is made.
|
|
22
|
+
#
|
|
23
|
+
# Read-only and quiet: plain GETs only (through Ucp::Check), never a
|
|
24
|
+
# cart, never a checkout. WebMCP needs a live page, and Portage never
|
|
25
|
+
# launches a browser for this: it only reads tools from a tab the
|
|
26
|
+
# Portage browser profile already has open on the store's host
|
|
27
|
+
# (`portage browser profile open --url ...`). With no such tab, or with
|
|
28
|
+
# portage-ucp-webmcp not installed, `webmcp.status` is "skipped" and says
|
|
29
|
+
# why. A `webmcp_bridge:` can be injected instead (specs, library use).
|
|
30
|
+
class Check
|
|
31
|
+
CART_CAP = "dev.ucp.shopping.cart".freeze
|
|
32
|
+
CHECKOUT_CAP = "dev.ucp.shopping.checkout".freeze
|
|
33
|
+
|
|
34
|
+
# Verdicts `portage check` exits 0 for.
|
|
35
|
+
USABLE_VERDICTS = %w[automated webmcp].freeze
|
|
36
|
+
|
|
37
|
+
NO_TAB_REASON = "no Portage browser profile tab is open on this store " \
|
|
38
|
+
"(portage browser profile open --url URL), and check never launches a browser".freeze
|
|
39
|
+
|
|
40
|
+
def self.call(url, **) = new(url, **).call
|
|
41
|
+
|
|
42
|
+
# @param webmcp_bridge [#list_tools, nil] a bridge over the store's page.
|
|
43
|
+
# @param handoff_only [HandoffOnly]
|
|
44
|
+
# @param checker [#call] url -> Portage::Ucp::Check report (injectable).
|
|
45
|
+
def initialize(url, webmcp_bridge: nil, handoff_only: HandoffOnly.new, checker: Portage::Ucp::Check.method(:call))
|
|
46
|
+
raw = url.to_s.strip
|
|
47
|
+
@uri = URI.parse(raw =~ %r{\Ahttps?://}i ? raw : "https://#{raw}")
|
|
48
|
+
@webmcp_bridge = webmcp_bridge
|
|
49
|
+
@handoff_only = handoff_only
|
|
50
|
+
@checker = checker
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def call
|
|
54
|
+
return handoff_only_report if HandoffHost.restricted?(@uri.host, handoff_only: @handoff_only)
|
|
55
|
+
|
|
56
|
+
report = @checker.call(@uri.to_s).merge(handoff_only: false)
|
|
57
|
+
report = report.merge(adapter: adapter_for(report))
|
|
58
|
+
report = report.merge(webmcp: webmcp_for(report))
|
|
59
|
+
verdict = verdict_for(report)
|
|
60
|
+
report.merge(verdict: verdict, next_step: CheckNextStep.call(verdict, report))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def handoff_only_report
|
|
66
|
+
{ url: @uri.to_s, native_ucp: nil, platform: nil, recommended_gem: nil, handoff_only: true, adapter: nil,
|
|
67
|
+
webmcp: webmcp_skipped("#{@uri.host} is hand-off only, so it isn't probed"),
|
|
68
|
+
verdict: "handoff",
|
|
69
|
+
next_step: "#{@uri.host} restricts automated purchasing agents. Portage opens the page and you buy." }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# --- adapter ---
|
|
73
|
+
|
|
74
|
+
def adapter_for(report)
|
|
75
|
+
platform = Portage::Ucp::Resolver::PLATFORMS.find { |p| p.name == report[:platform] }
|
|
76
|
+
return nil unless platform
|
|
77
|
+
|
|
78
|
+
{ gem: platform.gem, installed: gem_installed?(platform),
|
|
79
|
+
missing_env: Portage::Ucp::Resolver.missing_env(platform, Portage::Ucp::Resolver.env_for(platform)) }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Same test the live probe uses: the adapter's require_path either
|
|
83
|
+
# loads or it doesn't. Loading has no I/O.
|
|
84
|
+
def gem_installed?(platform)
|
|
85
|
+
require platform.require_path
|
|
86
|
+
true
|
|
87
|
+
rescue LoadError
|
|
88
|
+
false
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# --- webmcp ---
|
|
92
|
+
|
|
93
|
+
def webmcp_for(report)
|
|
94
|
+
return webmcp_skipped("the store speaks UCP natively") if report[:native_ucp]
|
|
95
|
+
return webmcp_skipped("portage-ucp-webmcp isn't installed") unless Webmcp.available?
|
|
96
|
+
|
|
97
|
+
bridge = @webmcp_bridge || existing_tab_bridge
|
|
98
|
+
return webmcp_skipped(NO_TAB_REASON) unless bridge
|
|
99
|
+
|
|
100
|
+
detect_webmcp(bridge)
|
|
101
|
+
rescue StandardError => e
|
|
102
|
+
{ status: "error", tools: [], reason: "#{e.class}: #{e.message}" }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def webmcp_skipped(reason) = { status: "skipped", tools: [], reason: reason }
|
|
106
|
+
|
|
107
|
+
def detect_webmcp(bridge)
|
|
108
|
+
tools = bridge.list_tools
|
|
109
|
+
names = tools.map { |tool| (tool["name"] || tool[:name]).to_s }
|
|
110
|
+
return { status: "none", tools: [], reason: "the page registers no WebMCP tools" } if names.empty?
|
|
111
|
+
|
|
112
|
+
preset = Portage::Ucp::WebMcp::Presets.detect(tools)
|
|
113
|
+
session = Portage::Ucp::WebMcp.connect(bridge: bridge, preset: preset)
|
|
114
|
+
return { status: "available", tools: names, reason: nil } if cart_and_checkout?(session)
|
|
115
|
+
|
|
116
|
+
{ status: "none", tools: names,
|
|
117
|
+
reason: "the page's WebMCP tools aren't a cart and checkout set Portage recognises" }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def cart_and_checkout?(session) = session.advertises?(CART_CAP) && session.advertises?(CHECKOUT_CAP)
|
|
121
|
+
|
|
122
|
+
# Only ever a tab that is already open on the store's host: no new tab
|
|
123
|
+
# is created, no browser is started, nothing is navigated.
|
|
124
|
+
def existing_tab_bridge
|
|
125
|
+
profile = BrowserProfile::Profile.new
|
|
126
|
+
return nil unless profile.status[:running]
|
|
127
|
+
|
|
128
|
+
tab = BrowserProfile::Cdp.list(port: profile.port).find { |t| t["type"] == "page" && same_host?(t["url"]) }
|
|
129
|
+
ws_url = tab && tab["webSocketDebuggerUrl"]
|
|
130
|
+
return nil unless ws_url
|
|
131
|
+
|
|
132
|
+
BrowserProfile::Bridge.new(socket: BrowserProfile::CdpSocket.connect(ws_url),
|
|
133
|
+
allowlist: BrowserProfile::Allowlist.new(hosts: [@uri.host]))
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def same_host?(url)
|
|
137
|
+
URI(url.to_s).host == @uri.host
|
|
138
|
+
rescue URI::InvalidURIError
|
|
139
|
+
false
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# --- verdict ---
|
|
143
|
+
|
|
144
|
+
def verdict_for(report)
|
|
145
|
+
return "automated" if report[:native_ucp]
|
|
146
|
+
return "webmcp" if report.dig(:webmcp, :status) == "available"
|
|
147
|
+
return "automated" if report.dig(:live_probe, :status) == "ok"
|
|
148
|
+
return "handoff" if report[:platform]
|
|
149
|
+
|
|
150
|
+
"unsupported"
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module Portage
|
|
2
|
+
module Cli
|
|
3
|
+
# The plain-English `next_step` of a Check report, one sentence per
|
|
4
|
+
# verdict — kept apart from Check so its detection logic stays readable.
|
|
5
|
+
module CheckNextStep
|
|
6
|
+
def self.call(verdict, report)
|
|
7
|
+
case verdict
|
|
8
|
+
when "automated" then automated(report)
|
|
9
|
+
when "webmcp" then "Portage will build the cart; you pay in your browser."
|
|
10
|
+
when "handoff" then adapter(report)
|
|
11
|
+
else unsupported(report)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.automated(report)
|
|
16
|
+
return "Nothing to do — this store speaks UCP natively." if report[:native_ucp]
|
|
17
|
+
|
|
18
|
+
"Nothing more to set up — the #{report[:platform]} adapter (#{report.dig(:adapter, :gem)}) is installed " \
|
|
19
|
+
"and answered a live probe."
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.adapter(report)
|
|
23
|
+
adapter = report[:adapter]
|
|
24
|
+
return "#{report[:url]} restricts automated purchasing agents. Portage opens the page and you buy." unless
|
|
25
|
+
adapter
|
|
26
|
+
|
|
27
|
+
actions = []
|
|
28
|
+
actions << "install #{adapter[:gem]}" unless adapter[:installed]
|
|
29
|
+
actions << "set #{adapter[:missing_env].join(', ')}" if adapter[:missing_env].any?
|
|
30
|
+
return probe_failed(report) if actions.empty?
|
|
31
|
+
|
|
32
|
+
"To automate this #{report[:platform]} store (adapters act as the store's owner), " \
|
|
33
|
+
"#{actions.join(' and ')}. Until then Portage opens the store and you buy."
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.probe_failed(report)
|
|
37
|
+
reason = report.dig(:live_probe, :reason)
|
|
38
|
+
"The #{report[:platform]} adapter is set up but its live probe failed#{" (#{reason})" if reason}. " \
|
|
39
|
+
"Run `portage doctor`; until then Portage opens the store and you buy."
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.unsupported(report)
|
|
43
|
+
webmcp = report[:webmcp]
|
|
44
|
+
note = webmcp[:status] == "skipped" ? " (WebMCP not checked: #{webmcp[:reason]})" : ""
|
|
45
|
+
"No UCP manifest, known platform or WebMCP cart found#{note}. Portage can only open the store " \
|
|
46
|
+
"for you to browse."
|
|
47
|
+
end
|
|
48
|
+
private_class_method :automated, :adapter, :probe_failed, :unsupported
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
require_relative "handoff_only"
|
|
2
|
+
require_relative "offer_sources"
|
|
3
|
+
|
|
4
|
+
module Portage
|
|
5
|
+
module Cli
|
|
6
|
+
# The one answer to "is this host hand-off only?" that both `buy` and
|
|
7
|
+
# `check` give, so `portage check` never says something `portage buy`
|
|
8
|
+
# wouldn't do. Three sources, in the order Buy has always consulted them:
|
|
9
|
+
# the user's own list (HandoffOnly, Amazon by default), the built-in
|
|
10
|
+
# retail hand-off hosts (Walmart, eBay, Best Buy), and Etsy for an
|
|
11
|
+
# ordinary buyer — portage-ucp-etsy is a seller-side adapter, so a shop
|
|
12
|
+
# owner with their own ETSY_* credentials set is not hand-off only.
|
|
13
|
+
module HandoffHost
|
|
14
|
+
def self.restricted?(host, handoff_only: HandoffOnly.new)
|
|
15
|
+
return true if handoff_only.host?(host)
|
|
16
|
+
return true if OfferSources.retail_handoff_host?(host)
|
|
17
|
+
|
|
18
|
+
etsy_buyer_host?(host)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.etsy_buyer_host?(host)
|
|
22
|
+
HandoffOnly.matches_any?(host, %w[etsy.com]) && !etsy_adapter_configured?
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.etsy_adapter_configured?
|
|
26
|
+
platform = Portage::Ucp::Resolver::PLATFORMS.find { |p| p.name == "Etsy" }
|
|
27
|
+
return false unless platform
|
|
28
|
+
|
|
29
|
+
Portage::Ucp::Resolver.missing_env(platform, Portage::Ucp::Resolver.env_for(platform)).empty?
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
data/lib/portage/cli/version.rb
CHANGED
data/lib/portage/cli.rb
CHANGED
|
@@ -15,6 +15,7 @@ require_relative "cli/browser_profile"
|
|
|
15
15
|
require_relative "cli/buy"
|
|
16
16
|
require_relative "cli/find"
|
|
17
17
|
require_relative "cli/compare"
|
|
18
|
+
require_relative "cli/check"
|
|
18
19
|
require_relative "cli/history"
|
|
19
20
|
require_relative "cli/quotes"
|
|
20
21
|
require_relative "cli/money"
|
|
@@ -56,6 +57,7 @@ module Portage
|
|
|
56
57
|
portage find --query "..." [--max-price N] [--limit N] [--json]
|
|
57
58
|
portage compare <url> --product-id ID [--id VALUE ...] [--results N]
|
|
58
59
|
[--max-price N] [--json]
|
|
60
|
+
portage check <url> [--json]
|
|
59
61
|
portage pick [--search LAST|SEARCH_ID] [--via auto|tty|agent] [--json]
|
|
60
62
|
[--choose REF | --compare REF | --view REF]
|
|
61
63
|
portage approve QUOTE_ID [--via auto|tty|agent] [--relayed-yes | --view] [--json]
|
|
@@ -93,13 +95,13 @@ module Portage
|
|
|
93
95
|
portage generate agent-profile [--out FILE] [--key-out FILE] [--rotate]
|
|
94
96
|
portage --version
|
|
95
97
|
|
|
96
|
-
proxy flags (buy/find/compare/doctor/payment enroll):
|
|
98
|
+
proxy flags (buy/find/compare/check/doctor/payment enroll):
|
|
97
99
|
[--proxy URL] [--proxy-mode forward|gateway] [--proxy-header "Name: value"]
|
|
98
100
|
[--no-proxy HOSTS] [--proxy-route ROUTE=URL|direct] [--proxy-chain URL,URL,...]
|
|
99
101
|
[--proxy-passthrough HEADER] [--proxy-ca FILE] [--no-env-proxy]
|
|
100
102
|
USAGE
|
|
101
103
|
|
|
102
|
-
COMMANDS = { "buy" => :run_buy, "find" => :run_find, "compare" => :run_compare,
|
|
104
|
+
COMMANDS = { "buy" => :run_buy, "find" => :run_find, "compare" => :run_compare, "check" => :run_check,
|
|
103
105
|
"pick" => :run_pick, "approve" => :run_approve,
|
|
104
106
|
"history" => :run_history, "payment" => :run_payment, "policy" => :run_policy,
|
|
105
107
|
"orders" => :run_orders, "index" => :run_index, "browser" => :run_browser,
|
|
@@ -207,6 +209,53 @@ module Portage
|
|
|
207
209
|
def self.to_minor_units(major) = (major * 100).round
|
|
208
210
|
private_class_method :to_minor_units
|
|
209
211
|
|
|
212
|
+
# --- check ---
|
|
213
|
+
|
|
214
|
+
# `portage check <url>` — can Portage buy from this store, and how?
|
|
215
|
+
# Exit 0 for `automated`/`webmcp`, 1 for anything else (and for a bad
|
|
216
|
+
# or missing URL, with the usage on stderr).
|
|
217
|
+
def self.run_check(argv)
|
|
218
|
+
opts = parse_check_options(argv)
|
|
219
|
+
return 1 unless opts
|
|
220
|
+
return 1 unless apply_proxy_settings(opts[:proxy])
|
|
221
|
+
|
|
222
|
+
report = Check.new(opts[:url]).call
|
|
223
|
+
puts opts[:json] ? JSON.pretty_generate(report) : format_check(report)
|
|
224
|
+
Check::USABLE_VERDICTS.include?(report[:verdict]) ? 0 : 1
|
|
225
|
+
end
|
|
226
|
+
private_class_method :run_check
|
|
227
|
+
|
|
228
|
+
def self.parse_check_options(argv)
|
|
229
|
+
opts = { proxy: {} }
|
|
230
|
+
OptionParser.new do |parser|
|
|
231
|
+
parser.on("--json") { opts[:json] = true }
|
|
232
|
+
ProxySettings.add_options(parser, opts[:proxy])
|
|
233
|
+
end.parse!(argv)
|
|
234
|
+
opts[:url] = argv.shift.to_s.strip
|
|
235
|
+
return opts if !opts[:url].empty? && argv.empty? && valid_check_url?(opts[:url])
|
|
236
|
+
|
|
237
|
+
warn USAGE
|
|
238
|
+
nil
|
|
239
|
+
end
|
|
240
|
+
private_class_method :parse_check_options
|
|
241
|
+
|
|
242
|
+
def self.valid_check_url?(url)
|
|
243
|
+
uri = URI.parse(url =~ %r{\Ahttps?://}i ? url : "https://#{url}")
|
|
244
|
+
uri.is_a?(URI::HTTP) && !uri.host.to_s.empty?
|
|
245
|
+
rescue URI::InvalidURIError
|
|
246
|
+
false
|
|
247
|
+
end
|
|
248
|
+
private_class_method :valid_check_url?
|
|
249
|
+
|
|
250
|
+
def self.format_check(report)
|
|
251
|
+
lines = ["#{report[:url]}: #{report[:verdict]}"]
|
|
252
|
+
lines << " platform: #{report[:platform]}" if report[:platform]
|
|
253
|
+
lines << " webmcp: #{report[:webmcp][:status]}#{" (#{report[:webmcp][:reason]})" if report[:webmcp][:reason]}"
|
|
254
|
+
lines << " next: #{report[:next_step]}"
|
|
255
|
+
lines.join("\n")
|
|
256
|
+
end
|
|
257
|
+
private_class_method :format_check
|
|
258
|
+
|
|
210
259
|
# --- compare ---
|
|
211
260
|
|
|
212
261
|
def self.run_compare(argv)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: portage-cli
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.10.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tom Whitbread
|
|
@@ -30,14 +30,14 @@ dependencies:
|
|
|
30
30
|
requirements:
|
|
31
31
|
- - "~>"
|
|
32
32
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: '0.
|
|
33
|
+
version: '0.11'
|
|
34
34
|
type: :runtime
|
|
35
35
|
prerelease: false
|
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
37
|
requirements:
|
|
38
38
|
- - "~>"
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
|
-
version: '0.
|
|
40
|
+
version: '0.11'
|
|
41
41
|
- !ruby/object:Gem::Dependency
|
|
42
42
|
name: portage-ucp-client
|
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -184,6 +184,8 @@ files:
|
|
|
184
184
|
- lib/portage/cli/buy.rb
|
|
185
185
|
- lib/portage/cli/buyer_context.rb
|
|
186
186
|
- lib/portage/cli/catalog_products.rb
|
|
187
|
+
- lib/portage/cli/check.rb
|
|
188
|
+
- lib/portage/cli/check_next_step.rb
|
|
187
189
|
- lib/portage/cli/checkout_handoff.rb
|
|
188
190
|
- lib/portage/cli/classifier.rb
|
|
189
191
|
- lib/portage/cli/compare.rb
|
|
@@ -197,6 +199,7 @@ files:
|
|
|
197
199
|
- lib/portage/cli/generate/adapter.rb
|
|
198
200
|
- lib/portage/cli/generate/agent_profile.rb
|
|
199
201
|
- lib/portage/cli/handoff_agents.rb
|
|
202
|
+
- lib/portage/cli/handoff_host.rb
|
|
200
203
|
- lib/portage/cli/handoff_only.rb
|
|
201
204
|
- lib/portage/cli/handoff_reconciler.rb
|
|
202
205
|
- lib/portage/cli/handoff_reconciler/wire_adapters.rb
|