portage-cli 0.7.4 → 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 +21 -0
- data/README.md +56 -14
- data/exe/portage +5 -0
- data/exe/portage-console +3 -0
- data/lib/portage/cli/doctor.rb +41 -7
- data/lib/portage/cli/dot_env.rb +62 -0
- data/lib/portage/cli/version.rb +1 -1
- data/lib/portage/cli.rb +3 -1
- metadata +2 -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,27 @@ 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
|
+
|
|
7
28
|
## [0.7.4] - 2026-09-25
|
|
8
29
|
|
|
9
30
|
- **`portage doctor` reports how it was installed, and warns when another
|
data/README.md
CHANGED
|
@@ -424,10 +424,15 @@ installed, then lists anything that needs fixing:
|
|
|
424
424
|
earlier on `PATH` shadows this one (see "Two copies on PATH" above).
|
|
425
425
|
- `shipping`: warns when `PORTAGE_SHIP_*` is missing or incomplete (see
|
|
426
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.
|
|
427
430
|
- Seller-side checks against `Portage::Ucp.configuration` (authenticator,
|
|
428
|
-
rate limiter, signing keys, payment handlers
|
|
429
|
-
your app's initializer
|
|
430
|
-
|
|
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.
|
|
431
436
|
|
|
432
437
|
With `--json` the output is an array of findings, each with `check`,
|
|
433
438
|
`message`, `level` (`warning` or `info`) and, for the install checks,
|
|
@@ -602,21 +607,58 @@ gives up after 5 seconds and reports `notify_error` instead.
|
|
|
602
607
|
`portage find`'s offer order comes from `Support::OfferRanking`: buyable
|
|
603
608
|
first, then cheapest, then unpriced.
|
|
604
609
|
|
|
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
|
+
|
|
605
647
|
### Shipping address
|
|
606
648
|
|
|
607
|
-
Set your shipping address
|
|
608
|
-
|
|
609
|
-
|
|
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:
|
|
610
652
|
|
|
611
653
|
```bash
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
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
|
|
620
662
|
```
|
|
621
663
|
|
|
622
664
|
`street`/`city`/`country`/`postal_code` are required — a partial profile
|
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
|
@@ -4,6 +4,7 @@ require_relative "confidence_check"
|
|
|
4
4
|
require_relative "user_agent"
|
|
5
5
|
require_relative "proxy_settings"
|
|
6
6
|
require_relative "shipping_profile"
|
|
7
|
+
require_relative "dot_env"
|
|
7
8
|
|
|
8
9
|
module Portage
|
|
9
10
|
module Cli
|
|
@@ -26,19 +27,27 @@ module Portage
|
|
|
26
27
|
def to_h = super.compact
|
|
27
28
|
end
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
SELLER_CHECKS = "authenticator, rate limiter, signing keys, payment handlers".freeze
|
|
31
|
+
|
|
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)
|
|
30
39
|
@adapter_class = adapter_class
|
|
31
40
|
@proxy_settings = proxy_settings
|
|
32
41
|
@install_doctor = install_doctor
|
|
42
|
+
@seller = seller
|
|
43
|
+
@dot_env_path = dot_env_path
|
|
33
44
|
end
|
|
34
45
|
|
|
35
46
|
def call
|
|
36
47
|
[
|
|
37
48
|
*@install_doctor.findings,
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
signing_keys_finding,
|
|
41
|
-
payment_handlers_finding,
|
|
49
|
+
dot_env_finding,
|
|
50
|
+
*seller_findings,
|
|
42
51
|
decision_backend_finding,
|
|
43
52
|
user_agent_finding,
|
|
44
53
|
shipping_finding,
|
|
@@ -52,6 +61,31 @@ module Portage
|
|
|
52
61
|
|
|
53
62
|
def config = Portage::Ucp.configuration
|
|
54
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
|
+
|
|
55
89
|
def authenticator_finding
|
|
56
90
|
return unless config.authenticator.is_a?(Portage::Ucp::UnconfiguredAuthenticator)
|
|
57
91
|
|
|
@@ -115,8 +149,8 @@ module Portage
|
|
|
115
149
|
.select { |var| ENV[var].to_s.empty? }
|
|
116
150
|
return if missing.empty?
|
|
117
151
|
|
|
118
|
-
Finding.new(check: "shipping", message: "#{shipping_gap(missing)}
|
|
119
|
-
".env.example lists every
|
|
152
|
+
Finding.new(check: "shipping", message: "#{shipping_gap(missing)} Set them in ~/.portage/.env " \
|
|
153
|
+
"(.env.example lists every variable) or your shell.")
|
|
120
154
|
end
|
|
121
155
|
|
|
122
156
|
def shipping_gap(missing)
|
|
@@ -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
|
data/lib/portage/cli/version.rb
CHANGED
data/lib/portage/cli.rb
CHANGED
|
@@ -854,7 +854,9 @@ module Portage
|
|
|
854
854
|
proxy_settings = apply_proxy_settings(opts[:proxy])
|
|
855
855
|
return 1 unless proxy_settings
|
|
856
856
|
|
|
857
|
-
|
|
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])
|
|
858
860
|
end
|
|
859
861
|
private_class_method :run_doctor
|
|
860
862
|
|
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
|