gemvault 0.2.5 → 0.2.7

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4447e59a767dbc08bd6c0964964fefaf00bb1f2768421f7ce1fc6247f9e8f2c4
4
- data.tar.gz: b017dc605efee9f1a576b1fc362b2cf51ebab25e48b424c2be7e50d9cb5f1e3f
3
+ metadata.gz: d8478b689eb6f6b68e0c18872f4b2ca47034b1bd93ef9d0d16ecb92e8666de85
4
+ data.tar.gz: cb7f53a602a6b55a1a0593eda146635197e79dadb7366ca930a1e67b5b40d80d
5
5
  SHA512:
6
- metadata.gz: 5c993d8b4fd00d8c35ed6de9645653400882565860f44449e7adbbcd53730ad7ca161e4cae10e7e817f512c3b63e2101e2ceb58496ebebe97eadd32eceefc46a
7
- data.tar.gz: 39bfbb4959725d6ad92401dfd3d6841ea72c94021031bdf7680bdde9f2563bd721b51329db7cd76eaf0ef01a32dbfb0a9a788e7795befc50cdc6396f8c2db595
6
+ metadata.gz: afa47c5b978c008d5fe46fb7399a3d1625505c8ad502d7e86e54c2f59a74b20b77077db8a31e60c70e294beeb54d51e8b808e005196c0b42f5e7e5cba0237574
7
+ data.tar.gz: b9a93c14eaeb496f1a46e78494b6129faeb8501333f678a85e8d1d07afb1a14465b32f7cc941eed294eae0bb7c319dcbbd5a0264305843394a4f1a1113a62530
data/CHANGELOG.md CHANGED
@@ -9,15 +9,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  ### Added
11
11
  - Tarball vault format ("Tarvault"): new vaults are portable tarballs with a
12
- `manifest.json` index and per-gem SHA256 integrity, with no sqlite3 dependency
13
- on the read/write path (works on JRuby). The original SQLite format
14
- ("Dbvault") is still read transparently.
12
+ plain-text `manifest` index and per-gem SHA256 integrity, with no sqlite3
13
+ dependency on the read/write path (works on JRuby). The original SQLite
14
+ format ("Dbvault") is still read transparently.
15
15
  - Vaults carry an explicit on-disk **format version**, decoupled from the gem
16
16
  version and validated on open; gemvault refuses a vault written by a newer
17
17
  gemvault instead of misreading it.
18
18
  - `gemvault upgrade` migrates a vault to the current format (e.g. SQLite → tar),
19
- preserving every gem and timestamp, writing a `.bak` backup by default, with
20
- `--dry-run` and `--no-backup` flags. It is a no-op on an already-current vault.
19
+ preserving every gem, writing a `.bak` backup by default, with `--dry-run` and
20
+ `--no-backup` flags. Timestamps survive a v1 upgrade; a v2 vault's are
21
+ restamped, its stored times living in an index this gemvault no longer reads
22
+ (see Changed). It is a no-op on an already-current vault.
21
23
 
22
24
  - All CLI commands accept `vault://` and `file://` locators wherever they take
23
25
  a vault path, e.g. `gemvault list vault:///path/to/myvault.gemv`; resolution
@@ -33,6 +35,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
33
35
  plugin.)` — Bundler reinstalls the gem correctly but validates the ghost's
34
36
  dead path — and nothing project-local can recover, because the wreck lives
35
37
  in the ambient gem home (issue #23).
38
+ - `gemvault doctor` now works in a project whose Gemfile is inline
39
+ (`bundler/inline`). Such a script keeps its plugin index in
40
+ `<project>/.bundle/plugin`, a root bundler only consults mid-script, so
41
+ doctor's uninstall silently repaired nothing and the `bundle install` that
42
+ followed dumped bundler's entire usage screen. Doctor now reaches the
43
+ project's index the way `bundler/inline` does and, with no Gemfile to
44
+ reinstall from, says so and exits 0 — the inline gemfile reinstalls the
45
+ plugin the next time the script runs (issue #14). Run outside any project,
46
+ doctor no longer claims to have cleared a project index it never touched.
47
+ - `gemvault doctor` no longer strands a machine without the plugin. Its
48
+ closing `bundle install` does more than reinstall the plugin, and when that
49
+ extra work failed (a Ruby version pin, an unresolvable gem) the already-run
50
+ uninstall had cleared the plugin index for nothing — strictly worse than
51
+ the wreck doctor was asked to repair. The index is now snapshotted before
52
+ the uninstall and restored when the reinstall never happened, `bundle
53
+ install` runs as a child rather than replacing the process, and every
54
+ failure is one line on stderr and exit 1 instead of a backtrace
55
+ (issues #27, #24).
56
+ - The shim no longer spams `already initialized constant` warnings when
57
+ bundler evaluates two installed copies of it in one process — an upgrade
58
+ resolving a newer bundler-source-vault while the plugin index still names
59
+ the old one. The first copy wins, matching the guard plugins.rb already
60
+ applies to the vault source class (issue #26).
36
61
  - `bundle plugin install bundler-source-vault` no longer dies with
37
62
  `LoadError: cannot load such file -- bundler/plugin/vault_source`. The shim's
38
63
  `plugins.rb` now derives the gem root from its own installed location (local
@@ -60,17 +85,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
60
85
  evaluation, so `Gemvault::GemEntry` can no longer be defined twice from two
61
86
  different gem roots (issue #13).
62
87
  - `gemvault new` no longer raises `cannot load such file -- json` on a stock
63
- distro ruby. json backs the tarball vault's manifest, so every current-format
64
- vault needs it; it is a default gem upstream but a separate package on
65
- distros, where `dnf install ruby` leaves it absent. It is now a declared
66
- runtime dependency.
88
+ distro ruby, where json is a default gem upstream but a separate package that
89
+ `dnf install ruby` leaves absent. json backed the tarball vault's manifest
90
+ when this was first fixed by declaring it a runtime dependency; the manifest
91
+ is now plain text and gemvault never loads json at runtime, so the failure is
92
+ retired outright rather than papered over (see Changed, issue #25).
67
93
  - Reading a vault through the Bundler source no longer raises `cannot load such
68
94
  file -- json` either. Loading gemvault off `$LOAD_PATH` skips activation, and
69
- therefore skips its dependencies, so the declared dependency alone did not
70
- reach the plugin path. The shim now resolves gemvault's declared runtime
71
- dependencies the same way it resolves gemvault and puts their require paths —
72
- extension directories included — on `$LOAD_PATH`. A dependency it cannot find
73
- is skipped rather than fatal, which is what activation could not do.
95
+ therefore skips its dependencies, so a declared dependency (json, at the time)
96
+ never reached the plugin path. The shim now resolves gemvault's declared
97
+ runtime dependencies the same way it resolves gemvault and puts their require
98
+ paths — extension directories included — on `$LOAD_PATH`; a dependency it
99
+ cannot find is skipped rather than fatal, which is what activation could not
100
+ do. The manifest being plain text, nothing on this path requires json at all
101
+ (see Changed, issue #25).
74
102
  - `bundle install` no longer fails with `Could not find 'command_kit' (~> 0.6)`
75
103
  when gemvault is installed into the plugin root without its dependencies.
76
104
  Loading the vault source no longer activates the gemvault gem, which would
@@ -78,6 +106,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
78
106
  gemvault through `require_relative` alone (issue #13).
79
107
 
80
108
  ### Changed
109
+ - The manifest is now line-oriented text rather than JSON (vault format 3).
110
+ It was never document-shaped: a header plus one whitespace-separated line
111
+ per gem — name, version, platform, stored-at, SHA256, encrypted — with no
112
+ nesting and no free text, so `tar -xOf myvault.gemv manifest` gives you
113
+ something `grep` and `awk` read directly. Because every field comes from an
114
+ alphabet without whitespace, reading needs no parser beyond `split` plus
115
+ per-field validation: no recursion to exhaust the stack, no escape grammar,
116
+ and no library to load — which is what stops gemvault from ever activating
117
+ a json gem a project had locked (issue #25). Times are stored as
118
+ `2026-08-12T18:23:32Z`; a legacy vault's `2026-08-12 18:23:32` is converted
119
+ on upgrade. A format-2 vault (`manifest.json`) stays readable: it opens
120
+ read-only through the same path a legacy SQLite vault does, and
121
+ `gemvault upgrade` migrates it. That backend never parses the old manifest —
122
+ it derives the index from the stored gems, each of which carries its own
123
+ gemspec — so migrated gems keep their identity but are restamped, the old
124
+ index's times being unrecoverable.
81
125
  - `sqlite3` is no longer a runtime dependency. Gemvault runs dependency-free on
82
126
  the tarball path (including JRuby); `sqlite3` is loaded lazily only to read a
83
127
  legacy SQLite vault, with a clear error if it is not installed.
@@ -89,10 +133,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
89
133
  `spec:integration` rake task exists alongside a non-integration `spec:core`, the
90
134
  unit job no longer needs podman, and the integration job installs podman
91
135
  when the runner image lacks it.
92
- - Reading a vault's `manifest.json` is now strict: an entry missing any required
93
- field (`name`, `version`, `platform`, `created_at`, `sha256`, `encrypted`)
94
- raises instead of silently loading `nil` fields, so a truncated or hand-edited
95
- manifest fails fast rather than yielding a subtly broken vault.
136
+ - Reading a vault's manifest is now strict: every field is validated against
137
+ its own alphabet, so a truncated or hand-edited manifest fails fast rather
138
+ than yielding a subtly broken vault.
96
139
 
97
140
  ### Deprecated
98
141
  - The SQLite vault format is deprecated and now **read-only**: existing SQLite
data/CLAUDE.md CHANGED
@@ -27,7 +27,7 @@ Do NOT modify `.rubocop.yml` or use inline `# rubocop:disable` tags without expl
27
27
 
28
28
  ## Project Overview
29
29
 
30
- Multi-gem portable archives. A single `.gemv` file is a tarball holding multiple `.gem` files plus a `manifest.json` index; legacy SQLite vaults are read-only.
30
+ Multi-gem portable archives. A single `.gemv` file is a tarball holding multiple `.gem` files plus a line-oriented `manifest` index; legacy SQLite vaults are read-only.
31
31
 
32
32
  Two gems, one repo:
33
33
 
@@ -58,7 +58,10 @@ gem install --source file:///path/to/myvault.gemv foo
58
58
  ## Architecture
59
59
 
60
60
  - `gemvault.gemspec` — main gem spec (name: `gemvault`)
61
- - `lib/gemvault/vault.rb` — Vault facade choosing a backend by file format (Tarvault current, legacy Dbvault read-only)
61
+ - `lib/gemvault/vault.rb` — Vault facade choosing a backend by file format (Tarvault current; Dbvault and LegacyTarvault read-only)
62
+ - `lib/gemvault/legacy_tarvault.rb` — read-only backend for a format-2 (manifest.json) vault; derives its index from the stored gems instead of parsing that manifest, so `gemvault upgrade` migrates one through the ordinary pipeline
63
+ - `lib/gemvault/manifest_text.rb` — the manifest's on-disk notation: a header plus one whitespace-separated line per gem, validated field by field on read
64
+ - `lib/gemvault/timestamp.rb` — the space-free notation a vault records times in, and conversion of a legacy vault's times into it
62
65
  - `lib/gemvault/cli.rb` — CLI dispatcher (new/add/list/remove/extract)
63
66
  - `lib/gemvault/ghost_specification.rb` — installation records whose gem directory is gone; swept by `gemvault doctor` (issue #23)
64
67
  - `lib/bundler/plugin/vault_source.rb` — Bundler `Plugin::API::Source` implementation
@@ -165,6 +168,6 @@ container.
165
168
 
166
169
  - `bundler` — NOT a dependency; the plugin always runs inside an existing Bundler process, and declaring it breaks gem activation under `bundle exec`'s restricted GEM_PATH
167
170
  - `command_kit` (~> 0.6) — runtime (CLI)
168
- - `json` — NOT a dependency, though it backs the tarball vault's manifest. A default gem upstream; distros that unbundle it (Fedora's `rubygem-json`) still install it alongside ruby itself, and it stays requireable even under `bundle exec`'s restricted view. Declaring it would compile a native extension on every `gem install gemvault` for no gain.
171
+ - `json` — NOT a dependency and never loaded at runtime. The manifest is a table, not a document, so `Gemvault::ManifestText` reads and writes it directly; nothing in `lib/` may `require "json"`. On rubies where require resolves through gem activation, a require inside a Bundler process activates the newest installed json and a project locking an older one dies in `check_for_activated_spec!` (issue #25).
169
172
  - `sqlite3` (~> 2.0) — NOT a runtime dependency; loaded lazily only to read a legacy SQLite (Dbvault) vault. Declared in the Gemfile for development/test.
170
173
  - `minitest`, `rspec`, `rake` — development
data/README.md CHANGED
@@ -48,23 +48,33 @@ gemvault upgrade myvault.gemv # migrate to the current format
48
48
 
49
49
  ## Vault format versioning
50
50
 
51
- Every vault records an on-disk **format version** — `1` for the original SQLite format, `2` for the current tarball format. This version lives inside the file and is **independent of the gemvault gem version**: it changes only when the storage layout changes, so upgrading the gem never invalidates your vaults.
51
+ Every vault records an on-disk **format version** — `1` for the original SQLite format, `2` for a tarball with a JSON manifest, `3` for the current tarball with a plain-text manifest. This version lives inside the file and is **independent of the gemvault gem version**: it changes only when the storage layout changes, so upgrading the gem never invalidates your vaults.
52
52
 
53
53
  gemvault reads any format up to the one it understands and **refuses a vault written by a newer gemvault** with a clear message (rather than silently misreading it).
54
54
 
55
- The SQLite format (`1`) is **deprecated and read-only**: you can still read and migrate an existing SQLite vault, but `add`/`remove` are refused and print `gemvault upgrade`. Opening one shows a one-time deprecation notice (silence it with `GEMVAULT_SILENCE_DEPRECATIONS=1`). SQLite support will be removed in a future release (0.3–0.5). To migrate a vault to the current format:
55
+ Formats `1` (SQLite) and `2` (JSON manifest) are **deprecated and read-only**: you can still list, extract from, and migrate an existing one, but `add`/`remove` are refused and print `gemvault upgrade`. Opening one shows a one-time deprecation notice (silence it with `GEMVAULT_SILENCE_DEPRECATIONS=1`). SQLite support will be removed in a future release (0.3–0.5). To migrate a vault to the current format:
56
56
 
57
57
  ```bash
58
- gemvault upgrade myvault.gemv # e.g. SQLite (v1) -> tarball (v2)
58
+ gemvault upgrade myvault.gemv # e.g. SQLite (v1) or JSON manifest (v2) -> current (v3)
59
59
  gemvault upgrade myvault.gemv --dry-run # show the plan, change nothing
60
60
  gemvault upgrade myvault.gemv --no-backup # skip the default myvault.gemv.bak copy
61
61
  ```
62
62
 
63
- `upgrade` preserves every gem and its timestamp, writes `myvault.gemv.bak` by default, and is a no-op on an already-current vault.
63
+ `upgrade` preserves every gem, writes `myvault.gemv.bak` by default, and is a no-op on an already-current vault. Timestamps survive a v1 upgrade; a v2 vault's are restamped, because its stored times live in the JSON index this gemvault deliberately no longer reads — the gems themselves are read straight out of the tarball.
64
64
 
65
65
  ## How It Works
66
66
 
67
- The gemv file is a tarball containing your .gem files and a json manifest. It has no dependencies other than the tar utilities that rubygems provides.
67
+ The gemv file is a tarball containing your .gem files and a plain-text manifest. It has no dependencies other than the tar utilities that rubygems provides.
68
+
69
+ ```console
70
+ $ tar -xOf myvault.gemv manifest
71
+ gemvault 3
72
+ created 2026-08-12T18:23:32Z
73
+
74
+ rails 7.1.3 ruby 2026-08-12T18:23:32Z 9f2c... 0
75
+ ```
76
+
77
+ One header, then one line per gem: name, version, platform, when it was stored, its SHA256, and whether it is encrypted. Every field is drawn from an alphabet without whitespace, so reading the index needs no parser beyond `split` — and `grep`, `awk` and `cut` work on it directly.
68
78
 
69
79
  When Bundler sees `type: :vault` in your Gemfile, it auto-installs the `bundler-source-vault` plugin from rubygems.org. The plugin implements the `Bundler::Plugin::API::Source` interface — it reads gemspecs from the vault, participates in dependency resolution, then extracts and installs gems from the vault's blob storage.
70
80
 
@@ -98,7 +108,11 @@ To recover, update the Gemfile to point at the new path and run:
98
108
  gemvault doctor
99
109
  ```
100
110
 
101
- `doctor` removes any ghost installation records, clears the broken entry from bundler's plugin index (`bundle plugin uninstall bundler-source-vault`) and then re-runs `bundle install`, which reinstalls the plugin against whatever the current Gemfile declares. Run it from your project directory. If a ghost record sits in a root-owned gem home, doctor says so on one line — re-run it with permissions for that gem home (e.g. `sudo gemvault doctor`).
111
+ `doctor` removes any ghost installation records, clears the broken entry from bundler's plugin index (`bundle plugin uninstall bundler-source-vault`) and then, when a Gemfile exists, re-runs `bundle install`, which reinstalls the plugin against whatever the current Gemfile declares. Run it from your project directory. If a ghost record sits in a root-owned gem home, doctor says so on one line — re-run it with permissions for that gem home (e.g. `sudo gemvault doctor`).
112
+
113
+ A project whose Gemfile is inline (`require "bundler/inline"`) works too. Such a script keeps its plugin index in `<project>/.bundle/plugin`, a root bundler only consults while the script runs; doctor reaches it the same way the script did. With no Gemfile on disk there is nothing to reinstall from, so doctor clears the entry, says so, and exits 0 — the inline gemfile reinstalls the plugin the next time the script runs. Run outside any project, doctor works against bundler's global index and points you back at the project directory for the reinstall.
114
+
115
+ The repair is transactional. `bundle install` does more than reinstall the plugin, and that extra work can fail on its own — a Ruby version pin, an unresolvable gem. When it does, doctor says so in one line and exits 1; and if the failure struck before the plugin was reinstalled, doctor puts the plugin index back the way it found it. A failed run never leaves the machine with less than it started with — fix the install error and re-run.
102
116
 
103
117
  The published `bundler-source-vault` gem installed from rubygems.org is immune to this: it lives in a bundler-managed directory that does not move.
104
118
 
data/issues.rec CHANGED
@@ -750,7 +750,42 @@ Id: 14
750
750
  Updated: Wed, 29 Jul 2026 16:41:11 -0400
751
751
  Name: gemvault doctor fails when bundler/inline is used
752
752
  Description: Loudly fails with 2 errors, one of which is "can't find gemfile"
753
- Status: open
753
+ +
754
+ + RESOLUTION: both errors reproduced in podman, and doctor turned out to be
755
+ + broken twice over rather than once.
756
+ +
757
+ + (1) "Plugin bundler-source-vault is not installed". bundler/inline sets
758
+ + BUNDLE_GEMFILE to a bare "Gemfile" for the life of the script (bundler
759
+ + inline.rb:53), so Bundler.root is the script's working directory and the plugin
760
+ + index is written to <project>/.bundle/plugin. A later `gemvault doctor` sets no
761
+ + such variable and finds no Gemfile on disk, so Bundler::Plugin.root --
762
+ + local_root only when SharedHelpers.in_bundle?, global_root otherwise
763
+ + (plugin.rb:162) -- falls back to ~/.bundle/plugin. The uninstall therefore
764
+ + looked in the wrong root and repaired nothing, silently.
765
+ +
766
+ + (2) "Could not locate Gemfile". doctor then exec'd `bundle install`, which with
767
+ + no Gemfile prints its entire usage screen and exits 10 -- reading as gemvault
768
+ + itself failing, after the repair it was asked for had already not happened.
769
+ +
770
+ + doctor now runs the uninstall with BUNDLE_GEMFILE="Gemfile" when the project
771
+ + owns a plugin root Bundler would ignore, which is bundler/inline's own trick and
772
+ + needs no file to exist; verified against the real thing (plain uninstall says
773
+ + "not installed", the same command with BUNDLE_GEMFILE set says "Uninstalled
774
+ + plugin bundler-source-vault" and empties the index). When no Gemfile exists it
775
+ + skips the reinstall and says so, exiting 0, because clearing the entry is the
776
+ + repair and only the reinstall is unavailable -- an inline gemfile reinstalls the
777
+ + plugin the next time the script runs.
778
+ +
779
+ + Gemvault::BundlerGemfile mirrors SharedHelpers#find_gemfile and
780
+ + Gemvault::BundlerPluginRoot mirrors Plugin.root, both reimplemented rather than
781
+ + delegated because bundler is deliberately not a dependency and doctor runs
782
+ + outside any Bundler process. Replicated first by
783
+ + spec/integration/cli/commands/doctor_inline_spec.rb -- all 8 examples failed
784
+ + against the old code -- with unit cover in spec/gemvault/bundler_gemfile_spec.rb,
785
+ + spec/gemvault/bundler_plugin_root_spec.rb and the extended doctor_spec.rb. The
786
+ + inline support now runs its script from the project directory, as a real user
787
+ + does; that is what puts the plugin root where the report describes.
788
+ Status: closed
754
789
 
755
790
  Id: 15
756
791
  Updated: Wed, 29 Jul 2026 17:39:23 -0400
@@ -783,7 +818,11 @@ Id: 16
783
818
  Updated: Wed, 29 Jul 2026 18:25:48 -0400
784
819
  Name: Add jruby-ci workflow
785
820
  Description: It should be already set up on the jruby-support branch. It just needs to be picked from it. DO NOT use the other code in that - it is very outdated
786
- Status: open
821
+ +
822
+ + RESOLUTION: shipped in commit 635ba41 ("ci: add JRuby test job") -- the jruby
823
+ + job in .github/workflows/ci.yml runs the minitest suite on jruby-10.1.0.0 and
824
+ + is green on CI.
825
+ Status: closed
787
826
 
788
827
  Id: 17
789
828
  Updated: Thu, 30 Jul 2026 00:07:42 +0000
@@ -972,7 +1011,15 @@ Description: [dev@rstore]/workspace% bundle install
972
1011
  + from /usr/local/bin/bundle:25:in '<main>'
973
1012
  +
974
1013
  + Though I will add that gemvault doctor followed by bundle install fixed the issue
975
- Status: open
1014
+ +
1015
+ + RESOLUTION: the broken-plugin-path wreck, recovered exactly as reported --
1016
+ + gemvault doctor then bundle install. The plugin index entry pointed into a
1017
+ + reinstalled rbenv ruby's gem home; bundler does not revalidate stored plugin
1018
+ + paths, skips loading the plugin, and crashes in SourceList#add_plugin_source,
1019
+ + upstream behavior README documents under "A broken plugin path". doctor is
1020
+ + the repair. Hinting at doctor from the failure site was considered and
1021
+ + declined: the crash happens in bundler frames before any gemvault code loads.
1022
+ Status: closed
976
1023
 
977
1024
  Id: 21
978
1025
  Updated: Fri, 31 Jul 2026 13:54:09 -0400
@@ -1011,12 +1058,27 @@ Description: bundler-cache: true on the ruby/setup-ruby action will not work cor
1011
1058
  + run: | # confine gems to vendor/bundle so the cache never shadows vendor/vendored.gemv
1012
1059
  + bundle config set --local path vendor/bundle
1013
1060
  + bundle install --jobs 4 --retry 3
1061
+ Status: open
1062
+
1063
+ Id: 26
1014
1064
  Updated: Mon, 10 Aug 2026 10:14:51 -0400
1015
1065
  Name: Gemvault spams warnings on bundle install
1016
1066
  Description: /workspace/.bundle/plugin/gems/bundler-source-vault-0.2.2/gemvault_load_path.rb:31: warning: already initialized constant BundlerSourceVault::GemvaultLoadPath::GEM
1017
1067
  + /workspace/.bundle/plugin/gems/bundler-source-vault-0.2.2/gemvault_load_path.rb:32: warning: already initialized constant BundlerSourceVault::GemvaultLoadPath::SHIM
1018
1068
  + /workspace/.bundle/plugin/gems/bundler-source-vault-0.2.2/gemvault_load_path.rb:33: warning: already initialized constant BundlerSourceVault::GemvaultLoadPath::VAULT_SOURCE
1019
- Status: open
1069
+ +
1070
+ + RESOLUTION: bundler can evaluate two installed copies of the shim in one
1071
+ + process -- an upgrade resolving a newer bundler-source-vault while the index
1072
+ + still names the old one, the dual-version plugin root the failure report
1073
+ + also recorded as issue #29 -- and require_relative cannot deduplicate
1074
+ + across paths, so the second copy reopened GemvaultLoadPath and warned on
1075
+ + every constant. gemvault_load_path.rb now returns before redefining
1076
+ + anything when the module is already loaded, first copy winning as
1077
+ + plugins.rb already arranges for Bundler::Plugin::VaultSource. Replicated by
1078
+ + spec/shim_reload_spec.rb (two copies loaded in one process, red against the
1079
+ + old shim), with the plain-install path guarded in
1080
+ + spec/integration/bundle_install_spec.rb.
1081
+ Status: closed
1020
1082
 
1021
1083
  Id: 23
1022
1084
  Updated: Mon, 10 Aug 2026 17:40:28 -0400
@@ -1086,4 +1148,133 @@ Id: 24
1086
1148
  Updated: Mon, 10 Aug 2026 23:16:35 +0000
1087
1149
  Name: doctor prints a backtrace when the plugin uninstall step fails
1088
1150
  Description: gemvault doctor's `bundle plugin uninstall` step runs with exception: true; a nonzero exit raises RuntimeError and a missing bundle raises Errno::ENOENT, and both escape Command#run to command_kit's ExceptionHandler, which prints error.full_message -- a full backtrace -- violating the one-line-and-exit-1 convention issue #15 established. Adjacent to issue #14 (doctor failing under bundler/inline). The ghost-specification sweep added for issue #23 reports its own failures as one line; the uninstall step should match.
1151
+ +
1152
+ + RESOLUTION: fixed alongside issue #27's transactional rework. The uninstall
1153
+ + step no longer runs with exception: true; a failing or missing bundle is one
1154
+ + line on stderr ("doctor: bundle plugin uninstall bundler-source-vault
1155
+ + failed") and exit 1, matching the convention issues #15 and #23 established,
1156
+ + and the snapshotted index is left untouched.
1157
+ Status: closed
1158
+
1159
+ Id: 25
1160
+ Updated: Tue, 11 Aug 2026 20:05:27 -0400
1161
+ Name: Project with Gemfile + locked json version + gemvault throws error
1162
+ Description: Project with Gemfile + locked json version + gemvault throws "bundler: failed to load command: rails (/private/tmp/local/flipmine/flipmine/vendor/ruby/4.0.0/bin/rails)
1163
+ + /Users/davidgillis/.rbenv/versions/4.0.1/lib/ruby/gems/4.0.0/gems/bundler-4.0.18/lib/bundler/runtime.rb:328:in 'Bundler::Runtime#check_for_activated_spec!': You have already activated json 2.21.2, but your Gemfile requires json 2.19.7. Prepending `bundle exec` to your command may solve this. (Gem::LoadError)"
1164
+ +
1165
+ + I believe this error to be coming from gemvault's dependency on JSON. It is clear that dependencies are cursed in this project, whether its the added load time for reinstalling them on every `bundle install`, or errors like this one. Rather than rely on JSON, I think it would be better to just marshal our own manifest, or use some other serialization tool if any that rubygems already provides.
1166
+ +
1167
+ + RESOLUTION: gemvault no longer loads json, and the manifest is no longer a
1168
+ + JSON document. Whether gemvault's require was the activator on the reported
1169
+ + machine cannot be settled from here -- on the Fedora reproduction stack
1170
+ + require resolves json from the stdlib path with no activation at all, and
1171
+ + bundler loads json itself before the vault source runs -- but on rbenv-style
1172
+ + stacks require does activate, gemvault held a live require in its
1173
+ + vault-read path, and it can no longer be the component that detonates
1174
+ + check_for_activated_spec!.
1175
+ +
1176
+ + Marshal was considered as the reporter suggested and rejected: a .gemv is a
1177
+ + file that arrives from elsewhere, the manifest is the root of trust (the
1178
+ + SHA256 digests live inside it, so nothing verifies it first), and
1179
+ + Marshal.load runs attacker-chosen code during load itself -- demonstrated
1180
+ + here, a hash key's #hash fires inside load before the caller touches the
1181
+ + result. `gemvault list` on an untrusted vault would become arbitrary code
1182
+ + execution, and Marshal's versioned wire format would break the portability
1183
+ + a .gemv exists for.
1184
+ +
1185
+ + What replaced JSON is neither: the manifest was never document-shaped. It is
1186
+ + a table -- a header plus fixed-arity records of scalars, no nesting, no free
1187
+ + text -- and every field is drawn from an alphabet without whitespace once
1188
+ + times are canonicalized (Gemvault::Timestamp; rubygems itself validates gem
1189
+ + names against /\A[a-zA-Z0-9._-]+\z/). Gemvault::ManifestText writes and
1190
+ + reads it as lines. There is no recursion to exhaust the stack, no escape
1191
+ + grammar, no backtracking, and no library to load -- the same reasoning that
1192
+ + keeps BundlerPluginIndex from requiring yaml. Reading validates every field
1193
+ + and rejects everything else, because the file comes from elsewhere; writing
1194
+ + trusts the value objects this library constructed. Precedent is rubygems'
1195
+ + own compact index and Gemfile.lock, both line-oriented text.
1196
+ +
1197
+ + Format version 3; vaults are inspectable with `tar -xOf v.gemv manifest`.
1198
+ +
1199
+ + An existing format-2 vault keeps working. Gemvault::LegacyTarvault opens one
1200
+ + read-only, exactly as Dbvault does for SQLite, so `gemvault upgrade` migrates
1201
+ + it through the ordinary pipeline rather than leaving its gems stranded behind
1202
+ + an error. That backend never parses manifest.json: it derives the index from
1203
+ + the payload, since every stored .gem carries the gemspec the manifest's
1204
+ + identity fields came from -- which keeps the old notation from re-entering
1205
+ + the codebase and makes the reader indifferent to a damaged manifest. Two
1206
+ + things the old index held are lost with it: each gem's stored time (entries
1207
+ + are restamped on open, and the deprecation notice says so) and the per-gem
1208
+ + digests (the new vault records fresh digests of exactly the bytes it copied).
1209
+ + Covered end to end by spec/integration/cli/commands/legacy_tarvault_spec.rb
1210
+ + against a committed format-2 fixture: list, extract, refused add, upgrade,
1211
+ + and writing to the upgraded vault.
1212
+ +
1213
+ + Settled after all: the Fedora image reproduces the rbenv-style stack once
1214
+ + the unbundled stdlib json is cleared away -- require then resolves through
1215
+ + gem activation, exactly the reported semantics.
1216
+ + spec/integration/locked_json_spec.rb runs the reporter's flow (a project
1217
+ + locking an older json than the machine's newest, gemvault ambient, bundle
1218
+ + exec through the vault source) and fails against a gemvault that requires
1219
+ + json on either the CLI path or the plugin path, the latter dying with the
1220
+ + reported check_for_activated_spec! error verbatim.
1221
+ Status: closed
1222
+
1223
+ Id: 27
1224
+ Updated: Wed, 12 Aug 2026 16:05:55 +0000
1225
+ Name: gemvault doctor can leave the machine with no plugin at all
1226
+ Description: Reported from a container where bundle install could not succeed
1227
+ + (ruby version pin mismatch): doctor removed ghost records, uninstalled the
1228
+ + plugin, emptied .bundle/plugin/index, then exec'd bundle install -- which
1229
+ + failed, leaving no plugin installed. Strictly worse than the broken-path
1230
+ + state doctor was invoked to repair, and recoverable only by knowing to run
1231
+ + `bundle plugin install bundler-source-vault` by hand. doctor repairs by
1232
+ + mutation with no dry-run, no report of findings before acting, and no
1233
+ + rollback when the reinstall it hands off to fails. Suggested in the report:
1234
+ + report-then-fix (--dry-run, or report by default and repair under --fix);
1235
+ + reinstall the plugin directly with `bundle plugin install` and verify the
1236
+ + index rather than exec'ing into `bundle install`, so a failed dependency
1237
+ + install cannot take the plugin down with it; or restore the previous index
1238
+ + when the step after the uninstall fails.
1239
+ +
1240
+ + RESOLUTION: the uninstall-reinstall pair is now transactional. doctor
1241
+ + snapshots the plugin index bundler will consult (Gemvault::BundlerPluginIndex
1242
+ + over BundlerPluginRoot#consulted) before clearing it, runs `bundle install`
1243
+ + as a child instead of exec'ing into it, and on failure checks whether the
1244
+ + plugin came back: reinstalled means the repair stands and the one-line error
1245
+ + says only the bundle is unfinished; not reinstalled means the snapshot is
1246
+ + restored, so the machine is left as found and re-running doctor once the
1247
+ + install error is fixed recovers fully. Replicated first by
1248
+ + spec/integration/cli/commands/doctor_reinstall_failure_spec.rb -- doctor run
1249
+ + against a Gemfile still naming a renamed plugin path, an un-installable
1250
+ + bundle in the reporter's shape -- which fails against the old code and
1251
+ + proves the restored index heals on the next run. Unit cover in
1252
+ + spec/gemvault/bundler_plugin_index_spec.rb and the reworked doctor_spec.rb.
1253
+ Status: closed
1254
+
1255
+ Id: 28
1256
+ Updated: Wed, 12 Aug 2026 16:05:55 +0000
1257
+ Name: plugins.rb dies with a bare LoadError when gemvault is installed nowhere
1258
+ Description: A plugin root holding only the shim -- the normal state wherever
1259
+ + `gem install gemvault` has run, since Bundler skips installing a plugin
1260
+ + dependency already present -- stops loading when the tree lands on a machine
1261
+ + with no gemvault in any gem root: GemvaultLoadPath.entries returns [] and
1262
+ + `require "bundler/plugin/vault_source"` raises a bare LoadError with no hint.
1263
+ + Observed after a macOS working tree was copied into a Linux container (the
1264
+ + shim alone in .bundle/plugin, gemvault absent from every gem root on the
1265
+ + box); also reachable by `gem uninstall gemvault` on a single machine. The
1266
+ + ambient-root search cannot cover gemvault being truly absent. Suggested:
1267
+ + fail with an explicit "gemvault not found in any gem root" message naming
1268
+ + the remedy (gem install gemvault, or bundle plugin uninstall + install to
1269
+ + get a self-contained root), or vendor gemvault into the shim.
1270
+ Status: open
1271
+
1272
+ Id: 29
1273
+ Updated: Wed, 12 Aug 2026 16:05:55 +0000
1274
+ Name: Stale plugin versions are never reaped from the plugin root
1275
+ Description: bundler-source-vault-0.2.4 survived in the plugin root's gems/,
1276
+ + specifications/ and cache/ across the 0.2.5 upgrade and an explicit `bundle
1277
+ + plugin uninstall`; only 0.2.5 was ever referenced by the index. Possibly
1278
+ + Bundler's doing rather than the plugin's, but doctor is well placed to sweep
1279
+ + plugin-root versions the index no longer references.
1089
1280
  Status: open
@@ -0,0 +1,63 @@
1
+ require "pathname"
2
+
3
+ module Gemvault
4
+ ##
5
+ # The Gemfile Bundler would load from a directory.
6
+ #
7
+ # Mirrors Bundler::SharedHelpers#find_gemfile: <tt>BUNDLE_GEMFILE</tt> wins
8
+ # when set and non-empty, otherwise the search walks up looking for
9
+ # <tt>gems.rb</tt> then <tt>Gemfile</tt> in each directory. Reimplemented
10
+ # rather than delegated because bundler is deliberately not a gemvault
11
+ # dependency (see gemvault.gemspec) and +gemvault doctor+ runs as a plain
12
+ # CLI, outside any Bundler process.
13
+ #
14
+ # One difference is deliberate. Bundler returns <tt>BUNDLE_GEMFILE</tt>
15
+ # whatever it points at, existing or not -- bundler/inline relies on that,
16
+ # setting it to a bare "Gemfile" purely to make Bundler treat the working
17
+ # directory as the app root. The question here is whether +bundle install+
18
+ # could work, so a value that does not name a file is ignored and the
19
+ # walk-up search decides instead.
20
+ class BundlerGemfile
21
+ # Checked in this order within each directory, as Bundler checks them.
22
+ NAMES = ["gems.rb", "Gemfile"].freeze
23
+
24
+ def initialize(dir: Dir.pwd, env: ENV)
25
+ @dir = Pathname(dir)
26
+ @env = env
27
+ end
28
+
29
+ # :call-seq:
30
+ # path -> Pathname or nil
31
+ #
32
+ # The Gemfile Bundler would load, or +nil+ when there is none.
33
+ def path
34
+ return @path if defined?(@path)
35
+
36
+ @path = from_env || search_up
37
+ end
38
+
39
+ # Whether Bundler would find a Gemfile at all.
40
+ def exist?
41
+ !path.nil?
42
+ end
43
+
44
+ private
45
+
46
+ def from_env
47
+ given = @env["BUNDLE_GEMFILE"].to_s
48
+ return nil if given.empty?
49
+
50
+ candidate = Pathname(given).expand_path(@dir)
51
+ candidate.file? ? candidate : nil
52
+ end
53
+
54
+ def search_up
55
+ @dir.expand_path.ascend do |dir|
56
+ found = NAMES.map { |name| dir / name }.find(&:file?)
57
+ return found if found
58
+ end
59
+
60
+ nil
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,58 @@
1
+ require "pathname"
2
+
3
+ module Gemvault
4
+ ##
5
+ # Bundler's plugin index file inside a plugin root.
6
+ #
7
+ # The uninstall step of +gemvault doctor+ clears entries from this file, and
8
+ # the reinstall that follows can fail for reasons unrelated to the plugin.
9
+ # Snapshot and restore make that pair transactional: a repair that cannot
10
+ # finish puts the index back instead of leaving the machine with no plugin
11
+ # registered at all (issue #27).
12
+ #
13
+ # The plugin_paths section is scanned textually rather than parsed as YAML:
14
+ # loading a YAML library into doctor's process invites the same
15
+ # stdlib-activation conflicts as issue #25, and bundler's own emitter writes
16
+ # one two-space-indented "name: path" line per plugin.
17
+ class BundlerPluginIndex
18
+ def initialize(root)
19
+ @root = Pathname(root)
20
+ end
21
+
22
+ def file
23
+ @root / "index"
24
+ end
25
+
26
+ # Whether plugin_paths currently lists +plugin+.
27
+ def registered?(plugin)
28
+ plugin_paths.any? { |line| line.match?(/\A\s{2}#{Regexp.escape(plugin)}:/) }
29
+ end
30
+
31
+ # :call-seq:
32
+ # snapshot -> String or nil
33
+ #
34
+ # The index content as it stands, +nil+ when no index exists.
35
+ def snapshot
36
+ file.file? ? file.read : nil
37
+ end
38
+
39
+ # Puts the index back the way +snapshot+ recorded it; restoring +nil+
40
+ # removes an index that did not exist at snapshot time.
41
+ def restore(snapshot)
42
+ return file.write(snapshot) if snapshot
43
+
44
+ file.delete if file.exist?
45
+ end
46
+
47
+ private
48
+
49
+ def plugin_paths
50
+ return [] unless file.file?
51
+
52
+ file.readlines
53
+ .drop_while { |line| !line.start_with?("plugin_paths:") }
54
+ .drop(1)
55
+ .take_while { |line| line.start_with?(" ") }
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,59 @@
1
+ require "pathname"
2
+ require_relative "bundler_gemfile"
3
+
4
+ module Gemvault
5
+ ##
6
+ # Where Bundler keeps plugin data for a directory.
7
+ #
8
+ # Bundler::Plugin.root answers the project-local <tt>.bundle/plugin</tt> when
9
+ # <tt>SharedHelpers.in_bundle?</tt> holds, and the global
10
+ # <tt>~/.bundle/plugin</tt> when it does not. Since +in_bundle?+ is just
11
+ # "was a Gemfile found", a project can own a plugin root that Bundler
12
+ # declines to look in.
13
+ class BundlerPluginRoot
14
+ LOCAL_DIR = ".bundle/plugin".freeze
15
+
16
+ def initialize(dir: Dir.pwd, gemfile: BundlerGemfile.new(dir: dir), env: ENV)
17
+ @dir = Pathname(dir)
18
+ @gemfile = gemfile
19
+ @env = env
20
+ end
21
+
22
+ # The project's own plugin root, whether or not Bundler would consult it.
23
+ def local
24
+ @dir.expand_path / LOCAL_DIR
25
+ end
26
+
27
+ # Where Bundler keeps plugins for a user outside any project, mirroring
28
+ # Bundler's user_bundle_path lookup for plugins: <tt>BUNDLE_USER_PLUGIN</tt>
29
+ # names the root directly, <tt>BUNDLE_USER_HOME</tt> relocates
30
+ # <tt>.bundle</tt>, and the home directory is the default.
31
+ def global
32
+ named = @env["BUNDLE_USER_PLUGIN"]
33
+ return Pathname(named) if named
34
+
35
+ Pathname(@env["BUNDLE_USER_HOME"] || File.join(Dir.home, ".bundle")) / "plugin"
36
+ end
37
+
38
+ # The plugin root Bundler will consult here: beside the Gemfile when one
39
+ # was found, the project's own root when doctor points Bundler at it (see
40
+ # #unreachable?), the global root otherwise.
41
+ def consulted
42
+ return @gemfile.path.dirname / LOCAL_DIR if @gemfile.exist?
43
+ return local if unreachable?
44
+
45
+ global
46
+ end
47
+
48
+ # Whether this project has a plugin root that Bundler currently ignores.
49
+ #
50
+ # bundler/inline installs plugins into <tt><cwd>/.bundle/plugin</tt>, having
51
+ # set <tt>BUNDLE_GEMFILE</tt> to a bare "Gemfile" for the duration of the
52
+ # script. A later command in that same directory sets no such variable and
53
+ # finds no Gemfile on disk, so Bundler falls back to the global root and
54
+ # reports the plugin as not installed -- while the entry sits here.
55
+ def unreachable?
56
+ !@gemfile.exist? && local.directory?
57
+ end
58
+ end
59
+ end