gemvault 0.1.1 → 0.1.3

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: 7de6dc35280e873a80b18d4619ef13b789525cc2e071417acbd80fe65d040ada
4
- data.tar.gz: 8520005cc49b5eead43706f03c622063b330c5e391ab3d8966c6e69b4482d267
3
+ metadata.gz: 0e990085de84c0a510129de569ba8ed103bb12d78dc5cb2a6b666eb4a9e8303d
4
+ data.tar.gz: 7c37b0d7ada34a481ed270185a120509a90d296c1bee01a596803e0e33d0a149
5
5
  SHA512:
6
- metadata.gz: 34f5e6485abe04eaed2f39b10bf7394b89d7e7f0b3f3bd8b51b60aeeed43e140ef1b4c43e20ed0f73d6f8f60f81dcdd98eb818af67d17297db655bd239535d03
7
- data.tar.gz: 52dc205f6c9f989a52881d14ebb828ab0cd8733bfad990db472307393ca62cac444ee539070f95efac91e447e4fce319730d83cb058ad697e44867a2a3876f62
6
+ metadata.gz: b3e09ff83f50247af6f394a7e1cae36387ddeb5de859c7ef845c024d13bc9062cff800f9061202e3af8ca3bffc6dfbc2e9cc7029986f726bd6bfa1221ca9e80b
7
+ data.tar.gz: 7d864a45342f84b6b82f0e168c8f9c2e9378bae0fe5fe4ff46bf2c5a454dfdda8f3c90f8446b82fad61caecb8ab136f385a2e6fd85de66829cad3f144141a416
data/.rubocop.yml CHANGED
@@ -146,6 +146,7 @@ Style/Documentation:
146
146
  Enabled: true
147
147
  Exclude:
148
148
  - "spec/**/*"
149
+ - "test/**/*"
149
150
 
150
151
  # Trailing commas in multiline literals and arguments.
151
152
  Style/TrailingCommaInArrayLiteral:
data/CHANGELOG.md ADDED
@@ -0,0 +1,29 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.3] - 2026-06-22
9
+
10
+ ### Added
11
+ - `gemvault doctor` command to recover from a broken `bundler-source-vault`
12
+ plugin index entry after its path-installed source directory has been renamed
13
+ (issue #1).
14
+
15
+ ### Fixed
16
+ - `gem install --source vault://<absolute-path>` no longer resolves the vault
17
+ relative to the working directory; the `vault://` scheme is now stripped just
18
+ like `file://` (issue #5).
19
+ - `bundle install` logs now show the vault path as written in the Gemfile
20
+ (e.g. `vendor/vendored.gemv`) instead of only the basename (issue #3).
21
+ - Renaming a `.gemv` file and updating the Gemfile to match no longer crashes
22
+ `bundle install` on the stale lockfile entry; the vault existence check is
23
+ deferred until the source is actually queried (issue #2).
24
+
25
+ ### Changed
26
+ - CI runs the unit and container-backed integration suites as separate jobs, and
27
+ the codebase is RuboCop-clean.
28
+
29
+ [0.1.3]: https://github.com/gillisd/gemvault/releases/tag/v0.1.3
data/CLAUDE.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # CLAUDE.md — Gemvault
2
2
 
3
+ ## Rubocop
4
+
5
+ Do NOT modify `.rubocop.yml` or use inline `# rubocop:disable` tags without explicit permission. Fix the code to satisfy the cop instead.
6
+
7
+ ## General rules
8
+
9
+ 1. Specs always come first. Every plan should start with the skeleton of the BDD specs being added, changed or removed. Skeleton means the empty RSpec language, without implementation.
10
+ 2. Specs should never have comments. Any urge to put a comment in a spec means that comment should probably be its own spec
11
+ 3. DO NOT edit .rubocop.yml or add inline rubocop exemptions without explicit permission
12
+ 4. DO NOT run any git command that will rewrite history without explicit permission
13
+ 5. PREFER method & class extraction over comments
14
+ 6. Making new files, classes, modules, and methods IS NOT overengineering
15
+ 7. BEFORE writing code, identify which domain concept owns the behavior. Each class and module should have a single responsibility. If the new behavior doesn't fit an existing class's responsibility, create a new one — don't expand the scope of what's already there.
16
+ 8. DO NOT name classes with suffixes like "-er" or "-or" unless using a canonical pattern name (e.g., Parser, Router, Controller)
17
+ 9. ALWAYS write specs first. The workflow is: identify the domain concept (rule 5), write specs describing its behavior, then implement. No implementation without a failing spec.
18
+ 10. Integration specs are the first line of defense for CLI-tool bugs. For any bug reported from using the CLI tool (not the gemvault lib / Ruby API), the FIRST spec you write is an integration spec that reproduces the user's exact invocation — real subprocess, real vault, real exit code. Stub-heavy unit specs are complementary, not sufficient: they prove internal logic produces the expected value assuming surrounding wiring works, but a user's bug report is evidence the wiring didn't work.
19
+ 11. If an integration spec is not catching a reported CLI-tool bug, one of two things is true, and the fix starts by diagnosing which: (a) existing integration specs are not specific enough — extend them to cover the exact scenario before touching production code; or (b) the scenario is not spec'd at all, which means the work is not a bug fix but a new feature — write integration specs for the contract first (per rule 1), then implement.
20
+
21
+ ## Additional rules
22
+
23
+ 1. NEVER use Ruby's `sleep` method
24
+ 2. NEVER create any class ending in "er" or "or"
25
+
3
26
  ## Project Overview
4
27
 
5
28
  Multi-gem portable archives backed by SQLite. A single `.gemv` file contains multiple `.gem` files.
@@ -57,7 +80,7 @@ gem install --source file:///path/to/myvault.gemv foo
57
80
  ## Testing
58
81
 
59
82
  ```bash
60
- bundle exec rake test # 122 tests, 289 assertions
83
+ bundle exec rake test
61
84
  ```
62
85
 
63
86
  - `test/vault_test.rb` — 33 unit tests for Vault class
data/README.md CHANGED
@@ -58,6 +58,27 @@ When Bundler sees `type: :vault` in your Gemfile, it auto-installs the `bundler-
58
58
 
59
59
  The RubyGems plugin works similarly: `gem install --source vault.gemv` loads specs and extracts gems on demand.
60
60
 
61
+ ## Recovering from a broken bundler plugin path: `gemvault doctor`
62
+
63
+ If you installed `bundler-source-vault` from a local path (e.g. `plugin "bundler-source-vault", path: "/path/to/gemvault"` in a Gemfile), bundler records that absolute path in its plugin index. Moving, renaming, or deleting the source directory afterwards invalidates the stored path, and the next `bundle install` prints:
64
+
65
+ ```
66
+ The following plugin paths don't exist: /path/to/gemvault/shim/.
67
+ Continuing without installing plugin bundler-source-vault.
68
+ ```
69
+
70
+ Once the plugin skips loading, bundler crashes with `NoMethodError: undefined method 'new' for nil` on any Gemfile that uses `type: :vault`. This is a bundler limitation — the plugin index isn't revalidated against the filesystem, and there's no plugin-side hook that fires early enough to preempt it.
71
+
72
+ To recover, update the Gemfile to point at the new path and run:
73
+
74
+ ```bash
75
+ gemvault doctor
76
+ ```
77
+
78
+ `doctor` 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.
79
+
80
+ 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.
81
+
61
82
  ## Development
62
83
 
63
84
  ```bash
data/Rakefile CHANGED
@@ -8,7 +8,9 @@ end
8
8
 
9
9
  require "rspec/core/rake_task"
10
10
 
11
- RSpec::Core::RakeTask.new(:spec)
11
+ RSpec::Core::RakeTask.new(:spec) do |t|
12
+ t.exclude_pattern = "spec/integration/**/*_spec.rb"
13
+ end
12
14
 
13
15
  require "rubocop/rake_task"
14
16
  RuboCop::RakeTask.new
@@ -27,6 +29,11 @@ namespace :spec do
27
29
  "-f", "Dockerfile.test",
28
30
  "."
29
31
  end
32
+
33
+ desc "Run container-backed integration specs (requires Podman; run spec:build first)"
34
+ RSpec::Core::RakeTask.new(:integration) do |t|
35
+ t.pattern = "spec/integration/**/*_spec.rb"
36
+ end
30
37
  end
31
38
 
32
39
  namespace :shim do
data/issues.rec ADDED
@@ -0,0 +1,160 @@
1
+ %rec: Issue
2
+ %key: Id
3
+ %typedef: text_t regexp /^.*$/
4
+ %typedef: Status_t enum open in_progress closed
5
+ %type: Id int
6
+ %type: Name line
7
+ %type: Description text_t
8
+ %type: Status Status_t
9
+ %type: Updated date
10
+ %auto: Id Updated
11
+ %mandatory: Name Description Status
12
+
13
+ Id: 0
14
+ Updated: Wed, 22 Apr 2026 13:07:40 +0000
15
+ Name: removal of gems is not working
16
+ Description: Removal of gems when version is specified is broken. try the following:
17
+ +
18
+ + ```bash
19
+ + gemvault remove vault.gemv somegemwithversion-1.0.1
20
+ + ```
21
+ +
22
+ + it will raise a not found error
23
+ Status: closed
24
+
25
+ Id: 1
26
+ Updated: Tue, 21 Apr 2026 23:28:44 -0400
27
+ Name: Getting error when dir structure has slightly changed
28
+ Description: Installing bundler-source-vault 0.1.2
29
+ + The following plugin paths don't exist: /workspace/gemvault/shim/..
30
+ +
31
+ + This can happen if the plugin was installed with a different version of Ruby that has since been uninstalled.
32
+ +
33
+ + If you would like to reinstall the plugin, run:
34
+ +
35
+ + bundler plugin uninstall bundler-source-vault && bundler plugin install bundler-source-vault
36
+ +
37
+ + Continuing without installing plugin bundler-source-vault.
38
+ + /Users/davidgillis/.rbenv/versions/4.0.1/lib/ruby/gems/4.0.0/gems/bundler-4.0.8/lib/bundler/source_list.rb:59:in 'Bundler::SourceList#add_plugin_source': undefined method 'new' for nil (NoMethodError)
39
+ +
40
+ + add_source_to_list Plugin.source(source).new(options), @plugin_sources
41
+ + ^^^^
42
+ + from /Users/davidgillis/.rbenv/versions/4.0.1/lib/ruby/gems/4.0.0/gems/bundler-4.0.8/lib/bundler/dsl.rb:131:in 'Bundler::Dsl#source'
43
+ + from /Users/davidgillis/repos/tries/2026-03-27-5misc/fooz/Gemfile:5:in 'block in Bundler::Dsl#eval_gemfile'
44
+ + from /Users/davidgillis/.rbenv/versions/4.0.1/lib/ruby/gems/4.0.0/gems/bundler-4.0.8/lib/bundler/dsl.rb:49:in 'BasicObject#instance_eval'
45
+ + from /Users/davidgillis/.rbenv/versions/4.0.1/lib/ruby/gems/4.0.0/gems/bundler-4.0.8/lib/bundler/dsl.rb:49:in 'block in Bundler::Dsl#eval_gemfile'
46
+ + from /Users/davidgillis/.rbenv/versions/4.0.1/lib/ruby/gems/4.0.0/gems/bundler-4.0.8/lib/bundler/dsl.rb:326:in 'Bundler::Dsl#with_gemfile'
47
+ + from /Users/davidgillis/.rbenv/versions/4.0.1/lib/ruby/gems/4.0.0/gems/bundler-4.0.8/lib/bundler/dsl.rb:47:in 'Bundler::Dsl#eval_gemfile'
48
+ + from /Users/davidgillis/.rbenv/versions/4.0.1/lib/ruby/gems/4.0.0/gems/bundler-4.0.8/lib/bundler/dsl.rb:13:in 'Bundler::Dsl.evaluate'
49
+ + from /Users/davidgillis/.rbenv/versions/4.0.1/lib/ruby/gems/4.0.0/gems/bundler-4.0.8/lib/bundler/definition.rb:40:in 'Bundler::Definition.build'
50
+ + from /Users/davidgillis/.rbenv/versions/4.0.1/lib/ruby/gems/4.0.0/gems/bundler-4.0.8/lib/bundler.rb:234:in 'Bundler.definition'
51
+ + from /Users/davidgillis/.rbenv/versions/4.0.1/lib/ruby/gems/4.0.0/gems/bundler-4.0.8/lib/bundler/env.rb:36:in 'Bundler::Env.report'
52
+ + from /Users/davidgillis/.rbenv/versions/4.0.1/lib/ruby/gems/4.0.0/gems/bundler-4.0.8/lib/bundler/friendly_errors.rb:71:in 'Bundler::FriendlyErrors.request_issue_report_for'
53
+ + from /Users/davidgillis/.rbenv/versions/4.0.1/lib/ruby/gems/4.0.0/gems/bundler-4.0.8/lib/bundler/friendly_errors.rb:50:in 'Bundler::FriendlyErrors.log_error'
54
+ + from /Users/davidgillis/.rbenv/versions/4.0.1/lib/ruby/gems/4.0.0/gems/bundler-4.0.8/lib/bundler/friendly_errors.rb:124:in 'Bundler.with_friendly_errors'
55
+ + from /Users/davidgillis/.rbenv/versions/4.0.1/lib/ruby/gems/4.0.0/gems/bundler-4.0.8/exe/bundle:20:in '<top (required)>'
56
+ + from /Users/davidgillis/.rbenv/versions/4.0.1/lib/ruby/site_ruby/4.0.0/rubygems.rb:304:in 'Kernel#load'
57
+ + from /Users/davidgillis/.rbenv/versions/4.0.1/lib/ruby/site_ruby/4.0.0/rubygems.rb:304:in 'Gem.activate_and_load_bin_path'
58
+ + from /Users/davidgillis/.rbenv/versions/4.0.1/bin/bundle:25:in '<main>'
59
+ + /Users/davidgillis/.rbenv/versions/4.0.1/lib/ruby/gems/4.0.0/gems/bundler-4.0.8/lib/bundler/source_list.rb:59:in 'Bundler::SourceList#add_plugin_source': undefined method 'new' for nil (NoMethodError)
60
+ +
61
+ + add_source_to_list Plugin.source(source).new(options), @plugin_sources
62
+ Status: open
63
+
64
+ Id: 2
65
+ Updated: Tue, 21 Apr 2026 23:31:42 -0400
66
+ Name: Renaming gemv file breaks bundler config
67
+ Description: Steps to replicate:
68
+ + 1. add vault source with some gems
69
+ + 2. move gemv file to another directory in project, rename, and update Gemfile to reflect
70
+ + 3. Run bundle install
71
+ + 4. An error is raised that looks something like
72
+ + ```bash
73
+ + [dev@5misc2]/workspace/fooz% bundle install
74
+ + Fetching gem metadata from https://rubygems.org/.......
75
+ + Resolving dependencies...
76
+ + Installing command_kit 0.6.0
77
+ + Installing sqlite3 2.9.3 (aarch64-linux-gnu)
78
+ + Installing gemvault 0.1.2
79
+ + Installing bundler-source-vault 0.1.2
80
+ + Could not find vault 'vendored.gemv' referenced in Gemfile (relative to /workspace/fooz)
81
+ + ```
82
+ +
83
+ + Where vendored.gemv is the name of the old file, and there is still an entry in Gemfile.lock
84
+ Status: open
85
+
86
+ Id: 3
87
+ Updated: Tue, 21 Apr 2026 23:42:59 -0400
88
+ Name: Name of vault in logs should show relative path
89
+ Description: This vault is in vendor/vendored_gems.gmv, relative to the Gemfile
90
+ + in the logs of bundle install I see:
91
+ +
92
+ + ```
93
+ + Installing acme 1.0.1 from vault vendored_gems.gemv
94
+ + ```
95
+ Status: open
96
+
97
+ Id: 4
98
+ Updated: Wed, 22 Apr 2026 12:16:44 -0400
99
+ Name: Running bundle install reinstalls gemvault deps every single run
100
+ Description: Anytime I run bundle install, I always see:
101
+ +
102
+ + ```
103
+ + Fetching gem metadata from https://rubygems.org/.......
104
+ + Resolving dependencies...
105
+ + Installing command_kit 0.6.0
106
+ + Installing sqlite3 2.9.3 (aarch64-linux-gnu)
107
+ + Installing gemvault 0.1.2
108
+ + Installing bundler-source-vault 0.1.2
109
+ + ```
110
+ +
111
+ + This is unacceptable. these should only need to be installed once
112
+ Status: open
113
+
114
+ Id: 5
115
+ Updated: Wed, 22 Apr 2026 18:24:09 -0400
116
+ Name: Installing gem via rubygems fails to resolve absolute path
117
+ Description: Consider the following:
118
+ + ```
119
+ + ng command gem install --source vault:///Users/davidgillis/repos/flipmine/vendor/vendored_gems.gemv acme
120
+ + ERROR: While executing gem ... (Gemvault::Vault::NotFoundError)
121
+ + Vault not found: /private/tmp/local/ddog/vault:/Users/davidgillis/repos/flipmine/vendor/vendored_gems.gemv
122
+ + ```
123
+ +
124
+ + Notice an absolute path was passed to --source, yet it tried to resolve locally
125
+ Status: open
126
+
127
+ Id: 6
128
+ Updated: Thu, 23 Apr 2026 11:04:39 -0400
129
+ Name: Cannot add gem with non-numerical suffix
130
+ Description: When adding a gem like 'ronin-db-0.2.1.patch1.gem', gemvault successfully adds the gem, but the gem install command fails:
131
+ + ```
132
+ + gemvault add myvault.gemv ronin-db-0.2.1.patch1.gem # success
133
+ + gem install --debug --verbose --clear-sources --source file://myvault.gemv ronin-db # failure
134
+ + ```
135
+ Status: open
136
+
137
+ Id: 7
138
+ Updated: Fri, 19 Jun 2026 10:42:46 -0400
139
+ Name: Gemvault pollutes project dir with plugin files in .bundle
140
+ Description: Why is it not using the plugin that is already installed globally?
141
+ Status: open
142
+
143
+ Id: 8
144
+ Updated: Wed, 22 Apr 2026 18:24:09 -0400
145
+ Name: Installing gem via rubygems fails to resolve absolute path
146
+ Description: Consider the following:
147
+ + ```
148
+ + ng command gem install --source vault:///Users/davidgillis/repos/flipmine/vendor/vendored_gems.gemv acme
149
+ + ERROR: While executing gem ... (Gemvault::Vault::NotFoundError)
150
+ + Vault not found: /private/tmp/local/ddog/vault:/Users/davidgillis/repos/flipmine/vendor/vendored_gems.gemv
151
+ + ```
152
+ +
153
+ + Notice an absolute path was passed to --source, yet it tried to resolve locally
154
+ Status: open
155
+
156
+ Id: 9
157
+ Updated: Sun, 28 Jun 2026 00:17:27 -0400
158
+ Name: Gemvault commands should also be able to accept a uri
159
+ Description: e.g. gemvault list vault:///Users/davidgillis/repos/vault/rubylib/myvault.gemv
160
+ Status: open
@@ -3,72 +3,27 @@ require_relative "../../gemvault/vault"
3
3
 
4
4
  module Bundler
5
5
  module Plugin
6
+ # Bundler plugin source that installs gems from a .gemv vault file.
6
7
  class VaultSource
7
8
  def initialize(opts)
8
9
  super
9
10
  @vault_path = resolve_vault_path(@uri)
10
- validate_vault_exists!
11
11
  end
12
12
 
13
13
  def fetch_gemspec_files
14
- gemspec_files = []
14
+ validate_vault_exists!
15
15
 
16
16
  Gemvault::Vault.open(@vault_path) do |vault|
17
- vault.gem_entries.each do |entry|
18
- spec = vault.spec_from_blob(entry.name, entry.version, entry.platform)
19
- full_name = spec.full_name
20
- spec_ruby = spec.to_ruby
21
-
22
- gem_dir = gem_dir_for(full_name)
23
- if File.directory?(gem_dir)
24
- gemspec_files << anchor_gemspec(gem_dir, full_name, spec_ruby)
25
- else
26
- gemspec_dir = File.join(Bundler.tmp("vault_source"), "specifications")
27
- FileUtils.mkdir_p(gemspec_dir)
28
- gemspec_path = File.join(gemspec_dir, "#{full_name}.gemspec")
29
- File.write(gemspec_path, spec_ruby)
30
- gemspec_files << gemspec_path
31
- end
32
- end
17
+ vault.gem_entries.map { |entry| gemspec_file_for(vault, entry) }
33
18
  end
34
-
35
- gemspec_files
36
19
  end
37
20
 
38
21
  def install(spec, opts = {})
39
22
  gem_dir = gem_dir_for(spec.full_name)
40
- if File.directory?(gem_dir) && !opts[:force]
41
- Bundler.ui.debug "Using #{version_message(spec)} from vault #{File.basename(@vault_path)}"
42
- gemspec_in_gem = File.join(gem_dir, "#{spec.full_name}.gemspec")
43
- spec.full_gem_path = gem_dir
44
- spec.loaded_from = gemspec_in_gem
45
- return nil
46
- end
47
-
48
- Bundler.ui.confirm "Installing #{version_message(spec)} from vault #{File.basename(@vault_path)}"
49
-
50
- Gemvault::Vault.open(@vault_path) do |vault|
51
- vault.with_gem_file(spec.name, spec.version.to_s, platform: spec.platform.to_s) do |gem_path|
52
- require "bundler/rubygems_gem_installer"
53
-
54
- installer = Bundler::RubyGemsGemInstaller.at(
55
- gem_path,
56
- install_dir: Bundler.bundle_path.to_s,
57
- bin_dir: Bundler.system_bindir.to_s,
58
- ignore_dependencies: true,
59
- wrappers: true,
60
- env_shebang: true,
61
- build_args: opts[:build_args] || [],
62
- )
63
-
64
- installed_spec = installer.install
65
-
66
- gem_dir = installed_spec.full_gem_path
67
- spec.full_gem_path = gem_dir
68
- spec.loaded_from = anchor_gemspec(gem_dir, spec.full_name, installed_spec.to_ruby)
69
- end
70
- end
23
+ return use_installed_gem(spec, gem_dir) if File.directory?(gem_dir) && !opts[:force]
71
24
 
25
+ Bundler.ui.confirm "Installing #{version_message(spec)} from vault #{@uri}"
26
+ install_into_bundle(spec, opts)
72
27
  spec.post_install_message
73
28
  end
74
29
 
@@ -76,12 +31,65 @@ module Bundler
76
31
  {}
77
32
  end
78
33
 
34
+ # No source-level install_path to copy: VaultSource#install installs
35
+ # each gem into Bundler.bundle_path via RubyGemsGemInstaller, so the
36
+ # default Source#cache would dereference a non-existent directory.
37
+ def cache(spec, custom_path = nil); end
38
+
79
39
  def to_s
80
40
  "vault at #{@uri}"
81
41
  end
82
42
 
83
43
  private
84
44
 
45
+ def gemspec_file_for(vault, entry)
46
+ spec = vault.spec_from_blob(entry.name, entry.version, entry.platform)
47
+ gem_dir = gem_dir_for(spec.full_name)
48
+ return anchor_gemspec(gem_dir, spec.full_name, spec.to_ruby) if File.directory?(gem_dir)
49
+
50
+ write_tmp_gemspec(spec.full_name, spec.to_ruby)
51
+ end
52
+
53
+ def write_tmp_gemspec(full_name, spec_ruby)
54
+ gemspec_dir = File.join(Bundler.tmp("vault_source"), "specifications")
55
+ FileUtils.mkdir_p(gemspec_dir)
56
+ gemspec_path = File.join(gemspec_dir, "#{full_name}.gemspec")
57
+ File.write(gemspec_path, spec_ruby)
58
+ gemspec_path
59
+ end
60
+
61
+ def use_installed_gem(spec, gem_dir)
62
+ Bundler.ui.debug "Using #{version_message(spec)} from vault #{@uri}"
63
+ spec.full_gem_path = gem_dir
64
+ spec.loaded_from = File.join(gem_dir, "#{spec.full_name}.gemspec")
65
+ nil
66
+ end
67
+
68
+ def install_into_bundle(spec, opts)
69
+ Gemvault::Vault.open(@vault_path) do |vault|
70
+ vault.with_gem_file(spec.name, spec.version.to_s, platform: spec.platform.to_s) do |gem_path|
71
+ installed_spec = build_installer(gem_path, opts).install
72
+ gem_dir = installed_spec.full_gem_path
73
+ spec.full_gem_path = gem_dir
74
+ spec.loaded_from = anchor_gemspec(gem_dir, spec.full_name, installed_spec.to_ruby)
75
+ end
76
+ end
77
+ end
78
+
79
+ def build_installer(gem_path, opts)
80
+ require "bundler/rubygems_gem_installer"
81
+
82
+ Bundler::RubyGemsGemInstaller.at(
83
+ gem_path,
84
+ install_dir: Bundler.bundle_path.to_s,
85
+ bin_dir: Bundler.system_bindir.to_s,
86
+ ignore_dependencies: true,
87
+ wrappers: true,
88
+ env_shebang: true,
89
+ build_args: opts[:build_args] || [],
90
+ )
91
+ end
92
+
85
93
  def version_message(spec)
86
94
  message = "#{spec.name} #{spec.version}"
87
95
  message += " (#{spec.platform})" if spec.platform != Gem::Platform::RUBY && !spec.platform.nil?
@@ -3,6 +3,7 @@ require_relative "../vault"
3
3
 
4
4
  module Gemvault
5
5
  class CLI
6
+ # Base class for gemvault subcommands; opens a vault and reports vault errors.
6
7
  class Command < CommandKit::Command
7
8
  private
8
9
 
@@ -3,6 +3,7 @@ require_relative "../command"
3
3
  module Gemvault
4
4
  class CLI
5
5
  module Commands
6
+ # Adds one or more .gem files into a vault.
6
7
  class Add < Command
7
8
  description "Add gem files to a vault"
8
9
 
@@ -0,0 +1,28 @@
1
+ require_relative "../command"
2
+
3
+ module Gemvault
4
+ class CLI
5
+ module Commands
6
+ # Recovers from a bundler plugin index entry whose stored path no
7
+ # longer exists. Bundler records absolute paths in .bundle/plugin/index
8
+ # for path-installed plugins. Moving or renaming the source directory
9
+ # leaves an invalid path behind -- Bundler::Plugin.load_plugin warns
10
+ # "The following plugin paths don't exist: ..." and silently returns,
11
+ # leaving @sources[<type>] nil. The next `source X, type: :vault`
12
+ # crashes inside Bundler::SourceList#add_plugin_source with
13
+ # NoMethodError on nil.
14
+ #
15
+ # Uninstalling clears the broken entry; re-running bundle install
16
+ # triggers Bundler to reinstall the plugin against whatever the
17
+ # current Gemfile declares. Run this from the project directory.
18
+ class Doctor < Command
19
+ description "Clear a broken bundler-source-vault plugin index entry and reinstall it"
20
+
21
+ def run
22
+ system("bundle", "plugin", "uninstall", "bundler-source-vault", exception: true)
23
+ exec("bundle", "install")
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -4,6 +4,7 @@ require_relative "../command"
4
4
  module Gemvault
5
5
  class CLI
6
6
  module Commands
7
+ # Extracts gem file(s) from a vault back onto the filesystem.
7
8
  class Extract < Command
8
9
  description "Extract gem file(s) from a vault"
9
10
 
@@ -28,19 +29,29 @@ module Gemvault
28
29
 
29
30
  with_vault(vault) do |v|
30
31
  ::FileUtils.mkdir_p(output_dir)
32
+ entries = matching_entries(v, name, version)
33
+ abort_missing_gem(name) if entries.empty?
34
+ extract_entries(v, entries, output_dir)
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def matching_entries(vault, name, version)
41
+ entries = vault.gem_entries.select { |e| e.name == name }
42
+ version ? entries.select { |e| e.version == version } : entries
43
+ end
44
+
45
+ def abort_missing_gem(name)
46
+ print_error("No gem named '#{name}' in vault")
47
+ exit(1)
48
+ end
31
49
 
32
- entries = v.gem_entries.select { |e| e.name == name }
33
- entries = entries.select { |e| e.version == version } if version
34
- if entries.empty?
35
- print_error("No gem named '#{name}' in vault")
36
- exit(1)
37
- end
38
-
39
- entries.each do |entry|
40
- data = v.gem_data(entry.name, entry.version, platform: entry.platform)
41
- File.binwrite(File.join(output_dir, entry.filename), data)
42
- puts "Extracted #{entry.filename}"
43
- end
50
+ def extract_entries(vault, entries, output_dir)
51
+ entries.each do |entry|
52
+ data = vault.gem_data(entry.name, entry.version, platform: entry.platform)
53
+ File.binwrite(File.join(output_dir, entry.filename), data)
54
+ puts "Extracted #{entry.filename}"
44
55
  end
45
56
  end
46
57
  end
@@ -3,6 +3,7 @@ require_relative "../command"
3
3
  module Gemvault
4
4
  class CLI
5
5
  module Commands
6
+ # Lists the gems stored in a vault.
6
7
  class List < Command
7
8
  description "List gems in a vault"
8
9
 
@@ -3,6 +3,7 @@ require_relative "../command"
3
3
  module Gemvault
4
4
  class CLI
5
5
  module Commands
6
+ # Creates a new, empty vault file.
6
7
  class New < Command
7
8
  description "Create a new vault"
8
9
 
@@ -1,8 +1,11 @@
1
1
  require_relative "../command"
2
+ require_relative "../../gem_reference"
2
3
 
3
4
  module Gemvault
4
5
  class CLI
5
6
  module Commands
7
+ # `gemvault remove` subcommand. Parses argv into a GemReference and
8
+ # asks the vault to remove matching gems.
6
9
  class Remove < Command
7
10
  description "Remove gem(s) from a vault"
8
11
 
@@ -12,21 +15,32 @@ module Gemvault
12
15
 
13
16
  argument :name, required: true,
14
17
  usage: "NAME",
15
- desc: "Gem name"
18
+ desc: "Gem name, or NAME-VERSION"
16
19
 
17
20
  argument :version, required: false,
18
21
  usage: "VERSION",
19
22
  desc: "Gem version (omit to remove all versions)"
20
23
 
21
- def run(vault, name, version = nil)
22
- with_vault(vault) do |v|
23
- count = v.remove(name, version)
24
- if count.zero?
25
- print_error("No matching gem found")
26
- exit(1)
27
- end
28
- puts "Removed #{count} gem(s)"
24
+ option :version, short: "-v",
25
+ value: { type: String, usage: "VERSION" },
26
+ desc: "Gem version (overrides positional and NAME-VERSION forms)"
27
+
28
+ def run(vault, name, positional_version = nil)
29
+ ref = Gemvault::GemReference.parse(name, version: options[:version] || positional_version)
30
+ with_vault(vault) { |v| report_removal(v.remove(ref)) }
31
+ rescue Gemvault::GemReference::NonExactVersionError => e
32
+ print_error(e.message)
33
+ exit(1)
34
+ end
35
+
36
+ private
37
+
38
+ def report_removal(count)
39
+ if count.zero?
40
+ print_error("No matching gem found")
41
+ exit(1)
29
42
  end
43
+ puts "Removed #{count} gem(s)"
30
44
  end
31
45
  end
32
46
  end
data/lib/gemvault/cli.rb CHANGED
@@ -4,6 +4,7 @@ require "command_kit/options/version"
4
4
  require_relative "version"
5
5
 
6
6
  module Gemvault
7
+ # Command-line entry point that auto-loads and dispatches gemvault subcommands.
7
8
  class CLI < CommandKit::Command
8
9
  include CommandKit::Commands::AutoLoad.new(
9
10
  dir: "#{__dir__}/cli/commands",
@@ -1,4 +1,5 @@
1
1
  module Gemvault
2
+ # Value object for one gem row (name, version, platform, created_at) in a vault.
2
3
  class GemEntry
3
4
  attr_reader :name, :version, :platform, :created_at
4
5
 
@@ -0,0 +1,7 @@
1
+ module Gemvault
2
+ class GemReference
3
+ # Matches every version of a named gem stored in the vault.
4
+ class AnyVersion < GemReference
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,29 @@
1
+ module Gemvault
2
+ class GemReference
3
+ # Matches one exact version of a named gem. `version:` is always a
4
+ # Gem::Version (never nil, never a String) `.parse` validates and
5
+ # constructs it before calling `.new`.
6
+ class SpecificVersion < GemReference
7
+ attr_reader :version
8
+
9
+ def initialize(name:, version:)
10
+ super(name: name)
11
+ @version = version
12
+ end
13
+
14
+ def ==(other)
15
+ super && version == other.version
16
+ end
17
+ alias eql? ==
18
+
19
+ def hash
20
+ [self.class, name, version].hash
21
+ end
22
+
23
+ def deconstruct_keys(keys)
24
+ data = { name: name, version: version }
25
+ keys.nil? ? data : data.slice(*keys)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,54 @@
1
+ require "rubygems/version"
2
+ require_relative "gem_reference/any_version"
3
+ require_relative "gem_reference/specific_version"
4
+
5
+ module Gemvault
6
+ # A reference to a gem in a vault. Abstract base class for two concrete
7
+ # kinds: AnyVersion (no version constraint) and SpecificVersion (an exact
8
+ # Gem::Version). Not instantiable directly. `.parse` is the factory that
9
+ # turns raw CLI input into one of the two subclasses.
10
+ class GemReference
11
+ class NonExactVersionError < StandardError; end
12
+
13
+ def self.parse(input, version: nil)
14
+ base_name, embedded = split_name_version(input)
15
+ version_string = version || embedded
16
+ return AnyVersion.new(name: base_name) if version_string.nil?
17
+ unless Gem::Version.correct?(version_string)
18
+ raise NonExactVersionError, "Version must be an exact version (got: #{version_string.inspect})"
19
+ end
20
+
21
+ SpecificVersion.new(name: base_name, version: Gem::Version.new(version_string))
22
+ end
23
+
24
+ def self.split_name_version(input)
25
+ idx = input.rindex("-")
26
+ return [input, nil] unless idx && Gem::Version.correct?(input[(idx + 1)..])
27
+
28
+ [input[0...idx], input[(idx + 1)..]]
29
+ end
30
+ private_class_method :split_name_version
31
+
32
+ attr_reader :name
33
+
34
+ def initialize(name:)
35
+ raise NotImplementedError, "abstract base use AnyVersion or SpecificVersion" if instance_of?(GemReference)
36
+
37
+ @name = name
38
+ end
39
+
40
+ def ==(other)
41
+ self.class == other.class && name == other.name
42
+ end
43
+ alias eql? ==
44
+
45
+ def hash
46
+ [self.class, name].hash
47
+ end
48
+
49
+ def deconstruct_keys(keys)
50
+ data = { name: name }
51
+ keys.nil? ? data : data.slice(*keys)
52
+ end
53
+ end
54
+ end
@@ -3,8 +3,10 @@ require "rubygems/package"
3
3
  require "fileutils"
4
4
  require "tempfile"
5
5
  require_relative "gem_entry"
6
+ require_relative "gem_reference"
6
7
 
7
8
  module Gemvault
9
+ # SQLite-backed archive of .gem blobs; supports add/remove/list/extract.
8
10
  class Vault
9
11
  class Error < StandardError; end
10
12
  class NotFoundError < Error; end
@@ -12,6 +14,7 @@ module Gemvault
12
14
  class InvalidGemError < Error; end
13
15
 
14
16
  SCHEMA_VERSION = "1".freeze
17
+ SQLITE_MAGIC = "SQLite format 3#{0.chr}".freeze
15
18
 
16
19
  attr_reader :path
17
20
 
@@ -28,63 +31,26 @@ module Gemvault
28
31
 
29
32
  def initialize(path, create: false)
30
33
  @path = File.expand_path(path)
31
-
32
- if create
33
- raise Error, "Vault already exists: #{@path}" if File.exist?(@path)
34
-
35
- @db = SQLite3::Database.new(@path)
36
- @db.results_as_hash = true
37
- create_schema
38
- else
39
- raise NotFoundError, "Vault not found: #{@path}" unless File.exist?(@path)
40
-
41
- validate_sqlite!
42
- @db = SQLite3::Database.new(@path)
43
- @db.results_as_hash = true
44
- end
34
+ create ? create_vault! : open_vault!
45
35
  end
46
36
 
47
37
  def add(gem_path)
48
38
  gem_path = File.expand_path(gem_path)
49
39
  raise NotFoundError, "Gem file not found: #{gem_path}" unless File.file?(gem_path)
50
40
 
51
- begin
52
- pkg = Gem::Package.new(gem_path)
53
- spec = pkg.spec
54
- rescue StandardError => e
55
- raise InvalidGemError, "Invalid gem file #{gem_path}: #{e.message}"
56
- end
57
-
58
- name = spec.name
59
- version = spec.version.to_s
60
- platform = spec.platform.to_s
61
-
62
- existing = @db.execute(
63
- "SELECT 1 FROM gems WHERE name = ? AND version = ? AND platform = ?",
64
- [name, version, platform],
65
- )
66
- unless existing.empty?
67
- raise DuplicateGemError,
68
- "Gem already in vault: #{name}-#{version} (#{platform})"
69
- end
70
-
71
- data = File.binread(gem_path)
72
- @db.execute(
73
- "INSERT INTO gems (name, version, platform, data) VALUES (?, ?, ?, ?)",
74
- [name, version, platform, SQLite3::Blob.new(data)],
75
- )
41
+ spec = load_gem_spec(gem_path)
42
+ raise_if_duplicate(spec)
43
+ insert_gem(gem_path, spec)
76
44
  end
77
45
 
78
- def remove(name, version = nil)
79
- if version
46
+ def remove(reference)
47
+ case reference
48
+ in GemReference::AnyVersion[name:]
49
+ @db.execute("DELETE FROM gems WHERE name = ?", [name])
50
+ in GemReference::SpecificVersion[name:, version:]
80
51
  @db.execute(
81
52
  "DELETE FROM gems WHERE name = ? AND version = ?",
82
- [name, version],
83
- )
84
- else
85
- @db.execute(
86
- "DELETE FROM gems WHERE name = ?",
87
- [name],
53
+ [name, version.to_s],
88
54
  )
89
55
  end
90
56
  @db.changes
@@ -121,11 +87,8 @@ module Gemvault
121
87
 
122
88
  def with_gem_file(name, version, platform: "ruby")
123
89
  data = gem_data(name, version, platform: platform)
124
- tmpfile = Tempfile.new(["vault_gem", ".gem"])
90
+ tmpfile = write_tempfile(data)
125
91
  begin
126
- tmpfile.binmode
127
- tmpfile.write(data)
128
- tmpfile.close
129
92
  yield tmpfile.path
130
93
  ensure
131
94
  tmpfile.close unless tmpfile.closed?
@@ -141,6 +104,59 @@ module Gemvault
141
104
 
142
105
  private
143
106
 
107
+ def create_vault!
108
+ raise Error, "Vault already exists: #{@path}" if File.exist?(@path)
109
+
110
+ @db = new_database
111
+ create_schema
112
+ end
113
+
114
+ def open_vault!
115
+ raise NotFoundError, "Vault not found: #{@path}" unless File.exist?(@path)
116
+
117
+ validate_sqlite!
118
+ @db = new_database
119
+ end
120
+
121
+ def new_database
122
+ db = SQLite3::Database.new(@path)
123
+ db.results_as_hash = true
124
+ db
125
+ end
126
+
127
+ def load_gem_spec(gem_path)
128
+ Gem::Package.new(gem_path).spec
129
+ rescue StandardError => e
130
+ raise InvalidGemError, "Invalid gem file #{gem_path}: #{e.message}"
131
+ end
132
+
133
+ def raise_if_duplicate(spec)
134
+ existing = @db.execute(
135
+ "SELECT 1 FROM gems WHERE name = ? AND version = ? AND platform = ?",
136
+ [spec.name, spec.version.to_s, spec.platform.to_s],
137
+ )
138
+ return if existing.empty?
139
+
140
+ raise DuplicateGemError,
141
+ "Gem already in vault: #{spec.name}-#{spec.version} (#{spec.platform})"
142
+ end
143
+
144
+ def insert_gem(gem_path, spec)
145
+ data = File.binread(gem_path)
146
+ @db.execute(
147
+ "INSERT INTO gems (name, version, platform, data) VALUES (?, ?, ?, ?)",
148
+ [spec.name, spec.version.to_s, spec.platform.to_s, SQLite3::Blob.new(data)],
149
+ )
150
+ end
151
+
152
+ def write_tempfile(data)
153
+ tmpfile = Tempfile.new(["vault_gem", ".gem"])
154
+ tmpfile.binmode
155
+ tmpfile.write(data)
156
+ tmpfile.close
157
+ tmpfile
158
+ end
159
+
144
160
  def create_schema
145
161
  @db.execute_batch(<<~SQL)
146
162
  CREATE TABLE metadata (
@@ -169,8 +185,7 @@ module Gemvault
169
185
  end
170
186
 
171
187
  def validate_sqlite!
172
- magic = File.binread(@path, 16)
173
- return if magic == "SQLite format 3\x00"
188
+ return if File.binread(@path, SQLITE_MAGIC.bytesize) == SQLITE_MAGIC
174
189
 
175
190
  raise Error, "Not a valid vault file (not SQLite): #{@path}"
176
191
  end
@@ -1,3 +1,3 @@
1
1
  module Gemvault
2
- VERSION = "0.1.1".freeze
2
+ VERSION = "0.1.3".freeze
3
3
  end
@@ -3,7 +3,6 @@
3
3
  #
4
4
  # Returns standard Gem::Resolver::IndexSpecification objects so the
5
5
  # resolver's install pipeline (download -> Gem::Installer) works unchanged.
6
-
7
6
  class Gem::Resolver::VaultSet < Gem::Resolver::Set
8
7
  def initialize(source)
9
8
  super()
@@ -12,28 +11,30 @@ class Gem::Resolver::VaultSet < Gem::Resolver::Set
12
11
  end
13
12
 
14
13
  def find_all(req)
15
- @specs.select { |tuple| req.match?(tuple) }.map do |tuple|
16
- Gem::Resolver::IndexSpecification.new(
17
- self,
18
- tuple.name,
19
- tuple.version,
20
- @source,
21
- tuple.platform,
22
- )
23
- end
14
+ @specs.filter_map { |tuple| index_spec_for(req, tuple) }
24
15
  end
25
16
 
26
17
  def prefetch(reqs); end
27
18
 
28
- def pretty_print(q)
29
- q.group 2, "[VaultSet", "]" do
19
+ def pretty_print(pp)
20
+ pp.group 2, "[VaultSet", "]" do
30
21
  next if @specs.empty?
31
22
 
32
- q.breakable
23
+ pp.breakable
33
24
 
34
- q.seplist @specs do |tuple|
35
- q.text tuple.full_name
25
+ pp.seplist @specs do |tuple|
26
+ pp.text tuple.full_name
36
27
  end
37
28
  end
38
29
  end
30
+
31
+ private
32
+
33
+ def index_spec_for(req, tuple)
34
+ return unless req.match?(tuple)
35
+
36
+ Gem::Resolver::IndexSpecification.new(
37
+ self, tuple.name, tuple.version, @source, tuple.platform
38
+ )
39
+ end
39
40
  end
@@ -1,4 +1,5 @@
1
1
  require "gemvault/vault"
2
+ require "uri"
2
3
 
3
4
  ##
4
5
  # A source backed by a .gemv vault file (SQLite archive of .gem blobs).
@@ -6,37 +7,24 @@ require "gemvault/vault"
6
7
  # Used by the gemvault RubyGems plugin to support:
7
8
  #
8
9
  # gem install --source myvault.gemv activesupport
9
-
10
10
  class Gem::Source::Vault < Gem::Source
11
11
  include Gem::UserInteraction
12
12
 
13
+ VAULT_URI_SCHEMES = %w[file vault].freeze
14
+
13
15
  attr_reader :path
14
16
 
15
17
  def initialize(path)
16
- path = path.to_s
17
- path = path.sub(%r{^file://}, "") if path.start_with?("file://")
18
- @path = File.expand_path(path)
19
- @uri = @path
18
+ @path = File.expand_path(filesystem_path(path))
19
+ super(@path)
20
+ @uri = @path
20
21
  @specs = nil
21
22
  end
22
23
 
23
24
  def load_specs(type)
24
25
  verbose "Loading #{type} specs from vault at #{@path}"
25
26
  ensure_specs_loaded
26
-
27
- case type
28
- when :released
29
- @specs.keys.reject { |t| t.version.prerelease? }
30
- when :prerelease
31
- @specs.keys.select { |t| t.version.prerelease? }
32
- when :latest
33
- @specs.keys
34
- .group_by { |tuple| [tuple.name, tuple.platform] }
35
- .values
36
- .map { |tuples| tuples.max_by(&:version) }
37
- else
38
- @specs.keys
39
- end
27
+ select_tuples(type)
40
28
  end
41
29
 
42
30
  def fetch_spec(name_tuple)
@@ -63,7 +51,7 @@ class Gem::Source::Vault < Gem::Source
63
51
  dest
64
52
  end
65
53
 
66
- def dependency_resolver_set(prerelease = false)
54
+ def dependency_resolver_set(prerelease = nil)
67
55
  require_relative "../resolver/vault_set"
68
56
  set = Gem::Resolver::VaultSet.new(self)
69
57
  set.prerelease = prerelease
@@ -72,15 +60,9 @@ class Gem::Source::Vault < Gem::Source
72
60
 
73
61
  def <=>(other)
74
62
  case other
75
- when Gem::Source::Installed,
76
- Gem::Source::Lock then
77
- -1
78
- when Gem::Source::Vault
79
- 0
80
- when Gem::Source::Local
81
- -1
82
- when Gem::Source
83
- 1
63
+ when Gem::Source::Installed, Gem::Source::Lock, Gem::Source::Local then -1
64
+ when Gem::Source::Vault then 0
65
+ when Gem::Source then 1
84
66
  end
85
67
  end
86
68
 
@@ -98,17 +80,48 @@ class Gem::Source::Vault < Gem::Source
98
80
  "vault at #{@path}"
99
81
  end
100
82
 
101
- def pretty_print(q)
102
- q.object_group(self) do
103
- q.group 2, "[Vault:", "]" do
104
- q.breakable
105
- q.text @path
83
+ def pretty_print(pp)
84
+ pp.object_group(self) do
85
+ pp.group 2, "[Vault:", "]" do
86
+ pp.breakable
87
+ pp.text @path
106
88
  end
107
89
  end
108
90
  end
109
91
 
110
92
  private
111
93
 
94
+ def filesystem_path(path)
95
+ uri = URI.parse(path.to_s)
96
+ VAULT_URI_SCHEMES.include?(uri.scheme) ? uri.path : path.to_s
97
+ rescue URI::InvalidURIError
98
+ path.to_s
99
+ end
100
+
101
+ def select_tuples(type)
102
+ case type
103
+ when :released then released_tuples
104
+ when :prerelease then prerelease_tuples
105
+ when :latest then latest_tuples
106
+ else @specs.keys
107
+ end
108
+ end
109
+
110
+ def released_tuples
111
+ @specs.keys.reject { |tuple| tuple.version.prerelease? }
112
+ end
113
+
114
+ def prerelease_tuples
115
+ @specs.keys.select { |tuple| tuple.version.prerelease? }
116
+ end
117
+
118
+ def latest_tuples
119
+ @specs.keys
120
+ .group_by { |tuple| [tuple.name, tuple.platform] }
121
+ .values
122
+ .map { |tuples| tuples.max_by(&:version) }
123
+ end
124
+
112
125
  def ensure_specs_loaded
113
126
  return if @specs
114
127
 
@@ -14,28 +14,37 @@ require "rubygems/source_list"
14
14
  # 3. SourceList#<< -- route .gemv strings to Gem::Source::Vault
15
15
 
16
16
  module Gemvault
17
+ # Lets .gemv paths bypass RubyGems' URI scheme validation.
17
18
  module AcceptVaultURI
19
+ VALID_URI_SCHEMES = ["http", "https", "file", "s3"].freeze
20
+
18
21
  def accept_uri_http
19
22
  Gem::OptionParser.accept Gem::URI::HTTP do |value|
20
23
  next value if value.to_s.end_with?(".gemv")
21
24
 
22
- begin
23
- uri = Gem::URI.parse value
24
- rescue Gem::URI::InvalidURIError
25
- raise Gem::OptionParser::InvalidArgument, value
26
- end
27
-
28
- valid_uri_schemes = ["http", "https", "file", "s3"]
29
- unless valid_uri_schemes.include?(uri.scheme)
30
- msg = "Invalid uri scheme for #{value}\nPreface URLs with one of #{valid_uri_schemes.map { |s| "#{s}://" }}"
31
- raise ArgumentError, msg
32
- end
33
-
25
+ uri = parse_uri(value)
26
+ validate_scheme!(uri, value)
34
27
  value
35
28
  end
36
29
  end
30
+
31
+ private
32
+
33
+ def parse_uri(value)
34
+ Gem::URI.parse value
35
+ rescue Gem::URI::InvalidURIError
36
+ raise Gem::OptionParser::InvalidArgument, value
37
+ end
38
+
39
+ def validate_scheme!(uri, value)
40
+ return if VALID_URI_SCHEMES.include?(uri.scheme)
41
+
42
+ schemes = VALID_URI_SCHEMES.map { |scheme| "#{scheme}://" }
43
+ raise ArgumentError, "Invalid uri scheme for #{value}\nPreface URLs with one of #{schemes}"
44
+ end
37
45
  end
38
46
 
47
+ # Skips the trailing-slash append for .gemv source URLs.
39
48
  module AddVaultSourceOption
40
49
  def add_source_option
41
50
  accept_uri_http
@@ -53,6 +62,7 @@ module Gemvault
53
62
  end
54
63
  end
55
64
 
65
+ # Routes .gemv source strings to Gem::Source::Vault.
56
66
  module VaultSourceList
57
67
  def <<(obj)
58
68
  if obj.is_a?(String) && obj.end_with?(".gemv")
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gemvault
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Gillis
@@ -64,6 +64,7 @@ files:
64
64
  - ".rubocop.yml"
65
65
  - ".ruby-version"
66
66
  - ASSESSMENT.md
67
+ - CHANGELOG.md
67
68
  - CLAUDE.md
68
69
  - Dockerfile.test
69
70
  - LICENSE
@@ -73,16 +74,21 @@ files:
73
74
  - Rakefile
74
75
  - docs/superpowers/plans/2026-04-16-container-integration-tests.md
75
76
  - exe/gemvault
77
+ - issues.rec
76
78
  - lib/bundler/plugin/vault_source.rb
77
79
  - lib/gemvault.rb
78
80
  - lib/gemvault/cli.rb
79
81
  - lib/gemvault/cli/command.rb
80
82
  - lib/gemvault/cli/commands/add.rb
83
+ - lib/gemvault/cli/commands/doctor.rb
81
84
  - lib/gemvault/cli/commands/extract.rb
82
85
  - lib/gemvault/cli/commands/list.rb
83
86
  - lib/gemvault/cli/commands/new.rb
84
87
  - lib/gemvault/cli/commands/remove.rb
85
88
  - lib/gemvault/gem_entry.rb
89
+ - lib/gemvault/gem_reference.rb
90
+ - lib/gemvault/gem_reference/any_version.rb
91
+ - lib/gemvault/gem_reference/specific_version.rb
86
92
  - lib/gemvault/vault.rb
87
93
  - lib/gemvault/version.rb
88
94
  - lib/rubygems/resolver/vault_set.rb
@@ -110,7 +116,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
110
116
  - !ruby/object:Gem::Version
111
117
  version: '0'
112
118
  requirements: []
113
- rubygems_version: 4.0.9
119
+ rubygems_version: 4.0.15
114
120
  specification_version: 4
115
121
  summary: Multi-gem portable archives backed by SQLite
116
122
  test_files: []