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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 58834de7ed60ddb98da33896147737f518ccb8d02d58f0d3aeceb645ecef8ea6
|
|
4
|
+
data.tar.gz: 24e471dfb23e48b8ff1f157dbac941442ca84571120ad754451dfb36d9c08d60
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 70c1a8b9db25d281091e496db0eb7e45de1e54b0afbc943a35866cc9ec2efb4a292b043a0720932ff5f3c2a93757ec3b217785d96ec4f2ece6e637132aed1d04
|
|
7
|
+
data.tar.gz: af42b5fb85cc582d86e60d616f38f177df92b6c53df59a7ccedcd8780855adb15f876e32ddbfda384bd579825ed90f68c5a833fd061ed79fce43e3a2c056edcd
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,148 @@ All notable changes to this project are documented here. Format loosely follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.0.0/); this project is
|
|
5
5
|
pre-1.0, so APIs may still shift between minor versions.
|
|
6
6
|
|
|
7
|
+
## [0.7.4] - 2026-09-25
|
|
8
|
+
|
|
9
|
+
- **`portage doctor` reports how it was installed, and warns when another
|
|
10
|
+
`portage` shadows it** (`docs/plans/homebrew-distribution.md` Phase 4).
|
|
11
|
+
New findings, all offline and without shelling out to `brew`:
|
|
12
|
+
- `install`: `homebrew` (with the Cellar keg) when this gem or its Ruby
|
|
13
|
+
lives under `HOMEBREW_PREFIX/Cellar/portage/` (`$HOMEBREW_PREFIX`,
|
|
14
|
+
then `/opt/homebrew`, `/usr/local`, `/home/linuxbrew/.linuxbrew`),
|
|
15
|
+
otherwise `gem` with the gem's own path;
|
|
16
|
+
- `runtime`: the Ruby version and `RbConfig.ruby` path, plus the
|
|
17
|
+
`portage-cli` version;
|
|
18
|
+
- `adapters`: each first-party adapter gem (plus `webmcp` and
|
|
19
|
+
`decision`), whether it loads and at which version. A gem install
|
|
20
|
+
without adapters is normal; a Homebrew install missing one is a
|
|
21
|
+
warning, since the formula bundles them all. A broken adapter is
|
|
22
|
+
reported, never raised;
|
|
23
|
+
- `path`: every `portage` on `PATH`, compared by resolved Cellar
|
|
24
|
+
location rather than raw path. Warns when a Homebrew install is
|
|
25
|
+
shadowed by an earlier `portage` (typically a `gem install` copy in a
|
|
26
|
+
mise/rbenv/asdf/rvm Ruby), naming the winner and how to fix it, and
|
|
27
|
+
when a gem install is shadowed by a Homebrew one.
|
|
28
|
+
- `portage doctor` also warns when the `PORTAGE_SHIP_*` address is missing
|
|
29
|
+
or incomplete, naming the missing variables. Without
|
|
30
|
+
`PORTAGE_SHIP_COUNTRY` a native UCP store gets no buyer context, which
|
|
31
|
+
is how a live Shopify store ended up reporting in-stock items as out of
|
|
32
|
+
stock.
|
|
33
|
+
- `doctor` findings now carry a `level` (`warning` or `info`) and, for the
|
|
34
|
+
new checks, structured `details`, both in `--json`. The JSON is still a
|
|
35
|
+
top-level array. Only warnings make doctor exit 1; the text output lists
|
|
36
|
+
info findings first, then the warnings or `No issues found.`.
|
|
37
|
+
- `ShippingProfile` treats an empty `PORTAGE_SHIP_*` value as unset, the
|
|
38
|
+
way `BuyerContext` and the WooCommerce billing fallback already did, so
|
|
39
|
+
a `.env` copied from `.env.example` with blanks left in no longer
|
|
40
|
+
submits an address of empty strings.
|
|
41
|
+
|
|
42
|
+
## [0.7.3] - 2026-09-25
|
|
43
|
+
|
|
44
|
+
- **Proxy support** (`docs/plans/proxy-support.md` Phases 2-3). Every
|
|
45
|
+
network command (`buy`, `find`, `compare`, `doctor`, `payment enroll`)
|
|
46
|
+
takes `--proxy`, `--proxy-mode`, `--proxy-header`, `--no-proxy`,
|
|
47
|
+
`--proxy-route`, `--proxy-chain`, `--proxy-passthrough`, `--proxy-ca` and
|
|
48
|
+
`--no-env-proxy`, resolved per field against `PORTAGE_PROXY*` env vars
|
|
49
|
+
and `~/.portage/config.json`'s `proxy` section into core's
|
|
50
|
+
`Support::ProxyConfig`. `proxy.password_ref` resolves through Keychain or
|
|
51
|
+
Secret Service (service `portage-cli-proxy`). The payment route stays
|
|
52
|
+
direct unless a proxy is named for it explicitly, so payment traffic no
|
|
53
|
+
longer inherits a bare `$http_proxy`/`$https_proxy`. `portage doctor`
|
|
54
|
+
probes each configured proxy through `Support::Connection` and warns on
|
|
55
|
+
plaintext credentials in `config.json`.
|
|
56
|
+
- Requires `portage-ucp` `~> 0.10` (for `Support::Connection` and
|
|
57
|
+
`ProxyConfig`) and `portage-ucp-client` `>= 0.6.3` (for
|
|
58
|
+
`Client::USER_AGENT`, which the default User-Agent is built from). Both
|
|
59
|
+
floors were too low before: against `portage-ucp` 0.9.0 or
|
|
60
|
+
`portage-ucp-client` 0.6.2, `require "portage/cli"` raised `NameError`.
|
|
61
|
+
- `portage --version` (also `-v` and `portage version`) prints the gem's
|
|
62
|
+
version and exits 0. Needed so a packaged install — the Homebrew
|
|
63
|
+
formula's offline `test do` block, or anyone else scripting around a
|
|
64
|
+
release — can confirm which build is on `PATH` without hitting the
|
|
65
|
+
network.
|
|
66
|
+
- **`portage-console` failed to start on Ruby 4.0.** It requires `irb`,
|
|
67
|
+
which stopped being a default gem in Ruby 4.0 (it's a bundled gem now),
|
|
68
|
+
and the gemspec never declared it, so any isolated install
|
|
69
|
+
(Homebrew's, or anything under Bundler) raised `LoadError: cannot load
|
|
70
|
+
such file -- irb`. `irb` is now a runtime dependency.
|
|
71
|
+
- **Did the shopper finish the checkout? `portage orders reconcile`.**
|
|
72
|
+
Every checkout `portage buy` hands to the shopper's own browser
|
|
73
|
+
(`requires_escalation`, `permission_denied`, `no_payment_token`,
|
|
74
|
+
`policy_blocked`, `low_confidence`, `checkout_mismatch`) now reserves a
|
|
75
|
+
pending record in the transaction log (best-effort — a failed write is a
|
|
76
|
+
report warning, never blocks the hand-off). `portage orders reconcile
|
|
77
|
+
[--checkout ID] [--json]` re-fetches each pending checkout from the store
|
|
78
|
+
and settles it: `complete` only on a store-reported `completed` status,
|
|
79
|
+
`failed` on `canceled` or an unanswered expiry, otherwise stays pending.
|
|
80
|
+
Never infers success from a vanished checkout or a plain timeout. Safe to
|
|
81
|
+
run from cron/launchd. See `docs/plans/handoff-reconcile.md`.
|
|
82
|
+
- **`handoff_spend_mode`** (`PORTAGE_HANDOFF_SPEND_MODE`, config.json) —
|
|
83
|
+
whether a reconciled shopper purchase counts toward the buyer's own spend
|
|
84
|
+
cap/velocity limit. `block` (default): counts like any agent purchase.
|
|
85
|
+
`warn`: recorded but excluded from cap math. `precheck`: `block`, plus a
|
|
86
|
+
spend-cap check at hand-off time that suppresses auto-open (never the
|
|
87
|
+
URL) when this checkout would already exceed the cap.
|
|
88
|
+
- Phase 0 of the plan above (a live signal check against a real store) was
|
|
89
|
+
not run before this shipped — see `docs/design-log.md` §44.
|
|
90
|
+
- **`portage buy --wait [--wait-timeout DURATION|off]`.** After a hand-off,
|
|
91
|
+
polls `HandoffReconciler` with backoff (2s → 30s, plus jitter) until the
|
|
92
|
+
checkout settles or its deadline passes — the earlier of
|
|
93
|
+
`handoff_wait_timeout` (`PORTAGE_HANDOFF_WAIT_TIMEOUT`, config.json;
|
|
94
|
+
default 30m, `off` removes it) and the checkout's own `expires_at`.
|
|
95
|
+
Ctrl-C or the deadline leaves the record pending for a later `portage
|
|
96
|
+
orders reconcile`; it never settles from the wait itself. Under `--wait
|
|
97
|
+
--json`, stdout streams NDJSON (`handoff`, `handoff_status` on each
|
|
98
|
+
store-reported status change, `handoff_settled`) followed by the final
|
|
99
|
+
report object; plain `--json` with no `--wait` is unchanged byte-for-byte.
|
|
100
|
+
- **`reconcile_notify`** (`PORTAGE_RECONCILE_NOTIFY`, config.json) — a comma
|
|
101
|
+
list of channels a settled hand-off notifies on, from `--wait` or `portage
|
|
102
|
+
orders reconcile`. Default `webhook`. Adds `macos` (a native notification,
|
|
103
|
+
merchant/amount escaped into a fixed AppleScript template) and `terminal`
|
|
104
|
+
(a printed line, forced on for a plain-text `--wait` regardless of
|
|
105
|
+
configuration). `journal` is accepted for documentation — the Phase 1
|
|
106
|
+
order-snapshot journal write was already unconditional.
|
|
107
|
+
- **WebMCP outbound, opt-in (Phase 4, partial).** `Buy.new(webmcp_bridge:)`
|
|
108
|
+
takes a `portage-ucp-webmcp` outbound bridge already pointed at a
|
|
109
|
+
navigated page; when given one, `portage-cli` tries it after native-UCP
|
|
110
|
+
discovery finds nothing and before a platform-adapter fallback. Only
|
|
111
|
+
`webmcp_checkout_mode: express_stop` (the default) is implemented: it
|
|
112
|
+
builds the cart/checkout and always hands off (reason `express_stop`),
|
|
113
|
+
feeding Phases 1-3 unchanged, since a WebMCP page doesn't expose
|
|
114
|
+
`complete_checkout` by default anyway. `token` mode reports
|
|
115
|
+
`webmcp_token_unsupported` rather than attempting completion — it still
|
|
116
|
+
needs a `Confirmer`-swap seam in `Mcp::Server.build` and payment-token
|
|
117
|
+
enrollment that don't exist yet. No new hard runtime dependency:
|
|
118
|
+
`portage-ucp-webmcp` is lazily `require`d, same posture as an optional
|
|
119
|
+
platform adapter gem.
|
|
120
|
+
|
|
121
|
+
## [0.7.2] - 2026-09-24
|
|
122
|
+
|
|
123
|
+
- The User-Agent every store-facing request sends (`Cli::UserAgent`, née
|
|
124
|
+
`Cli::USER_AGENT`/`Cli::HTTP_HEADERS`) is now configurable: `PORTAGE_USER_AGENT`
|
|
125
|
+
beats `~/.portage/config.json`'s `user_agent` key, both of which beat the
|
|
126
|
+
`portage-cli/<ver> portage-ucp-client/<ver> (+https://github.com/...)`
|
|
127
|
+
default — same precedence `Notifier`'s webhook URL already uses. `portage
|
|
128
|
+
doctor` (and its `configure`/`setup` aliases) now flags a configured value
|
|
129
|
+
containing a stray newline, since `Net::HTTP` would otherwise raise on it
|
|
130
|
+
mid-checkout instead of at setup time.
|
|
131
|
+
|
|
132
|
+
## [0.7.1] - 2026-09-24
|
|
133
|
+
|
|
134
|
+
- Fix: `buy <url> --max-price` (and `--product-id` with `--max-price`) was
|
|
135
|
+
silently ignored — `add_search_options` only wired `--max-price` into the
|
|
136
|
+
`find` options, not `buy`, so a URL-driven buy could complete over the cap
|
|
137
|
+
the caller set. `Buy#select_product` now filters to products within
|
|
138
|
+
`max_price` first, using a variant's own price where available and
|
|
139
|
+
otherwise the product's lowest price (unpriced products stay eligible,
|
|
140
|
+
same rule `Find` already used). `no_match_message` now names the cap.
|
|
141
|
+
- Every store-facing request (`discover()` calls and the notifier webhook
|
|
142
|
+
POST) now sends `portage-cli/<ver> portage-ucp-client/<ver>
|
|
143
|
+
(+https://github.com/tomtom87/Portage)` instead of Ruby's or Faraday's
|
|
144
|
+
default User-Agent. New `Cli::USER_AGENT`/`Cli::HTTP_HEADERS`
|
|
145
|
+
(`lib/portage/cli/user_agent.rb`) replace the ad-hoc UA strings each of
|
|
146
|
+
`portage-buy`, `portage-find` and `portage-payment-enroll` built on their
|
|
147
|
+
own. Requires `portage-ucp-client >= 0.6.3`.
|
|
148
|
+
|
|
7
149
|
## [0.7.0] - 2026-09-23
|
|
8
150
|
|
|
9
151
|
- **The decision layer is wired in.** `buy`/`find` make their judgment
|
data/README.md
CHANGED
|
@@ -49,27 +49,83 @@ No single adapter gem is a hard dependency — install whichever
|
|
|
49
49
|
|
|
50
50
|
## Installation
|
|
51
51
|
|
|
52
|
+
**Homebrew** (macOS and Linux) — recommended for using the CLI:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
brew install tomtom87/portage/portage
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The formula installs `portage-cli` plus every adapter gem (Shopify, Wix,
|
|
59
|
+
WooCommerce, BigCommerce, Magento, Etsy, Instagram, WebMCP, Decision) into
|
|
60
|
+
its own directory, running on Homebrew's own `ruby`, so it doesn't depend on
|
|
61
|
+
or change whichever Ruby you use for anything else. It gives you `portage`
|
|
62
|
+
and `portage-console`.
|
|
63
|
+
|
|
64
|
+
**RubyGems** — on any Ruby ≥ 3.2, or when you only want some adapters:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
gem install portage-cli
|
|
68
|
+
gem install portage-ucp-shopify # optional: add only the adapters you need
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
In an app's `Gemfile` instead:
|
|
72
|
+
|
|
52
73
|
```ruby
|
|
53
|
-
# Gemfile
|
|
54
74
|
gem "portage-cli"
|
|
55
75
|
```
|
|
56
76
|
|
|
77
|
+
### Upgrading
|
|
78
|
+
|
|
79
|
+
- Homebrew: `brew upgrade portage`. Your config and data in `~/.portage`
|
|
80
|
+
(policy, payment-method metadata, transaction log, order ledger,
|
|
81
|
+
`config.json`) are left alone, as they are by `brew uninstall`.
|
|
82
|
+
- RubyGems: `gem update portage-cli` (and any adapter gems you added).
|
|
83
|
+
|
|
84
|
+
Portage has no self-update command, by design: whichever tool installed it
|
|
85
|
+
owns upgrades.
|
|
86
|
+
|
|
87
|
+
### Linux: stored secrets need `secret-tool`
|
|
88
|
+
|
|
89
|
+
On Linux, stored payment tokens (`portage payment enroll`) and proxy
|
|
90
|
+
passwords (`password_ref`) live in the Secret Service (GNOME Keyring,
|
|
91
|
+
KWallet) via the `secret-tool` command. It comes from your distribution,
|
|
92
|
+
not from the formula or the gem:
|
|
93
|
+
|
|
57
94
|
```bash
|
|
58
|
-
|
|
95
|
+
sudo apt install libsecret-tools # Debian/Ubuntu
|
|
96
|
+
sudo dnf install libsecret # Fedora
|
|
59
97
|
```
|
|
60
98
|
|
|
61
|
-
|
|
99
|
+
Without it (or without a live D-Bus session, e.g. over SSH or in CI),
|
|
100
|
+
Portage uses the headless tier: the token comes from
|
|
101
|
+
`PORTAGE_PAYMENT_TOKEN` and nothing is stored locally. On macOS the
|
|
102
|
+
Keychain is used and nothing extra is needed.
|
|
103
|
+
|
|
104
|
+
### Two copies on PATH
|
|
105
|
+
|
|
106
|
+
A `gem install` copy and a Homebrew copy can both be installed, and your
|
|
107
|
+
shell runs whichever `portage` comes first on `PATH`. A common case is an
|
|
108
|
+
old gem copy in a mise, rbenv, asdf or rvm Ruby's `bin`, which then keeps
|
|
109
|
+
running after `brew install` or `brew upgrade`. `portage doctor` warns
|
|
110
|
+
about this. To check:
|
|
62
111
|
|
|
63
112
|
```bash
|
|
64
|
-
|
|
113
|
+
which -a portage
|
|
65
114
|
```
|
|
66
115
|
|
|
116
|
+
To fix it, keep one copy: `gem uninstall portage-cli` (with the Ruby that
|
|
117
|
+
owns the gem copy active) to use Homebrew's, or `brew uninstall portage` to
|
|
118
|
+
use the gem's. Or reorder `PATH` so the one you want comes first.
|
|
119
|
+
|
|
67
120
|
## Usage
|
|
68
121
|
|
|
122
|
+
<!-- usage-start -->
|
|
69
123
|
```bash
|
|
70
124
|
portage buy <url> --query "..." [--qty N] [--payment-token TOKEN] [--product-id ID]
|
|
71
|
-
[--yes] [--dry-run] [--
|
|
125
|
+
[--yes] [--dry-run] [--auto-open|--no-auto-open]
|
|
126
|
+
[--notify-webhook URL] [--decision-backend jev|laya]
|
|
72
127
|
[--min-confidence N] [--json]
|
|
128
|
+
[--wait [--wait-timeout DURATION|off]]
|
|
73
129
|
portage buy --query "..." [--store URL] [--max-price N] [--limit N] ...
|
|
74
130
|
portage find --query "..." [--max-price N] [--limit N] [--json]
|
|
75
131
|
portage compare <url> --product-id ID [--id VALUE ...] [--results N]
|
|
@@ -88,7 +144,24 @@ portage policy set [--per-transaction-cap N --currency CUR]
|
|
|
88
144
|
[--rolling-cap N --rolling-window-seconds N --currency CUR]
|
|
89
145
|
[--velocity-count N --velocity-window-seconds N]
|
|
90
146
|
[--allow HOST ...] [--clear-allowlist]
|
|
147
|
+
portage orders reconcile [--checkout ID] [--json]
|
|
148
|
+
portage doctor [--require FILE] [--adapter CLASS_NAME] [--json] # aliases: configure, setup
|
|
149
|
+
portage generate adapter NAME [--dir DIR]
|
|
150
|
+
portage generate agent-profile [--out FILE] [--key-out FILE] [--rotate]
|
|
151
|
+
portage --version
|
|
91
152
|
```
|
|
153
|
+
<!-- usage-end -->
|
|
154
|
+
|
|
155
|
+
`buy`/`find`/`compare`/`doctor`/`payment enroll` (the network-touching commands —
|
|
156
|
+
`orders reconcile` doesn't take these) also accept:
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
[--proxy URL] [--proxy-mode forward|gateway] [--proxy-header "Name: value"]
|
|
160
|
+
[--no-proxy HOSTS] [--proxy-route ROUTE=URL|direct] [--proxy-chain URL,URL,...]
|
|
161
|
+
[--proxy-passthrough HEADER] [--proxy-ca FILE] [--no-env-proxy]
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
See "Proxy" below.
|
|
92
165
|
|
|
93
166
|
- `--query` — search term. Against the store's catalog when you name a store,
|
|
94
167
|
against the search backends when you don't.
|
|
@@ -102,7 +175,10 @@ portage policy set [--per-transaction-cap N --currency CUR]
|
|
|
102
175
|
search ranks first. If the id isn't in the results, nothing is bought.
|
|
103
176
|
- `--store` — name the merchant without giving a full URL; skips the search.
|
|
104
177
|
- `--max-price` — in major units (`400` means 400), compared per offer in that
|
|
105
|
-
offer's own currency. No FX conversion.
|
|
178
|
+
offer's own currency. No FX conversion. Applies per unit, to the search and
|
|
179
|
+
to the store's own catalog once one is settled (a URL, `--store`, or a
|
|
180
|
+
picked offer): nothing priced above it is checked out, even with
|
|
181
|
+
`--product-id`. A product with no price is still eligible.
|
|
106
182
|
- `--limit` — how many candidate stores to probe, capped at 12.
|
|
107
183
|
- `--yes` — skip the confirmation prompt before completing checkout.
|
|
108
184
|
- `--dry-run` — resolve and price the order without completing checkout.
|
|
@@ -270,6 +346,166 @@ opt-in guardrail, not a default-deny one. Per-token scopes (merchant/amount
|
|
|
270
346
|
limits bound to one enrolled card) are set via `portage payment enroll
|
|
271
347
|
--scope-*` above, not here.
|
|
272
348
|
|
|
349
|
+
### Orders reconcile
|
|
350
|
+
|
|
351
|
+
Nearly every real checkout `portage buy` can't finish itself hands the
|
|
352
|
+
shopper a link — `requires_escalation`, `permission_denied`,
|
|
353
|
+
`no_payment_token`, `policy_blocked`, `low_confidence`, or
|
|
354
|
+
`checkout_mismatch`. That's a pending purchase this process knows nothing
|
|
355
|
+
more about until something asks the store. `portage orders reconcile`
|
|
356
|
+
re-fetches each pending hand-off from the store and settles it once the
|
|
357
|
+
store itself reports a terminal status:
|
|
358
|
+
|
|
359
|
+
```bash
|
|
360
|
+
portage orders reconcile # every pending hand-off
|
|
361
|
+
portage orders reconcile --checkout chk_123
|
|
362
|
+
portage orders reconcile --json
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
A `completed` checkout settles `complete` — using the store's own total at
|
|
366
|
+
settle time, not the hand-off-time snapshot — records the order (when one
|
|
367
|
+
exists) and a journal entry, and is counted toward your spend policy's
|
|
368
|
+
rolling cap/velocity per `handoff_spend_mode` below. A `canceled` checkout,
|
|
369
|
+
or one that expires with no answer, settles `failed`. Anything still
|
|
370
|
+
in-progress, or not currently reachable, is left pending for the next run —
|
|
371
|
+
safe to put on a cron/launchd schedule; two overlapping runs never
|
|
372
|
+
double-settle the same record.
|
|
373
|
+
|
|
374
|
+
`handoff_spend_mode` (`PORTAGE_HANDOFF_SPEND_MODE`, config.json's
|
|
375
|
+
`handoff_spend_mode`) controls whether a reconciled shopper purchase counts
|
|
376
|
+
toward the caps `portage policy set` configures:
|
|
377
|
+
|
|
378
|
+
- `block` (default) — counts like any agent-completed purchase.
|
|
379
|
+
- `warn` — recorded, but excluded from the cap/velocity math; still notifies
|
|
380
|
+
when it would have pushed spend over the cap.
|
|
381
|
+
- `precheck` — `block`, plus a spend-cap check at hand-off time
|
|
382
|
+
(`portage buy`, not reconcile): if this checkout's total would already
|
|
383
|
+
exceed your cap, the URL is still printed but auto-open is suppressed.
|
|
384
|
+
|
|
385
|
+
`--wait [--wait-timeout DURATION|off]` on `portage buy` polls the same
|
|
386
|
+
reconciler right after a hand-off, instead of waiting for a separate
|
|
387
|
+
`orders reconcile` run. It backs off (2s → 30s, plus jitter) until the
|
|
388
|
+
checkout settles or its deadline passes — the earlier of
|
|
389
|
+
`handoff_wait_timeout` (`PORTAGE_HANDOFF_WAIT_TIMEOUT`, config.json; default
|
|
390
|
+
`30m`, `off` removes it) and the checkout's own `expires_at`. Ctrl-C, or
|
|
391
|
+
the deadline, leaves the record pending for a later `portage orders
|
|
392
|
+
reconcile` — it never settles from the wait itself. Under `--wait --json`,
|
|
393
|
+
stdout streams NDJSON — `handoff`, `handoff_status` on each store-reported
|
|
394
|
+
status change, `handoff_settled` — followed by the final report object;
|
|
395
|
+
plain `--json` without `--wait` is unchanged.
|
|
396
|
+
|
|
397
|
+
`reconcile_notify` (`PORTAGE_RECONCILE_NOTIFY`, config.json's
|
|
398
|
+
`reconcile_notify`) is a comma list of channels a settled hand-off notifies
|
|
399
|
+
on, from `--wait` or `orders reconcile`: `webhook` (default), `macos` (a
|
|
400
|
+
native notification), `terminal` (a printed line — forced on for a
|
|
401
|
+
plain-text `--wait` regardless of configuration), and `journal` (the order
|
|
402
|
+
snapshot journal write, already unconditional — listing it just documents
|
|
403
|
+
that).
|
|
404
|
+
|
|
405
|
+
### Doctor
|
|
406
|
+
|
|
407
|
+
```bash
|
|
408
|
+
portage doctor # aliases: portage configure, portage setup
|
|
409
|
+
portage doctor --json
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
Checks this machine's setup without touching the network (apart from
|
|
413
|
+
probing any proxy you've configured). It first reports how Portage is
|
|
414
|
+
installed, then lists anything that needs fixing:
|
|
415
|
+
|
|
416
|
+
- `install`: `homebrew` (with the Cellar path) or `gem` (with the gem's
|
|
417
|
+
path).
|
|
418
|
+
- `runtime`: the Ruby version and path it runs on, and the `portage-cli`
|
|
419
|
+
version.
|
|
420
|
+
- `adapters`: which first-party adapter gems load, and at which version.
|
|
421
|
+
Missing adapters are expected on a gem install; on Homebrew, which
|
|
422
|
+
bundles them all, a missing one is a warning.
|
|
423
|
+
- `path`: which `portage` your shell actually runs. Warns when another copy
|
|
424
|
+
earlier on `PATH` shadows this one (see "Two copies on PATH" above).
|
|
425
|
+
- `shipping`: warns when `PORTAGE_SHIP_*` is missing or incomplete (see
|
|
426
|
+
"Shipping address" below), naming the variables to set.
|
|
427
|
+
- Seller-side checks against `Portage::Ucp.configuration` (authenticator,
|
|
428
|
+
rate limiter, signing keys, payment handlers; pass `--require` to load
|
|
429
|
+
your app's initializer first), the confidence gate's backend, the
|
|
430
|
+
User-Agent, and proxy settings.
|
|
431
|
+
|
|
432
|
+
With `--json` the output is an array of findings, each with `check`,
|
|
433
|
+
`message`, `level` (`warning` or `info`) and, for the install checks,
|
|
434
|
+
`details`. Doctor exits `1` when there's at least one warning, else `0`.
|
|
435
|
+
|
|
436
|
+
### Proxy
|
|
437
|
+
|
|
438
|
+
`buy`, `find`, `compare`, `doctor`, and `payment enroll` accept the flags below,
|
|
439
|
+
resolved (per field, flag > env > `~/.portage/config.json`) into a
|
|
440
|
+
`Portage::Ucp::Support::ProxyConfig` by `Portage::Cli::ProxySettings` — see
|
|
441
|
+
[`docs/proxy.md`](../docs/proxy.md) for corporate egress, a rotating residential
|
|
442
|
+
pool, an API gateway, mitmproxy for debugging, and nginx/Cloudflare in front of the
|
|
443
|
+
MCP/WebMCP endpoints, worked through end to end.
|
|
444
|
+
|
|
445
|
+
| Flag | Meaning |
|
|
446
|
+
| --- | --- |
|
|
447
|
+
| `--proxy URL` | The default proxy's URL (`http://user:pass@host:port`). Overrides only `proxy.default.url`; every other configured field (`no_proxy`, `routes`, `chains`, ...) stays as set in config.json. |
|
|
448
|
+
| `--proxy-mode forward\|gateway` | The default profile's mode. `forward` (the default) is a standard HTTP proxy; `gateway` is a URL-rewriting gateway — see `docs/proxy.md`. |
|
|
449
|
+
| `--proxy-header "Name: value"` | Repeatable. Sent only to the proxy (on the `CONNECT` request, or the gateway request) — never to the real target. `Authorization`/`User-Agent`/`X-Shopify-*-Access-Token`/`X-Payment-Token` are refused here, always. |
|
|
450
|
+
| `--no-proxy HOSTS` | Comma-separated hostnames/suffixes/CIDRs (or a bare `*`) to always bypass the proxy for, whatever the route resolves to. |
|
|
451
|
+
| `--proxy-route ROUTE=URL\|direct` | Repeatable. Points one fixed route (`store`, `search`, `notify`, `payment`, `platform`, `probe`) at its own URL, or forces it `direct` regardless of `default`. |
|
|
452
|
+
| `--proxy-chain URL,URL,...` | An ad-hoc multi-hop chain for this run — each hop is `forward` unless prefixed `gateway+https://...`. Overrides the whole `default` profile (not just its `url`), since a chain has no single "url" field to merge with config.json's own. |
|
|
453
|
+
| `--proxy-passthrough HEADER` | Repeatable, server commands only. Allowlists an inbound request header to ride along on outbound calls made while serving it — see `docs/proxy.md`'s passthrough section. Refuses a protected header name at parse time. |
|
|
454
|
+
| `--proxy-ca FILE` | A PEM file trusted in addition to the system store — for a TLS-intercepting corporate proxy or a debugging proxy like mitmproxy. |
|
|
455
|
+
| `--no-env-proxy` | Ignore `HTTPS_PROXY`/`HTTP_PROXY`/`NO_PROXY` (and their lowercase forms) entirely for this run — otherwise they're still the fallback for any route nothing else configured. |
|
|
456
|
+
|
|
457
|
+
**Env vars** (checked when the matching flag is absent, before config.json):
|
|
458
|
+
|
|
459
|
+
| Var | Matches |
|
|
460
|
+
| --- | --- |
|
|
461
|
+
| `PORTAGE_PROXY` | `--proxy` |
|
|
462
|
+
| `PORTAGE_PROXY_MODE` | `--proxy-mode` |
|
|
463
|
+
| `PORTAGE_NO_PROXY` | `--no-proxy` |
|
|
464
|
+
| `PORTAGE_PROXY_CA` | `--proxy-ca` |
|
|
465
|
+
| `PORTAGE_PROXY_HEADERS` | A JSON object of header name → value, merged under any `--proxy-header` flags (flags win on a name collision). |
|
|
466
|
+
|
|
467
|
+
Below all of the above, the standard `HTTPS_PROXY`/`HTTP_PROXY`/`NO_PROXY` (and
|
|
468
|
+
lowercase) variables are still the fallback for any route nothing here configures
|
|
469
|
+
at all — existing environments keep working unless `--no-env-proxy` is set. The
|
|
470
|
+
**`payment` route is always forced `direct`** unless a proxy is named for it
|
|
471
|
+
explicitly (`--proxy-route payment=...` or config.json's `routes.payment`) — it
|
|
472
|
+
never inherits a bare `default`/env proxy the way every other route does, so an
|
|
473
|
+
egress proxy nobody meant to hand payment tokens to never sees them by accident.
|
|
474
|
+
|
|
475
|
+
`${ENV_VAR}` inside a config.json header value (`proxy_headers`, `forward_headers.add`)
|
|
476
|
+
is expanded from the process environment at resolve time, so a secret can live in
|
|
477
|
+
the environment rather than the file itself. A proxy password can also live in
|
|
478
|
+
`password_ref` (resolved through the same macOS Keychain/Linux Secret Service tiers
|
|
479
|
+
`portage payment` uses, under its own `portage-cli-proxy` service name) instead of
|
|
480
|
+
plaintext in the URL — `portage doctor` warns when it finds a plaintext one anyway.
|
|
481
|
+
|
|
482
|
+
`portage doctor` reports, per route, what's effectively configured (credentials
|
|
483
|
+
redacted), whether each configured proxy/gateway is actually reachable, and flags
|
|
484
|
+
plaintext proxy credentials and an intercepting proxy (`ca_file` or `gateway` mode)
|
|
485
|
+
sitting on the `payment` route.
|
|
486
|
+
|
|
487
|
+
### WebMCP (library use, opt-in)
|
|
488
|
+
|
|
489
|
+
`portage buy` from the shell has no browser of its own, so there's no CLI
|
|
490
|
+
flag for this — it's for a caller embedding `Portage::Cli::Buy` directly
|
|
491
|
+
alongside its own browser automation:
|
|
492
|
+
|
|
493
|
+
```ruby
|
|
494
|
+
Portage::Cli::Buy.new(url: "shop.example", query: "mug",
|
|
495
|
+
webmcp_bridge: my_portage_ucp_webmcp_bridge).call
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
Given a `portage-ucp-webmcp` outbound bridge already pointed at a navigated
|
|
499
|
+
page, `Buy` tries it after native-UCP discovery finds nothing at that URL
|
|
500
|
+
and before falling back to a platform adapter. `webmcp_checkout_mode`
|
|
501
|
+
(`PORTAGE_WEBMCP_CHECKOUT_MODE`, config.json's `webmcp_checkout_mode`)
|
|
502
|
+
controls how it finishes: `express_stop` (default) builds the cart/checkout
|
|
503
|
+
and always hands off — reason `express_stop`, so `--wait`/`orders
|
|
504
|
+
reconcile`/`handoff_spend_mode` all apply exactly as they do to any other
|
|
505
|
+
hand-off. `token` isn't implemented yet; it reports
|
|
506
|
+
`webmcp_token_unsupported` rather than attempting completion. Requires
|
|
507
|
+
`gem install portage-ucp-webmcp` — not a hard dependency of `portage-cli`.
|
|
508
|
+
|
|
273
509
|
### Decisions
|
|
274
510
|
|
|
275
511
|
`portage buy` and `portage find` make their judgment calls
|
|
@@ -366,12 +602,11 @@ gives up after 5 seconds and reports `notify_error` instead.
|
|
|
366
602
|
`portage find`'s offer order comes from `Support::OfferRanking`: buyable
|
|
367
603
|
first, then cheapest, then unpriced.
|
|
368
604
|
|
|
369
|
-
### Shipping address
|
|
605
|
+
### Shipping address
|
|
370
606
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
`
|
|
374
|
-
rather than a flag, same posture as adapter credentials:
|
|
607
|
+
Set your shipping address via env rather than a flag, the same way as
|
|
608
|
+
adapter credentials (`.env.example` at the repo root lists them all).
|
|
609
|
+
`portage doctor` warns until the required ones are set:
|
|
375
610
|
|
|
376
611
|
```bash
|
|
377
612
|
export PORTAGE_SHIP_STREET="1 Main St"
|
|
@@ -384,12 +619,26 @@ export PORTAGE_SHIP_LAST_NAME="Lovelace" # optional
|
|
|
384
619
|
export PORTAGE_SHIP_PHONE="+1..." # optional
|
|
385
620
|
```
|
|
386
621
|
|
|
387
|
-
`street`/`city`/`country`/`postal_code` are required — a partial profile
|
|
388
|
-
treated as no profile at all.
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
622
|
+
`street`/`city`/`country`/`postal_code` are required — a partial profile
|
|
623
|
+
(or one with empty values) is treated as no profile at all.
|
|
624
|
+
|
|
625
|
+
The variables are used in two ways:
|
|
626
|
+
|
|
627
|
+
- **Native UCP stores** (`buy` and `find` over HTTP) get
|
|
628
|
+
`PORTAGE_SHIP_COUNTRY`, `_REGION` and `_POSTAL_CODE` (plus
|
|
629
|
+
`PORTAGE_CURRENCY` and `PORTAGE_LANGUAGE`, if set) as UCP buyer context,
|
|
630
|
+
which a store uses to pick the market it prices and stocks in. These
|
|
631
|
+
work on their own, without the full address.
|
|
632
|
+
Without at least the country, a live Shopify store can report in-stock
|
|
633
|
+
items as out of stock.
|
|
634
|
+
- **Your own store** (`portage buy`'s adapter-credentials fallback,
|
|
635
|
+
described at the top of this file), when its adapter supports
|
|
636
|
+
`dev.ucp.shopping.fulfillment`, also gets the full address as the
|
|
637
|
+
checkout's shipping destination. Once the merchant prices shipping
|
|
638
|
+
options against it, `portage buy` auto-picks the cheapest per fulfillment
|
|
639
|
+
group; there's no interactive rate picker, since this drives one
|
|
640
|
+
automated purchase. Native (non-adapter) UCP stores don't get the full
|
|
641
|
+
address yet — see `portage-ucp`'s design log for why.
|
|
393
642
|
|
|
394
643
|
## Buying without a URL
|
|
395
644
|
|