portage-cli 0.7.5 → 0.9.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.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +616 -0
  3. data/README.md +302 -5
  4. data/known-stores/categories.yml +1263 -0
  5. data/lib/portage/cli/agent_profile_url.rb +30 -0
  6. data/lib/portage/cli/approval_policy.rb +56 -0
  7. data/lib/portage/cli/approve.rb +135 -0
  8. data/lib/portage/cli/browser_import/categorize.rb +59 -0
  9. data/lib/portage/cli/browser_import/confirm.rb +35 -0
  10. data/lib/portage/cli/browser_import/domains.rb +47 -0
  11. data/lib/portage/cli/browser_import/filter.rb +91 -0
  12. data/lib/portage/cli/browser_import/importer.rb +248 -0
  13. data/lib/portage/cli/browser_import/plist_xml.rb +72 -0
  14. data/lib/portage/cli/browser_import/prober.rb +60 -0
  15. data/lib/portage/cli/browser_import/profiles.rb +114 -0
  16. data/lib/portage/cli/browser_import/readers.rb +179 -0
  17. data/lib/portage/cli/browser_import/saver.rb +62 -0
  18. data/lib/portage/cli/browser_import/sqlite.rb +68 -0
  19. data/lib/portage/cli/browser_import.rb +23 -0
  20. data/lib/portage/cli/browser_opener.rb +38 -0
  21. data/lib/portage/cli/browser_profile/allowlist.rb +40 -0
  22. data/lib/portage/cli/browser_profile/bridge.rb +120 -0
  23. data/lib/portage/cli/browser_profile/browsers.rb +69 -0
  24. data/lib/portage/cli/browser_profile/cdp.rb +67 -0
  25. data/lib/portage/cli/browser_profile/cdp_socket.rb +186 -0
  26. data/lib/portage/cli/browser_profile/errors.rb +26 -0
  27. data/lib/portage/cli/browser_profile/launcher.rb +34 -0
  28. data/lib/portage/cli/browser_profile/profile.rb +93 -0
  29. data/lib/portage/cli/browser_profile.rb +25 -0
  30. data/lib/portage/cli/buy.rb +559 -29
  31. data/lib/portage/cli/checkout_handoff.rb +5 -24
  32. data/lib/portage/cli/classifier.rb +158 -0
  33. data/lib/portage/cli/compare.rb +3 -0
  34. data/lib/portage/cli/doctor.rb +155 -1
  35. data/lib/portage/cli/dot_env.rb +55 -0
  36. data/lib/portage/cli/find.rb +103 -12
  37. data/lib/portage/cli/handoff_agents.rb +186 -0
  38. data/lib/portage/cli/handoff_only.rb +94 -0
  39. data/lib/portage/cli/handoff_reconciler.rb +15 -1
  40. data/lib/portage/cli/handoff_target.rb +61 -0
  41. data/lib/portage/cli/history.rb +47 -2
  42. data/lib/portage/cli/human_prompt.rb +118 -0
  43. data/lib/portage/cli/index/builder.rb +335 -0
  44. data/lib/portage/cli/index/exporter.rb +91 -0
  45. data/lib/portage/cli/index/known_cache.rb +155 -0
  46. data/lib/portage/cli/index/product_store.rb +101 -0
  47. data/lib/portage/cli/index/sources/browser.rb +31 -0
  48. data/lib/portage/cli/index/sources/shopify_catalog.rb +82 -0
  49. data/lib/portage/cli/index/sources/stores_file.rb +58 -0
  50. data/lib/portage/cli/index/sources/webmcp_sweep.rb +29 -0
  51. data/lib/portage/cli/index/sources/wikidata.rb +95 -0
  52. data/lib/portage/cli/index/sources.rb +44 -0
  53. data/lib/portage/cli/index/store.rb +109 -0
  54. data/lib/portage/cli/index.rb +20 -0
  55. data/lib/portage/cli/known_stores_url.rb +15 -0
  56. data/lib/portage/cli/money.rb +18 -0
  57. data/lib/portage/cli/offer_choice.rb +38 -0
  58. data/lib/portage/cli/offer_sources.rb +460 -0
  59. data/lib/portage/cli/payment_methods.rb +24 -3
  60. data/lib/portage/cli/pick.rb +167 -0
  61. data/lib/portage/cli/product_page.rb +84 -0
  62. data/lib/portage/cli/quotes.rb +82 -0
  63. data/lib/portage/cli/search_backends.rb +337 -12
  64. data/lib/portage/cli/setup_wizard/prompt.rb +67 -0
  65. data/lib/portage/cli/setup_wizard/steps/agent_profile.rb +60 -0
  66. data/lib/portage/cli/setup_wizard/steps/browser_import.rb +29 -0
  67. data/lib/portage/cli/setup_wizard/steps/handoff.rb +100 -0
  68. data/lib/portage/cli/setup_wizard/steps/index_build.rb +31 -0
  69. data/lib/portage/cli/setup_wizard/steps/policy.rb +60 -0
  70. data/lib/portage/cli/setup_wizard/steps/retailer_keys.rb +55 -0
  71. data/lib/portage/cli/setup_wizard/steps/search_keys.rb +54 -0
  72. data/lib/portage/cli/setup_wizard/steps/shipping.rb +51 -0
  73. data/lib/portage/cli/setup_wizard.rb +74 -0
  74. data/lib/portage/cli/version.rb +1 -1
  75. data/lib/portage/cli/webmcp.rb +10 -3
  76. data/lib/portage/cli/webmcp_autofill_confirm.rb +38 -0
  77. data/lib/portage/cli/webmcp_autofill_fields.rb +60 -0
  78. data/lib/portage/cli/webmcp_autofill_mode.rb +39 -0
  79. data/lib/portage/cli/webmcp_mapping_confirm.rb +68 -0
  80. data/lib/portage/cli/webmcp_mappings.rb +84 -0
  81. data/lib/portage/cli.rb +930 -43
  82. metadata +67 -2
@@ -0,0 +1,26 @@
1
+ module Portage
2
+ module Cli
3
+ module BrowserProfile
4
+ # Base for everything this feature raises — a caller (cli.rb, Buy)
5
+ # can rescue this one class instead of naming each subclass.
6
+ class Error < StandardError; end
7
+
8
+ # `Browsers.binary_for` found nothing installed for the requested
9
+ # browser.
10
+ class BrowserNotFoundError < Error; end
11
+
12
+ # The profile process never answered its own CDP endpoint within the
13
+ # launch wait — a slow machine, a browser that failed to start, or a
14
+ # port already held by something else.
15
+ class LaunchError < Error; end
16
+
17
+ # docs/plans/buy-skill-and-local-browser.md Phase 6: driving stayed
18
+ # inside the allowed domains (the store being bought from, plus its
19
+ # checkout host once known) or this stopped the run rather than
20
+ # continue on an unexpected host. Never rescued into "try again
21
+ # somewhere else" — the whole point is that nothing here recovers by
22
+ # itself.
23
+ class DomainNotAllowedError < Error; end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,34 @@
1
+ module Portage
2
+ module Cli
3
+ module BrowserProfile
4
+ # The one place `Process.spawn` is called from this feature —
5
+ # injectable (`spawn:`) so Profile's own specs never start a real
6
+ # browser (docs/plans/buy-skill-and-local-browser.md Phase 6's own
7
+ # "specs must not launch a real browser" rule).
8
+ #
9
+ # Flags: `--user-data-dir` is always the dedicated Portage profile
10
+ # directory (never the browser's default one — Chrome 136+ refuses
11
+ # remote debugging on the default profile anyway, and this never
12
+ # tries), `--remote-debugging-port` is this profile's own fixed
13
+ # port, and `--no-first-run`/`--no-default-browser-check` keep the
14
+ # launch from popping up onboarding UI the first time. Detached
15
+ # (its own process group, stdout/stderr discarded) so `portage
16
+ # browser profile open` returns immediately rather than blocking on
17
+ # the browser's own lifetime.
18
+ class Launcher
19
+ def initialize(spawn: ->(*args) { Process.spawn(*args) })
20
+ @spawn = spawn
21
+ end
22
+
23
+ def launch(binary:, profile_dir:, port:, url: nil)
24
+ argv = [binary, "--user-data-dir=#{profile_dir}", "--remote-debugging-port=#{port}",
25
+ "--no-first-run", "--no-default-browser-check"]
26
+ argv << url if url
27
+ pid = @spawn.call(*argv, %i[out err] => File::NULL, pgroup: true)
28
+ Process.detach(pid)
29
+ pid
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,93 @@
1
+ require "fileutils"
2
+ require_relative "browsers"
3
+ require_relative "cdp"
4
+ require_relative "errors"
5
+ require_relative "launcher"
6
+
7
+ module Portage
8
+ module Cli
9
+ module BrowserProfile
10
+ # `portage browser profile init|open|status` (docs/plans/
11
+ # buy-skill-and-local-browser.md Phase 6): a dedicated Chromium
12
+ # profile directory under ~/.portage/browser/<browser>/profile,
13
+ # launched with remote debugging bound to that profile only —
14
+ # never the browser's own default profile, and never a directory
15
+ # this class didn't create itself. The user signs into their
16
+ # shopping sites in this profile once; nothing here ever reads a
17
+ # credential, cookie or autofill store from it (that's Chromium's
18
+ # own job once the shopper is looking at the page).
19
+ class Profile
20
+ ROOT = File.join(Dir.home, ".portage", "browser").freeze
21
+ DEFAULT_PORT = 9223
22
+
23
+ attr_reader :browser, :port, :dir
24
+
25
+ def initialize(browser: "chrome", root: ROOT, port: DEFAULT_PORT, launcher: Launcher.new, cdp: Cdp,
26
+ poll_attempts: 20, poll_interval: 0.25, sleep_fn: ->(s) { sleep s })
27
+ raise ArgumentError, "unsupported browser #{browser.inspect} (want one of #{Browsers::CHROMIUM})" unless
28
+ Browsers::CHROMIUM.include?(browser)
29
+
30
+ @browser = browser
31
+ @dir = File.join(root, browser, "profile")
32
+ @port = port
33
+ @launcher = launcher
34
+ @cdp = cdp
35
+ @poll_attempts = poll_attempts
36
+ @poll_interval = poll_interval
37
+ @sleep_fn = sleep_fn
38
+ end
39
+
40
+ # Never launches anything — just makes sure the dedicated profile
41
+ # directory exists, so `portage browser profile open` always has
42
+ # somewhere of its own to point `--user-data-dir` at.
43
+ def init!
44
+ FileUtils.mkdir_p(@dir)
45
+ { browser: @browser, dir: @dir, port: @port, created: true }
46
+ end
47
+
48
+ # @return [Hash] `running:` plus, when running, whatever
49
+ # `/json/version` reports (Browser, protocolVersion, …) — no
50
+ # HTTP request is ever made to anything but 127.0.0.1:`port`.
51
+ def status
52
+ version = @cdp.version(port: @port)
53
+ base = { running: !version.nil?, browser: @browser, dir: @dir, port: @port }
54
+ version ? base.merge(version) : base
55
+ end
56
+
57
+ # Launches the browser if it isn't already running on this
58
+ # profile's port, then either opens a new tab at `url` or hands
59
+ # back the first existing tab. Idempotent: calling this against an
60
+ # already-running profile just attaches.
61
+ def open!(url: nil)
62
+ launch! unless status[:running]
63
+ attach(url)
64
+ end
65
+
66
+ private
67
+
68
+ def launch!
69
+ binary = Browsers.binary_for(@browser)
70
+ raise BrowserNotFoundError, "#{@browser} isn't installed on this machine" unless binary
71
+
72
+ FileUtils.mkdir_p(@dir)
73
+ @launcher.launch(binary: binary, profile_dir: @dir, port: @port)
74
+ wait_until_running!
75
+ end
76
+
77
+ def wait_until_running!
78
+ @poll_attempts.times do
79
+ return true if status[:running]
80
+
81
+ @sleep_fn.call(@poll_interval)
82
+ end
83
+ raise LaunchError, "#{@browser} didn't answer its remote-debugging port (#{@port}) in time"
84
+ end
85
+
86
+ def attach(url)
87
+ target = url ? @cdp.new_tab(port: @port, url: url) : @cdp.list(port: @port).first
88
+ { running: true, browser: @browser, dir: @dir, port: @port, target: target }
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,25 @@
1
+ require_relative "browser_profile/errors"
2
+ require_relative "browser_profile/browsers"
3
+ require_relative "browser_profile/cdp"
4
+ require_relative "browser_profile/cdp_socket"
5
+ require_relative "browser_profile/allowlist"
6
+ require_relative "browser_profile/bridge"
7
+ require_relative "browser_profile/launcher"
8
+ require_relative "browser_profile/profile"
9
+
10
+ module Portage
11
+ module Cli
12
+ # `portage browser profile init|open|status` — the Portage browser
13
+ # profile (docs/plans/buy-skill-and-local-browser.md Phase 6, Tier B).
14
+ # A dedicated Chromium-family profile directory under
15
+ # ~/.portage/browser/, never the browser's own default profile, that
16
+ # `portage buy --handoff-target profile` drives as a WebMCP bridge
17
+ # (see Bridge) limited to a domain allowlist (Allowlist) — the store
18
+ # being bought from, plus its checkout host once known. The user signs
19
+ # into their shopping sites in it once; Portage never reads a
20
+ # credential/cookie/autofill store from it, never touches a payment
21
+ # field, and never clicks pay.
22
+ module BrowserProfile
23
+ end
24
+ end
25
+ end