portage-cli 0.7.3 → 0.7.5
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 +56 -0
- data/README.md +170 -24
- data/exe/portage +5 -0
- data/exe/portage-console +3 -0
- data/lib/portage/cli/doctor.rb +82 -6
- data/lib/portage/cli/dot_env.rb +62 -0
- data/lib/portage/cli/install_doctor.rb +239 -0
- data/lib/portage/cli/shipping_profile.rb +7 -2
- data/lib/portage/cli/version.rb +1 -1
- data/lib/portage/cli.rb +16 -6
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a682278ec8b34cb6e2cd649f345cb6779a7cec5d0bc22e6950721f7996a2d877
|
|
4
|
+
data.tar.gz: ed490b87284e6a8c9bd9ab280ea600d16877820240386d34e239309a8204b16d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c22fd29667f7281c0173503cc9797158de1cd9d494cfcc98795bbce3ecfbaa03ed67ecb1321b3adf83000cf251168a62118db703bf1fb7b5417ba615a672b8c5
|
|
7
|
+
data.tar.gz: 42016dd1b716169b2dbaadef6f83a210ab8750584b23464e8c38c76aa39374988eeda97cfdc133c3edffaaf3c3ef8b34a76aa2fd57a9c96ad32292d529878df4
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,62 @@ 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.5] - 2026-09-25
|
|
8
|
+
|
|
9
|
+
- **`portage` loads `~/.portage/.env` on startup** (`Portage::Cli::DotEnv`),
|
|
10
|
+
so the shipping address, search keys and adapter credentials can live in
|
|
11
|
+
one file instead of a shell profile. `portage-console` does too. The
|
|
12
|
+
real environment always wins, empty values are skipped, and
|
|
13
|
+
`PORTAGE_ENV_FILE` names a different file. A `./.env` in the working
|
|
14
|
+
directory is never loaded automatically, so running `portage` inside a
|
|
15
|
+
cloned repo can't pick up that repo's proxy, webhook or credential
|
|
16
|
+
settings. Stdlib only, so the Homebrew formula gains no resource.
|
|
17
|
+
`portage doctor` reports the loaded file (`env_file`) and warns when
|
|
18
|
+
other users can read it.
|
|
19
|
+
- **`portage doctor` runs the seller-side checks only for a seller.** The
|
|
20
|
+
authenticator, rate limiter, signing keys and payment handlers checks
|
|
21
|
+
inspect `Portage::Ucp.configuration`, which in a bare `portage` process
|
|
22
|
+
is always the unconfigured default. So every fresh install got four
|
|
23
|
+
warnings that meant nothing to a shopper and made doctor exit 1. They
|
|
24
|
+
now run only with `--require` or `--adapter`; otherwise one info line
|
|
25
|
+
says they were skipped. `Doctor.new` keeps running them by default
|
|
26
|
+
(`seller: true`) for library callers.
|
|
27
|
+
|
|
28
|
+
## [0.7.4] - 2026-09-25
|
|
29
|
+
|
|
30
|
+
- **`portage doctor` reports how it was installed, and warns when another
|
|
31
|
+
`portage` shadows it** (`docs/plans/homebrew-distribution.md` Phase 4).
|
|
32
|
+
New findings, all offline and without shelling out to `brew`:
|
|
33
|
+
- `install`: `homebrew` (with the Cellar keg) when this gem or its Ruby
|
|
34
|
+
lives under `HOMEBREW_PREFIX/Cellar/portage/` (`$HOMEBREW_PREFIX`,
|
|
35
|
+
then `/opt/homebrew`, `/usr/local`, `/home/linuxbrew/.linuxbrew`),
|
|
36
|
+
otherwise `gem` with the gem's own path;
|
|
37
|
+
- `runtime`: the Ruby version and `RbConfig.ruby` path, plus the
|
|
38
|
+
`portage-cli` version;
|
|
39
|
+
- `adapters`: each first-party adapter gem (plus `webmcp` and
|
|
40
|
+
`decision`), whether it loads and at which version. A gem install
|
|
41
|
+
without adapters is normal; a Homebrew install missing one is a
|
|
42
|
+
warning, since the formula bundles them all. A broken adapter is
|
|
43
|
+
reported, never raised;
|
|
44
|
+
- `path`: every `portage` on `PATH`, compared by resolved Cellar
|
|
45
|
+
location rather than raw path. Warns when a Homebrew install is
|
|
46
|
+
shadowed by an earlier `portage` (typically a `gem install` copy in a
|
|
47
|
+
mise/rbenv/asdf/rvm Ruby), naming the winner and how to fix it, and
|
|
48
|
+
when a gem install is shadowed by a Homebrew one.
|
|
49
|
+
- `portage doctor` also warns when the `PORTAGE_SHIP_*` address is missing
|
|
50
|
+
or incomplete, naming the missing variables. Without
|
|
51
|
+
`PORTAGE_SHIP_COUNTRY` a native UCP store gets no buyer context, which
|
|
52
|
+
is how a live Shopify store ended up reporting in-stock items as out of
|
|
53
|
+
stock.
|
|
54
|
+
- `doctor` findings now carry a `level` (`warning` or `info`) and, for the
|
|
55
|
+
new checks, structured `details`, both in `--json`. The JSON is still a
|
|
56
|
+
top-level array. Only warnings make doctor exit 1; the text output lists
|
|
57
|
+
info findings first, then the warnings or `No issues found.`.
|
|
58
|
+
- `ShippingProfile` treats an empty `PORTAGE_SHIP_*` value as unset, the
|
|
59
|
+
way `BuyerContext` and the WooCommerce billing fallback already did, so
|
|
60
|
+
a `.env` copied from `.env.example` with blanks left in no longer
|
|
61
|
+
submits an address of empty strings.
|
|
62
|
+
|
|
7
63
|
## [0.7.3] - 2026-09-25
|
|
8
64
|
|
|
9
65
|
- **Proxy support** (`docs/plans/proxy-support.md` Phases 2-3). Every
|
data/README.md
CHANGED
|
@@ -49,26 +49,81 @@ 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]
|
|
73
128
|
[--wait [--wait-timeout DURATION|off]]
|
|
74
129
|
portage buy --query "..." [--store URL] [--max-price N] [--limit N] ...
|
|
@@ -90,7 +145,12 @@ portage policy set [--per-transaction-cap N --currency CUR]
|
|
|
90
145
|
[--velocity-count N --velocity-window-seconds N]
|
|
91
146
|
[--allow HOST ...] [--clear-allowlist]
|
|
92
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
|
|
93
152
|
```
|
|
153
|
+
<!-- usage-end -->
|
|
94
154
|
|
|
95
155
|
`buy`/`find`/`compare`/`doctor`/`payment enroll` (the network-touching commands —
|
|
96
156
|
`orders reconcile` doesn't take these) also accept:
|
|
@@ -342,6 +402,42 @@ plain-text `--wait` regardless of configuration), and `journal` (the order
|
|
|
342
402
|
snapshot journal write, already unconditional — listing it just documents
|
|
343
403
|
that).
|
|
344
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
|
+
- `env_file`: which env file was loaded (see "Environment file" below).
|
|
428
|
+
Warns when other users can read it.
|
|
429
|
+
- The confidence gate's backend, the User-Agent, and proxy settings.
|
|
430
|
+
- Seller-side checks against `Portage::Ucp.configuration` (authenticator,
|
|
431
|
+
rate limiter, signing keys, payment handlers). These only run when you
|
|
432
|
+
pass `--require` with your app's initializer (Rails:
|
|
433
|
+
`--require ./config/environment`) or `--adapter`. Without them doctor
|
|
434
|
+
would only ever see the unconfigured defaults, so it just notes that it
|
|
435
|
+
skipped them.
|
|
436
|
+
|
|
437
|
+
With `--json` the output is an array of findings, each with `check`,
|
|
438
|
+
`message`, `level` (`warning` or `info`) and, for the install checks,
|
|
439
|
+
`details`. Doctor exits `1` when there's at least one warning, else `0`.
|
|
440
|
+
|
|
345
441
|
### Proxy
|
|
346
442
|
|
|
347
443
|
`buy`, `find`, `compare`, `doctor`, and `payment enroll` accept the flags below,
|
|
@@ -511,30 +607,80 @@ gives up after 5 seconds and reports `notify_error` instead.
|
|
|
511
607
|
`portage find`'s offer order comes from `Support::OfferRanking`: buyable
|
|
512
608
|
first, then cheapest, then unpriced.
|
|
513
609
|
|
|
514
|
-
###
|
|
610
|
+
### Environment file
|
|
611
|
+
|
|
612
|
+
`portage` and `portage-console` load `~/.portage/.env` on startup, so your
|
|
613
|
+
shipping address, search keys and adapter credentials can live in one file
|
|
614
|
+
rather than your shell profile. `.env.example` at the repo root lists every
|
|
615
|
+
variable. Rules:
|
|
616
|
+
|
|
617
|
+
- Variables already set in your shell win over the file.
|
|
618
|
+
- Empty values are skipped, so blanks copied from `.env.example` set nothing.
|
|
619
|
+
- `KEY=value`, `export KEY=value`, `"double"` (with `\n` and `\"` escapes) and
|
|
620
|
+
`'single'` quotes all work; `#` starts a comment.
|
|
621
|
+
- `PORTAGE_ENV_FILE=path` loads a different file instead, for example
|
|
622
|
+
`PORTAGE_ENV_FILE=.env` for a project checkout's own.
|
|
623
|
+
|
|
624
|
+
Keep it private (`chmod 600 ~/.portage/.env`); `portage doctor` warns if
|
|
625
|
+
other users can read it.
|
|
626
|
+
|
|
627
|
+
#### Why `./.env` is never loaded automatically
|
|
628
|
+
|
|
629
|
+
Many tools load a `.env` from whatever directory you run them in. Portage
|
|
630
|
+
deliberately doesn't, because `portage` spends money and handles payment
|
|
631
|
+
tokens, and the directory you happen to be in isn't something you chose
|
|
632
|
+
to trust. If it did, running `portage` inside a cloned repo, a downloaded
|
|
633
|
+
project or a shared folder would silently apply that directory's
|
|
634
|
+
settings, for example:
|
|
635
|
+
|
|
636
|
+
- `PORTAGE_PROXY` plus `PORTAGE_PROXY_CA`, routing your store traffic
|
|
637
|
+
through someone else's intercepting proxy, where they can read it;
|
|
638
|
+
- a notify webhook that sends your checkout URLs and order details to
|
|
639
|
+
someone else;
|
|
640
|
+
- store credentials or `PORTAGE_STORES`, pointing purchases at a different
|
|
641
|
+
store than you think.
|
|
642
|
+
|
|
643
|
+
So only `~/.portage/.env`, a file you created in your own Portage
|
|
644
|
+
directory, loads automatically. To use a project's `.env`, name it on
|
|
645
|
+
purpose: `PORTAGE_ENV_FILE=.env portage ...`, after reading what's in it.
|
|
646
|
+
|
|
647
|
+
### Shipping address
|
|
515
648
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
rather than a flag, same posture as adapter credentials:
|
|
649
|
+
Set your shipping address in `~/.portage/.env` (or your shell) rather than
|
|
650
|
+
a flag, the same way as adapter credentials. `portage doctor` warns until
|
|
651
|
+
the required ones are set:
|
|
520
652
|
|
|
521
653
|
```bash
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
654
|
+
PORTAGE_SHIP_STREET="1 Main St"
|
|
655
|
+
PORTAGE_SHIP_CITY="Erie"
|
|
656
|
+
PORTAGE_SHIP_REGION="PA" # optional
|
|
657
|
+
PORTAGE_SHIP_COUNTRY="US"
|
|
658
|
+
PORTAGE_SHIP_POSTAL_CODE="16501"
|
|
659
|
+
PORTAGE_SHIP_FIRST_NAME="Ada" # optional
|
|
660
|
+
PORTAGE_SHIP_LAST_NAME="Lovelace" # optional
|
|
661
|
+
PORTAGE_SHIP_PHONE="+1..." # optional
|
|
530
662
|
```
|
|
531
663
|
|
|
532
|
-
`street`/`city`/`country`/`postal_code` are required — a partial profile
|
|
533
|
-
treated as no profile at all.
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
664
|
+
`street`/`city`/`country`/`postal_code` are required — a partial profile
|
|
665
|
+
(or one with empty values) is treated as no profile at all.
|
|
666
|
+
|
|
667
|
+
The variables are used in two ways:
|
|
668
|
+
|
|
669
|
+
- **Native UCP stores** (`buy` and `find` over HTTP) get
|
|
670
|
+
`PORTAGE_SHIP_COUNTRY`, `_REGION` and `_POSTAL_CODE` (plus
|
|
671
|
+
`PORTAGE_CURRENCY` and `PORTAGE_LANGUAGE`, if set) as UCP buyer context,
|
|
672
|
+
which a store uses to pick the market it prices and stocks in. These
|
|
673
|
+
work on their own, without the full address.
|
|
674
|
+
Without at least the country, a live Shopify store can report in-stock
|
|
675
|
+
items as out of stock.
|
|
676
|
+
- **Your own store** (`portage buy`'s adapter-credentials fallback,
|
|
677
|
+
described at the top of this file), when its adapter supports
|
|
678
|
+
`dev.ucp.shopping.fulfillment`, also gets the full address as the
|
|
679
|
+
checkout's shipping destination. Once the merchant prices shipping
|
|
680
|
+
options against it, `portage buy` auto-picks the cheapest per fulfillment
|
|
681
|
+
group; there's no interactive rate picker, since this drives one
|
|
682
|
+
automated purchase. Native (non-adapter) UCP stores don't get the full
|
|
683
|
+
address yet — see `portage-ucp`'s design log for why.
|
|
538
684
|
|
|
539
685
|
## Buying without a URL
|
|
540
686
|
|
data/exe/portage
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
# Before anything reads ENV: ~/.portage/.env (or PORTAGE_ENV_FILE), never
|
|
5
|
+
# overriding the real environment. See Portage::Cli::DotEnv.
|
|
6
|
+
require "portage/cli/dot_env"
|
|
7
|
+
Portage::Cli::DotEnv.load
|
|
8
|
+
|
|
4
9
|
require "portage/cli"
|
|
5
10
|
|
|
6
11
|
exit(Portage::Cli.run(ARGV))
|
data/exe/portage-console
CHANGED
data/lib/portage/cli/doctor.rb
CHANGED
|
@@ -3,6 +3,8 @@ require "uri"
|
|
|
3
3
|
require_relative "confidence_check"
|
|
4
4
|
require_relative "user_agent"
|
|
5
5
|
require_relative "proxy_settings"
|
|
6
|
+
require_relative "shipping_profile"
|
|
7
|
+
require_relative "dot_env"
|
|
6
8
|
|
|
7
9
|
module Portage
|
|
8
10
|
module Cli
|
|
@@ -16,21 +18,39 @@ module Portage
|
|
|
16
18
|
# process, so a host app passes `--require` to load its own initializer
|
|
17
19
|
# first (Rails: `--require ./config/environment`).
|
|
18
20
|
class Doctor
|
|
19
|
-
|
|
21
|
+
# `level` is "warning" (something to fix; any one makes doctor exit 1)
|
|
22
|
+
# or "info" (a report, e.g. how portage was installed). `details` is
|
|
23
|
+
# structured data for `--json`; `to_h` drops it when there is none.
|
|
24
|
+
Finding = Struct.new(:check, :message, :level, :details, keyword_init: true) do
|
|
25
|
+
def initialize(level: "warning", **) = super
|
|
26
|
+
def warning? = level == "warning"
|
|
27
|
+
def to_h = super.compact
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
SELLER_CHECKS = "authenticator, rate limiter, signing keys, payment handlers".freeze
|
|
20
31
|
|
|
21
|
-
|
|
32
|
+
# @param seller [Boolean] run the seller-side checks against
|
|
33
|
+
# Portage::Ucp.configuration. `portage doctor` passes true only when
|
|
34
|
+
# --require or --adapter loaded a seller's setup: in a bare shopper
|
|
35
|
+
# process that configuration is always the unconfigured default, so
|
|
36
|
+
# those four warnings were noise on every fresh install.
|
|
37
|
+
def initialize(adapter_class: nil, proxy_settings: ProxySettings.new, install_doctor: InstallDoctor.new,
|
|
38
|
+
seller: true, dot_env_path: DotEnv.loaded_path)
|
|
22
39
|
@adapter_class = adapter_class
|
|
23
40
|
@proxy_settings = proxy_settings
|
|
41
|
+
@install_doctor = install_doctor
|
|
42
|
+
@seller = seller
|
|
43
|
+
@dot_env_path = dot_env_path
|
|
24
44
|
end
|
|
25
45
|
|
|
26
46
|
def call
|
|
27
47
|
[
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
payment_handlers_finding,
|
|
48
|
+
*@install_doctor.findings,
|
|
49
|
+
dot_env_finding,
|
|
50
|
+
*seller_findings,
|
|
32
51
|
decision_backend_finding,
|
|
33
52
|
user_agent_finding,
|
|
53
|
+
shipping_finding,
|
|
34
54
|
proxy_finding,
|
|
35
55
|
*proxy_doctor_findings,
|
|
36
56
|
*capability_findings
|
|
@@ -41,6 +61,31 @@ module Portage
|
|
|
41
61
|
|
|
42
62
|
def config = Portage::Ucp.configuration
|
|
43
63
|
|
|
64
|
+
def seller_findings
|
|
65
|
+
return [authenticator_finding, rate_limiter_finding, signing_keys_finding, payment_handlers_finding] if @seller
|
|
66
|
+
|
|
67
|
+
[Finding.new(check: "seller", level: "info",
|
|
68
|
+
message: "Seller checks (#{SELLER_CHECKS}) skipped — pass --require with your app's " \
|
|
69
|
+
"initializer, or --adapter, to run them.")]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Which env file DotEnv loaded, and a warning when it's readable by
|
|
73
|
+
# other users: it's where store credentials and payment tokens end up.
|
|
74
|
+
def dot_env_finding
|
|
75
|
+
return unless @dot_env_path
|
|
76
|
+
|
|
77
|
+
mode = File.stat(@dot_env_path).mode & 0o777
|
|
78
|
+
details = { path: @dot_env_path, mode: format("%o", mode) }
|
|
79
|
+
return Finding.new(check: "env_file", level: "info", message: "Loaded #{@dot_env_path}", details: details) \
|
|
80
|
+
if mode.nobits?(0o077)
|
|
81
|
+
|
|
82
|
+
Finding.new(check: "env_file", details: details,
|
|
83
|
+
message: "#{@dot_env_path} is readable by other users (mode #{details[:mode]}) and may hold " \
|
|
84
|
+
"credentials — run `chmod 600 #{@dot_env_path}`.")
|
|
85
|
+
rescue SystemCallError
|
|
86
|
+
nil
|
|
87
|
+
end
|
|
88
|
+
|
|
44
89
|
def authenticator_finding
|
|
45
90
|
return unless config.authenticator.is_a?(Portage::Ucp::UnconfiguredAuthenticator)
|
|
46
91
|
|
|
@@ -93,6 +138,36 @@ module Portage
|
|
|
93
138
|
"raise instead of sending.")
|
|
94
139
|
end
|
|
95
140
|
|
|
141
|
+
# PORTAGE_SHIP_* (ShippingProfile, BuyerContext) has two jobs, and both
|
|
142
|
+
# fail quietly: with no complete address an own-store checkout goes
|
|
143
|
+
# out with no shipping destination, and with no PORTAGE_SHIP_COUNTRY a
|
|
144
|
+
# native UCP store gets no buyer context, so a live Shopify store
|
|
145
|
+
# builds a cart in no market and reports in-stock items as
|
|
146
|
+
# `merchandise_out_of_stock` (docs/ucp-tool-gating-investigation.md).
|
|
147
|
+
def shipping_finding
|
|
148
|
+
missing = ShippingProfile::REQUIRED.map { |key| ShippingProfile::ENV_VARS.fetch(key) }
|
|
149
|
+
.select { |var| ENV[var].to_s.empty? }
|
|
150
|
+
return if missing.empty?
|
|
151
|
+
|
|
152
|
+
Finding.new(check: "shipping", message: "#{shipping_gap(missing)} Set them in ~/.portage/.env " \
|
|
153
|
+
"(.env.example lists every variable) or your shell.")
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def shipping_gap(missing)
|
|
157
|
+
country = ShippingProfile::ENV_VARS.fetch(:address_country)
|
|
158
|
+
gap = if missing.length == ShippingProfile::REQUIRED.length
|
|
159
|
+
"No shipping address set (#{missing.join(', ')})."
|
|
160
|
+
else
|
|
161
|
+
"Shipping address incomplete: missing #{missing.join(', ')}, and a partial address is " \
|
|
162
|
+
"treated as none."
|
|
163
|
+
end
|
|
164
|
+
gap += " `portage buy` sends no shipping destination to your own store's checkout"
|
|
165
|
+
return "#{gap}." unless missing.include?(country)
|
|
166
|
+
|
|
167
|
+
"#{gap}, and without #{country} a native UCP store (e.g. Shopify) gets no market to price in " \
|
|
168
|
+
"and can report in-stock items as out of stock."
|
|
169
|
+
end
|
|
170
|
+
|
|
96
171
|
# Phase 0 of docs/plans/proxy-support.md: every raw Net::HTTP.start call
|
|
97
172
|
# site in portage-cli/portage-ucp/the adapter gems resolves its proxy
|
|
98
173
|
# from Ruby stdlib's own `:ENV` default, which — confirmed against a
|
|
@@ -192,3 +267,4 @@ end
|
|
|
192
267
|
# Doctor has to exist first (this file requires proxy_settings, not
|
|
193
268
|
# proxy_doctor, at the top for exactly this reason).
|
|
194
269
|
require_relative "proxy_doctor"
|
|
270
|
+
require_relative "install_doctor"
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
module Portage
|
|
2
|
+
module Cli
|
|
3
|
+
# Loads `~/.portage/.env` (or the file PORTAGE_ENV_FILE names) into ENV
|
|
4
|
+
# before `portage`/`portage-console` run, so shipping address, search
|
|
5
|
+
# keys and adapter credentials can live in one file next to config.json
|
|
6
|
+
# instead of a shell profile. A Homebrew install has no repo checkout to
|
|
7
|
+
# keep a `.env` in, so the user's own Portage directory is the one place
|
|
8
|
+
# every install method shares.
|
|
9
|
+
#
|
|
10
|
+
# Deliberately *not* `./.env` from the working directory: `portage` run
|
|
11
|
+
# inside a cloned repo would then take that repo's PORTAGE_PROXY/
|
|
12
|
+
# PORTAGE_PROXY_CA (an intercepting proxy on store traffic), notify
|
|
13
|
+
# webhooks or store credentials without the user ever choosing them.
|
|
14
|
+
# PORTAGE_ENV_FILE=.env opts into a project file explicitly.
|
|
15
|
+
#
|
|
16
|
+
# Stdlib only (no dotenv gem, so the formula gains no resource). The real
|
|
17
|
+
# environment always wins, and an empty value is skipped, so a file
|
|
18
|
+
# copied from .env.example with blanks left in sets nothing.
|
|
19
|
+
module DotEnv
|
|
20
|
+
DEFAULT_PATH = File.join(Dir.home, ".portage", ".env").freeze
|
|
21
|
+
ESCAPES = { "n" => "\n", '"' => '"', "\\" => "\\" }.freeze
|
|
22
|
+
LINE = /\A\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)\z/
|
|
23
|
+
|
|
24
|
+
class << self
|
|
25
|
+
# The file #load read in this process, for `portage doctor`.
|
|
26
|
+
attr_reader :loaded_path
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @return [String, nil] the path that was loaded, if any.
|
|
30
|
+
def self.load(path: ENV.fetch("PORTAGE_ENV_FILE", nil) || DEFAULT_PATH, env: ENV)
|
|
31
|
+
path = File.expand_path(path)
|
|
32
|
+
return unless File.file?(path) && File.readable?(path)
|
|
33
|
+
|
|
34
|
+
parse(File.read(path)).each { |key, value| env[key] = value unless env.key?(key) }
|
|
35
|
+
@loaded_path = path
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @return [Hash{String => String}] non-empty assignments, in file order.
|
|
39
|
+
def self.parse(text)
|
|
40
|
+
text.each_line.filter_map do |line|
|
|
41
|
+
match = LINE.match(line.chomp)
|
|
42
|
+
next unless match
|
|
43
|
+
|
|
44
|
+
value = unquote(match[2])
|
|
45
|
+
[match[1], value] unless value.empty?
|
|
46
|
+
end.to_h
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# `"..."` (with \n, \" and \\ escapes) or `'...'` (literal); unquoted
|
|
50
|
+
# values drop a trailing ` # comment`.
|
|
51
|
+
def self.unquote(raw)
|
|
52
|
+
case raw
|
|
53
|
+
when /\A"((?:[^"\\]|\\.)*)"/
|
|
54
|
+
Regexp.last_match(1).gsub(/\\([n"\\])/) { ESCAPES.fetch(Regexp.last_match(1)) }
|
|
55
|
+
when /\A'([^']*)'/ then Regexp.last_match(1)
|
|
56
|
+
else raw.sub(/\s+#.*\z/, "").strip
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
private_class_method :unquote
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
require "rbconfig"
|
|
2
|
+
require_relative "version"
|
|
3
|
+
|
|
4
|
+
module Portage
|
|
5
|
+
module Cli
|
|
6
|
+
# Phase 4 of docs/plans/homebrew-distribution.md's `doctor` checks: how
|
|
7
|
+
# this copy of portage-cli was installed (Homebrew formula or plain
|
|
8
|
+
# `gem install`), which Ruby it runs on, which first-party adapter gems
|
|
9
|
+
# it can load, and whether the `portage` a shell would actually run is
|
|
10
|
+
# this one.
|
|
11
|
+
#
|
|
12
|
+
# That last check is the one that matters. The formula installs into its
|
|
13
|
+
# own Cellar keg, and a `gem install portage-cli` copy in a mise/rbenv/
|
|
14
|
+
# asdf/rvm Ruby's bin usually comes earlier on PATH, so `portage` in a
|
|
15
|
+
# shell silently keeps running the old gem after `brew install` or
|
|
16
|
+
# `brew upgrade` (Homebrew's own caveat hit exactly this on the
|
|
17
|
+
# maintainer's machine). Homebrew's `bin/portage` is a symlink into the
|
|
18
|
+
# Cellar, whose env_script wrapper execs the libexec binary, so every
|
|
19
|
+
# comparison here is between canonical Cellar locations, never raw PATH
|
|
20
|
+
# entries.
|
|
21
|
+
#
|
|
22
|
+
# Everything is read from the filesystem and the process: no `brew`
|
|
23
|
+
# shell-out, no network, so doctor stays fast and works offline (the
|
|
24
|
+
# formula's `test do` block runs it in Homebrew's sandbox).
|
|
25
|
+
class InstallDoctor
|
|
26
|
+
DEFAULT_HOMEBREW_PREFIXES = %w[/opt/homebrew /usr/local /home/linuxbrew/.linuxbrew].freeze
|
|
27
|
+
ADAPTER_GEMS = %w[shopify wix woocommerce bigcommerce magento etsy instagram webmcp decision]
|
|
28
|
+
.map { |name| "portage-ucp-#{name}" }.freeze
|
|
29
|
+
EXECUTABLE = "portage".freeze
|
|
30
|
+
|
|
31
|
+
def self.homebrew_prefixes
|
|
32
|
+
[ENV.fetch("HOMEBREW_PREFIX", nil), *DEFAULT_HOMEBREW_PREFIXES].reject { |p| p.to_s.empty? }.uniq
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @param path [String] PATH to search for `portage` executables.
|
|
36
|
+
# @param homebrew_prefixes [Array<String>] candidate HOMEBREW_PREFIXes.
|
|
37
|
+
# @param gem_dir [String] this gem's own directory (its `lib/`'s parent).
|
|
38
|
+
# @param ruby [String] the running Ruby's executable path.
|
|
39
|
+
# @param adapter_probe [#call] gem name -> { installed:, loadable:, version:, error: }.
|
|
40
|
+
def initialize(path: ENV.fetch("PATH", ""), homebrew_prefixes: self.class.homebrew_prefixes,
|
|
41
|
+
gem_dir: File.expand_path("../../..", __dir__), ruby: RbConfig.ruby,
|
|
42
|
+
adapter_probe: method(:probe_adapter))
|
|
43
|
+
@path = path
|
|
44
|
+
@homebrew_prefixes = homebrew_prefixes.map { |prefix| canonical(prefix) }
|
|
45
|
+
@gem_dir = canonical(gem_dir)
|
|
46
|
+
@ruby = ruby
|
|
47
|
+
@adapter_probe = adapter_probe
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def findings
|
|
51
|
+
[install_finding, runtime_finding, adapters_finding, path_finding].compact
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @return [String, nil] the HOMEBREW_PREFIX this copy runs from, nil for a gem install.
|
|
55
|
+
def homebrew_prefix
|
|
56
|
+
return @homebrew_prefix if defined?(@homebrew_prefix)
|
|
57
|
+
|
|
58
|
+
@homebrew_prefix = @homebrew_prefixes.find do |prefix|
|
|
59
|
+
[@gem_dir, canonical(@ruby)].any? { |dir| under?(dir, keg_root(prefix)) }
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def homebrew? = !homebrew_prefix.nil?
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
# --- install method -------------------------------------------------
|
|
68
|
+
|
|
69
|
+
def install_finding
|
|
70
|
+
method = homebrew? ? "homebrew" : "gem"
|
|
71
|
+
path = homebrew? ? installed_keg : @gem_dir
|
|
72
|
+
details = { method: method, path: path }
|
|
73
|
+
details[:prefix] = homebrew_prefix if homebrew?
|
|
74
|
+
info("install", "#{method} (#{path})", details)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def keg_root(prefix) = File.join(prefix, "Cellar", "portage")
|
|
78
|
+
|
|
79
|
+
# `<prefix>/Cellar/portage/<version>`, the keg this copy lives in.
|
|
80
|
+
def installed_keg
|
|
81
|
+
root = keg_root(homebrew_prefix)
|
|
82
|
+
version = @gem_dir.delete_prefix("#{root}/").split("/").first
|
|
83
|
+
File.join(root, version)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# --- runtime --------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
def runtime_finding
|
|
89
|
+
info("runtime", "Ruby #{RUBY_VERSION} (#{@ruby}), portage-cli #{VERSION}",
|
|
90
|
+
{ ruby_version: RUBY_VERSION, ruby_path: @ruby, portage_cli_version: VERSION })
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# --- adapters -------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
# A gem install without adapters is normal: `gem install portage-cli`
|
|
96
|
+
# pulls in none of them, and you add the ones you use. The formula
|
|
97
|
+
# bundles every one, so a Homebrew install missing one is broken.
|
|
98
|
+
def adapters_finding
|
|
99
|
+
report = AdapterReport.new(ADAPTER_GEMS.map { |name| { name: name }.merge(safe_probe(name)) })
|
|
100
|
+
details = { adapters: report.adapters }
|
|
101
|
+
return warning("adapters", report.homebrew_missing_message, details) if homebrew? && report.missing?
|
|
102
|
+
|
|
103
|
+
info("adapters", report.summary, details)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def safe_probe(name)
|
|
107
|
+
@adapter_probe.call(name)
|
|
108
|
+
rescue StandardError, ScriptError => e
|
|
109
|
+
{ installed: true, loadable: false, version: nil, error: "#{e.class}: #{e.message}" }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def probe_adapter(name)
|
|
113
|
+
specs = Gem::Specification.find_all_by_name(name)
|
|
114
|
+
return { installed: false, loadable: false, version: nil } if specs.empty?
|
|
115
|
+
|
|
116
|
+
require "portage/ucp/#{name.delete_prefix('portage-ucp-')}"
|
|
117
|
+
spec = Gem.loaded_specs[name] || specs.max_by(&:version)
|
|
118
|
+
{ installed: true, loadable: true, version: spec.version.to_s }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# --- PATH shadowing -------------------------------------------------
|
|
122
|
+
|
|
123
|
+
def path_finding
|
|
124
|
+
candidates = path_candidates
|
|
125
|
+
return if candidates.empty?
|
|
126
|
+
|
|
127
|
+
details = { first: candidates.first[:path], candidates: candidates }
|
|
128
|
+
message = shadow_message(candidates.first)
|
|
129
|
+
return warning("path", message, details) if message
|
|
130
|
+
|
|
131
|
+
# Only a Homebrew keg can be matched to this copy for certain; a gem
|
|
132
|
+
# install's binstub can sit behind a mise/rbenv shim.
|
|
133
|
+
suffix = homebrew? ? " (this install)" : ""
|
|
134
|
+
info("path", "`portage` on PATH is #{candidates.first[:path]}#{suffix}", details)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Every `portage` on PATH, first match first, one entry per real file:
|
|
138
|
+
# the same Homebrew bin can sit on PATH twice, and `bin/portage` and
|
|
139
|
+
# `opt/portage/bin/portage` both resolve to one keg.
|
|
140
|
+
def path_candidates
|
|
141
|
+
candidates = @path.split(File::PATH_SEPARATOR).reject(&:empty?).filter_map { |dir| candidate(dir) }
|
|
142
|
+
candidates.uniq { |c| c[:realpath] }
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def candidate(dir)
|
|
146
|
+
file = File.join(dir, EXECUTABLE)
|
|
147
|
+
return unless File.file?(file) && File.executable?(file)
|
|
148
|
+
|
|
149
|
+
real = canonical(file)
|
|
150
|
+
{ path: file, realpath: real, kind: homebrew_keg?(real) ? "homebrew" : "other" }
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def shadow_message(first)
|
|
154
|
+
if homebrew?
|
|
155
|
+
return if under?(first[:realpath], keg_root(homebrew_prefix))
|
|
156
|
+
|
|
157
|
+
homebrew_shadowed_message(first)
|
|
158
|
+
elsif first[:kind] == "homebrew"
|
|
159
|
+
gem_shadowed_message(first)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def homebrew_shadowed_message(first)
|
|
164
|
+
brew_bin = File.join(homebrew_prefix, "bin")
|
|
165
|
+
"`portage` on PATH runs #{first[:path]}, not this Homebrew install (#{brew_bin}/portage), " \
|
|
166
|
+
"so `brew upgrade portage` won't change what your shell runs. Check with `which -a portage`. " \
|
|
167
|
+
"Fix: if that's a `gem install` copy, run `gem uninstall portage-cli` with that Ruby active; " \
|
|
168
|
+
"or put #{brew_bin} ahead of #{File.dirname(first[:path])} in PATH."
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def gem_shadowed_message(first)
|
|
172
|
+
"`portage` on PATH runs the Homebrew install (#{first[:path]}), not this gem install " \
|
|
173
|
+
"(#{@gem_dir}). Check with `which -a portage`. Fix: keep one — `brew uninstall portage` " \
|
|
174
|
+
"to use the gem, or `gem uninstall portage-cli` to use Homebrew; or reorder PATH."
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def homebrew_keg?(realpath)
|
|
178
|
+
@homebrew_prefixes.any? { |prefix| under?(realpath, keg_root(prefix)) }
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# --- helpers --------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
def under?(path, root) = path.start_with?("#{root}/")
|
|
184
|
+
|
|
185
|
+
def canonical(path)
|
|
186
|
+
File.realpath(path)
|
|
187
|
+
rescue SystemCallError
|
|
188
|
+
File.expand_path(path)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def info(check, message, details) = finding(check, message, details, "info")
|
|
192
|
+
def warning(check, message, details) = finding(check, message, details, "warning")
|
|
193
|
+
|
|
194
|
+
def finding(check, message, details, level)
|
|
195
|
+
Doctor::Finding.new(check: check, message: message, level: level, details: details)
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# The adapters line of InstallDoctor's report, one probe result per gem:
|
|
200
|
+
# `{ name:, installed:, loadable:, version:, error: }`.
|
|
201
|
+
class InstallDoctor
|
|
202
|
+
class AdapterReport
|
|
203
|
+
attr_reader :adapters
|
|
204
|
+
|
|
205
|
+
def initialize(adapters)
|
|
206
|
+
@adapters = adapters
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def missing = @adapters.reject { |a| a[:loadable] }
|
|
210
|
+
def missing? = missing.any?
|
|
211
|
+
|
|
212
|
+
def summary
|
|
213
|
+
loaded, broken, absent = partitioned
|
|
214
|
+
parts = [loaded.empty? ? "none loadable" : loaded.map { |a| "#{short(a)} #{a[:version]}" }.join(", ")]
|
|
215
|
+
parts << "failed to load: #{names(broken)}" unless broken.empty?
|
|
216
|
+
parts << "not installed: #{names(absent)}" unless absent.empty?
|
|
217
|
+
parts.join("; ")
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def homebrew_missing_message
|
|
221
|
+
described = missing.map { |a| a[:error] ? "#{short(a)} (#{a[:error]})" : short(a) }
|
|
222
|
+
"This Homebrew install bundles every adapter, but #{described.join(', ')} can't be loaded — " \
|
|
223
|
+
"reinstall with `brew reinstall portage`."
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
private
|
|
227
|
+
|
|
228
|
+
def partitioned
|
|
229
|
+
loaded, rest = @adapters.partition { |a| a[:loadable] }
|
|
230
|
+
broken, absent = rest.partition { |a| a[:installed] }
|
|
231
|
+
[loaded, broken, absent]
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def names(adapters) = adapters.map { |a| short(a) }.join(", ")
|
|
235
|
+
def short(adapter) = adapter[:name].delete_prefix("portage-ucp-")
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
@@ -20,9 +20,14 @@ module Portage
|
|
|
20
20
|
|
|
21
21
|
# @return [Portage::Ucp::PostalAddress, nil] nil unless every required
|
|
22
22
|
# field is set — a partial profile isn't enough to submit, and this
|
|
23
|
-
# module never guesses at a missing field.
|
|
23
|
+
# module never guesses at a missing field. An empty value counts as
|
|
24
|
+
# unset, so a `.env` copied from `.env.example` with blanks left in
|
|
25
|
+
# doesn't submit an address of empty strings.
|
|
24
26
|
def self.from_env
|
|
25
|
-
attrs = ENV_VARS.filter_map
|
|
27
|
+
attrs = ENV_VARS.filter_map do |key, var|
|
|
28
|
+
value = ENV.fetch(var, nil)
|
|
29
|
+
[key, value] unless value.to_s.empty?
|
|
30
|
+
end.to_h
|
|
26
31
|
return nil unless REQUIRED.all? { |key| attrs.key?(key) }
|
|
27
32
|
|
|
28
33
|
Portage::Ucp::PostalAddress.new(**attrs)
|
data/lib/portage/cli/version.rb
CHANGED
data/lib/portage/cli.rb
CHANGED
|
@@ -854,16 +854,26 @@ module Portage
|
|
|
854
854
|
proxy_settings = apply_proxy_settings(opts[:proxy])
|
|
855
855
|
return 1 unless proxy_settings
|
|
856
856
|
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
857
|
+
doctor = Doctor.new(adapter_class: adapter_class, proxy_settings: proxy_settings,
|
|
858
|
+
seller: !(opts[:require] || opts[:adapter]).nil?)
|
|
859
|
+
report_doctor(doctor.call, json: opts[:json])
|
|
860
860
|
end
|
|
861
861
|
private_class_method :run_doctor
|
|
862
862
|
|
|
863
|
-
def self.
|
|
864
|
-
|
|
863
|
+
def self.report_doctor(findings, json:)
|
|
864
|
+
puts json ? JSON.pretty_generate(findings.map(&:to_h)) : format_doctor(findings)
|
|
865
|
+
findings.none?(&:warning?) ? 0 : 1
|
|
866
|
+
end
|
|
867
|
+
private_class_method :report_doctor
|
|
865
868
|
|
|
866
|
-
|
|
869
|
+
# Info findings (install method, Ruby, adapters, PATH) first, then the
|
|
870
|
+
# warnings, which alone decide the exit code.
|
|
871
|
+
def self.format_doctor(findings)
|
|
872
|
+
info, warnings = findings.partition { |f| !f.warning? }
|
|
873
|
+
lines = info.map { |f| "[#{f.check}] #{f.message}" }
|
|
874
|
+
lines << "" unless info.empty?
|
|
875
|
+
lines.concat(warnings.empty? ? ["No issues found."] : warnings.map { |f| "[#{f.check}] #{f.message}" })
|
|
876
|
+
lines.join("\n")
|
|
867
877
|
end
|
|
868
878
|
private_class_method :format_doctor
|
|
869
879
|
|
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.7.
|
|
4
|
+
version: 0.7.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tom Whitbread
|
|
@@ -165,6 +165,7 @@ files:
|
|
|
165
165
|
- lib/portage/cli/console.rb
|
|
166
166
|
- lib/portage/cli/decisions.rb
|
|
167
167
|
- lib/portage/cli/doctor.rb
|
|
168
|
+
- lib/portage/cli/dot_env.rb
|
|
168
169
|
- lib/portage/cli/find.rb
|
|
169
170
|
- lib/portage/cli/generate/adapter.rb
|
|
170
171
|
- lib/portage/cli/generate/agent_profile.rb
|
|
@@ -175,6 +176,7 @@ files:
|
|
|
175
176
|
- lib/portage/cli/handoff_waiter.rb
|
|
176
177
|
- lib/portage/cli/history.rb
|
|
177
178
|
- lib/portage/cli/homepage_fetch.rb
|
|
179
|
+
- lib/portage/cli/install_doctor.rb
|
|
178
180
|
- lib/portage/cli/macos_notifier.rb
|
|
179
181
|
- lib/portage/cli/notifier.rb
|
|
180
182
|
- lib/portage/cli/payment_methods.rb
|