localvault 1.8.0 → 1.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +11 -6
- data/lib/localvault/cli/error_presenter.rb +13 -1
- data/lib/localvault/cli.rb +123 -5
- data/lib/localvault/stdin_secret_input.rb +22 -0
- data/lib/localvault/version.rb +1 -1
- data/lib/localvault.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a7cdca7447f0efeb125773a3385a6e3beee17cb03d4c05e04b91dc59254215dd
|
|
4
|
+
data.tar.gz: b3378c85ad59e00561d5eb5f590e203469d86f8c88925e7b547721717175f5a4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2e4409c50f9aa0350b86541e905703ed24520cd583498d49de11a320cda8b6c2ebf86bc14c377bed52cd83438ba2254b0e243d91ffcf5521f7f84d9445072e1c
|
|
7
|
+
data.tar.gz: 97ff4555610dc166e82d01b6f11200ce516d0d36acb701625519c73b6e2f31d9096ae53e19d863128246d240d54eb82ebbd6f60ed51309ef4a8ec45675b144fd
|
data/README.md
CHANGED
|
@@ -41,10 +41,10 @@ sudo dnf install libsodium-devel
|
|
|
41
41
|
# Create a vault (prompts for passphrase)
|
|
42
42
|
localvault init
|
|
43
43
|
|
|
44
|
-
# Store secrets
|
|
45
|
-
localvault set OPENAI_API_KEY
|
|
46
|
-
localvault set STRIPE_SECRET_KEY
|
|
47
|
-
localvault set DATABASE_URL
|
|
44
|
+
# Store secrets without putting values in shell history / process args
|
|
45
|
+
printf '%s' "$OPENAI_API_KEY" | localvault set OPENAI_API_KEY --stdin
|
|
46
|
+
printf '%s' "$STRIPE_SECRET_KEY" | localvault set STRIPE_SECRET_KEY --stdin
|
|
47
|
+
printf '%s' "$DATABASE_URL" | localvault set DATABASE_URL --stdin
|
|
48
48
|
|
|
49
49
|
# Retrieve a secret (raw, pipeable)
|
|
50
50
|
localvault get OPENAI_API_KEY
|
|
@@ -69,8 +69,9 @@ localvault exec -- rails server
|
|
|
69
69
|
| Command | Description |
|
|
70
70
|
|---------|-------------|
|
|
71
71
|
| `init [NAME]` | Create a vault (Argon2id key derivation) |
|
|
72
|
-
| `set KEY
|
|
73
|
-
| `set
|
|
72
|
+
| `set KEY --stdin` | Store a secret from stdin without argv/history exposure |
|
|
73
|
+
| `set KEY [VALUE]` | Store a secret (positional value kept for compatibility) |
|
|
74
|
+
| `set --group GROUP KEY --stdin` | Store a secret in a named group from stdin |
|
|
74
75
|
| `get KEY` | Retrieve a secret (raw, pipeable) |
|
|
75
76
|
| `show` | Display all secrets in a table (masked by default) |
|
|
76
77
|
| `show --reveal` | Display with values visible |
|
|
@@ -143,6 +144,7 @@ backward compatibility but the top-level forms are preferred.
|
|
|
143
144
|
|---------|-------------|
|
|
144
145
|
| `install-mcp [CLIENT]` | Configure MCP server in claude-code, cursor, or windsurf |
|
|
145
146
|
| `mcp` | Start MCP server (stdio transport) |
|
|
147
|
+
| `doctor` | Check install and PATH readiness, including brew/asdf shadowing |
|
|
146
148
|
|
|
147
149
|
All commands accept `--vault NAME` (or `-v NAME`) to target a specific vault. Default vault is `default`.
|
|
148
150
|
|
|
@@ -232,6 +234,9 @@ localvault unlock
|
|
|
232
234
|
# Verify setup without starting the blocking stdio server
|
|
233
235
|
localvault mcp --check
|
|
234
236
|
|
|
237
|
+
# Diagnose brew/asdf PATH shadowing after upgrades
|
|
238
|
+
localvault doctor
|
|
239
|
+
|
|
235
240
|
# MCP tools available to the agent:
|
|
236
241
|
# localvault_whoami — diagnose active vault/session state
|
|
237
242
|
# list_secrets(vault?, prefix?, query?) — list/search key names
|
|
@@ -60,8 +60,12 @@ module LocalVault
|
|
|
60
60
|
when :ambiguous then "Error: group name is ambiguous. Existing groups: #{@error.candidates.join(", ")}."
|
|
61
61
|
else "Error: group and key names may contain letters, digits, and underscores only."
|
|
62
62
|
end
|
|
63
|
+
elsif @error.is_a?(CLI::SetValueSourceError) && @error.kind == :multiple
|
|
64
|
+
"Error: #{@error.message}"
|
|
63
65
|
elsif group_save_attempt?
|
|
64
66
|
"Error: saving in a group needs GROUP, KEY, and VALUE."
|
|
67
|
+
elsif @error.is_a?(CLI::SetValueSourceError)
|
|
68
|
+
"Error: #{@error.message}"
|
|
65
69
|
elsif unknown_option
|
|
66
70
|
"Error: unknown option `#{unknown_option}`."
|
|
67
71
|
elsif context.command.nil?
|
|
@@ -97,7 +101,7 @@ module LocalVault
|
|
|
97
101
|
end
|
|
98
102
|
|
|
99
103
|
return group_save_suggestions if group_save_attempt?
|
|
100
|
-
return
|
|
104
|
+
return set_suggestions if context.command&.name == "set"
|
|
101
105
|
return show_group_suggestions if context.command&.name == "show" || unknown_option == "--group-by"
|
|
102
106
|
return ["localvault add HANDLE", "localvault team add HANDLE"] if context.namespace == ["team"] && context.command&.name == "add"
|
|
103
107
|
|
|
@@ -117,6 +121,14 @@ module LocalVault
|
|
|
117
121
|
]
|
|
118
122
|
end
|
|
119
123
|
|
|
124
|
+
def set_suggestions
|
|
125
|
+
[
|
|
126
|
+
%(printf '%s' "$SECRET" | localvault set KEY --stdin),
|
|
127
|
+
"localvault set KEY VALUE",
|
|
128
|
+
*group_save_suggestions
|
|
129
|
+
]
|
|
130
|
+
end
|
|
131
|
+
|
|
120
132
|
def show_group_suggestions
|
|
121
133
|
[
|
|
122
134
|
"localvault show --group GROUP",
|
data/lib/localvault/cli.rb
CHANGED
|
@@ -5,6 +5,7 @@ require "lipgloss"
|
|
|
5
5
|
require_relative "env_projection"
|
|
6
6
|
require_relative "key_lookup"
|
|
7
7
|
require_relative "session_cache"
|
|
8
|
+
require_relative "stdin_secret_input"
|
|
8
9
|
require_relative "vault_resolver"
|
|
9
10
|
require_relative "group_catalog"
|
|
10
11
|
|
|
@@ -49,6 +50,18 @@ module LocalVault
|
|
|
49
50
|
1
|
|
50
51
|
end
|
|
51
52
|
end
|
|
53
|
+
class SetValueSourceError < Thor::Error
|
|
54
|
+
attr_reader :kind
|
|
55
|
+
|
|
56
|
+
def initialize(kind, message)
|
|
57
|
+
@kind = kind
|
|
58
|
+
super(message)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def exit_status
|
|
62
|
+
1
|
|
63
|
+
end
|
|
64
|
+
end
|
|
52
65
|
|
|
53
66
|
def self.start(given_args = ARGV, config = {})
|
|
54
67
|
require_relative "cli/error_presenter"
|
|
@@ -105,7 +118,9 @@ module LocalVault
|
|
|
105
118
|
shell.say " localvault demo Create a demo vault to explore commands"
|
|
106
119
|
shell.say ""
|
|
107
120
|
shell.say "SECRETS"
|
|
108
|
-
shell.say " localvault set KEY
|
|
121
|
+
shell.say " printf '%s' \"$SECRET\" | localvault set KEY --stdin"
|
|
122
|
+
shell.say " Store a secret without argv/history exposure"
|
|
123
|
+
shell.say " localvault set KEY VALUE Store a secret (compatibility path)"
|
|
109
124
|
shell.say " localvault set --group G K V Store a secret inside group G"
|
|
110
125
|
shell.say " localvault get KEY Retrieve a secret"
|
|
111
126
|
shell.say " localvault show Display all secrets (masked by default)"
|
|
@@ -169,6 +184,7 @@ module LocalVault
|
|
|
169
184
|
shell.say " localvault login --status Show current login status"
|
|
170
185
|
shell.say " localvault logout Log out"
|
|
171
186
|
shell.say " localvault version Print version"
|
|
187
|
+
shell.say " localvault doctor Check install and PATH readiness"
|
|
172
188
|
shell.say " localvault help [COMMAND] Full help for any command"
|
|
173
189
|
shell.say ""
|
|
174
190
|
end
|
|
@@ -197,10 +213,14 @@ module LocalVault
|
|
|
197
213
|
abort_with e.message
|
|
198
214
|
end
|
|
199
215
|
|
|
200
|
-
desc "set KEY VALUE", "Store a secret (supports dot-notation for nested keys)"
|
|
216
|
+
desc "set KEY [VALUE]", "Store a secret (supports dot-notation for nested keys)"
|
|
201
217
|
long_desc <<~DESC
|
|
202
218
|
Store a secret in the current vault.
|
|
203
219
|
|
|
220
|
+
SAFE INPUT (preferred):
|
|
221
|
+
\x05 printf '%s' "$SECRET" | localvault set KEY --stdin
|
|
222
|
+
\x05 printf '%s' "$SECRET" | localvault set --group GROUP KEY --stdin
|
|
223
|
+
|
|
204
224
|
FLAT KEY (simple):
|
|
205
225
|
\x05 localvault set DATABASE_URL postgres://localhost/myapp
|
|
206
226
|
\x05 localvault set STRIPE_KEY sk_live_abc123
|
|
@@ -215,33 +235,42 @@ module LocalVault
|
|
|
215
235
|
\x05 localvault set GROUP.KEY VALUE
|
|
216
236
|
\x05 localvault groups [QUERY]
|
|
217
237
|
|
|
238
|
+
Positional values may be visible in process lists and shell history.
|
|
218
239
|
The dot separates project from key name. One vault can hold many projects.
|
|
219
240
|
Use `localvault show -p platepose -v vault` to view a single project.
|
|
220
241
|
Use `localvault import` to bulk-load from a .env, .json, or .yml file.
|
|
221
242
|
DESC
|
|
222
243
|
method_option :group, type: :string, desc: "Store KEY and VALUE inside this named group"
|
|
223
|
-
|
|
244
|
+
method_option :stdin, type: :boolean, default: false, desc: "Read VALUE from stdin instead of argv"
|
|
245
|
+
def set(key, value = nil)
|
|
246
|
+
validate_secret_value_source!(value)
|
|
224
247
|
vault = open_vault!
|
|
225
248
|
if options[:group]
|
|
226
249
|
group = canonical_group_name(vault, options[:group])
|
|
227
250
|
validate_group_segment!(group)
|
|
228
251
|
validate_group_segment!(key)
|
|
229
252
|
raise GroupSaveError, :collision if vault.all.key?(group) && !vault.all[group].is_a?(Hash)
|
|
253
|
+
value = read_secret_value(value)
|
|
230
254
|
vault.set("#{group}.#{key}", value)
|
|
231
255
|
$stdout.puts "Set #{key} in group `#{group}` in vault `#{vault.name}`."
|
|
232
256
|
$stdout.puts
|
|
233
257
|
$stdout.puts "Stored as:"
|
|
234
258
|
$stdout.puts " #{group}.#{key}"
|
|
235
259
|
else
|
|
260
|
+
value = read_secret_value(value)
|
|
236
261
|
vault.set(key, value)
|
|
237
262
|
$stdout.puts "Set #{key} in vault '#{vault.name}'"
|
|
238
263
|
end
|
|
264
|
+
rescue StdinSecretInput::InteractiveInput, StdinSecretInput::InvalidEncoding => e
|
|
265
|
+
raise SetValueSourceError.new(:stdin, e.message)
|
|
239
266
|
rescue Vault::InvalidKeyName => e
|
|
240
267
|
raise GroupSaveError, :invalid if options[:group]
|
|
241
268
|
abort_with e.message
|
|
269
|
+
CommandStatus.error
|
|
242
270
|
rescue RuntimeError => e
|
|
243
271
|
raise GroupSaveError, :collision if options[:group]
|
|
244
272
|
abort_with e.message
|
|
273
|
+
CommandStatus.error
|
|
245
274
|
end
|
|
246
275
|
|
|
247
276
|
desc "get KEY", "Retrieve a secret value by key"
|
|
@@ -1463,6 +1492,46 @@ module LocalVault
|
|
|
1463
1492
|
$stdout.puts "localvault #{VERSION}"
|
|
1464
1493
|
end
|
|
1465
1494
|
|
|
1495
|
+
desc "doctor", "Check install and PATH readiness"
|
|
1496
|
+
long_desc <<~DESC
|
|
1497
|
+
Check whether the localvault executable selected by PATH matches the
|
|
1498
|
+
install you expect.
|
|
1499
|
+
|
|
1500
|
+
This catches common Homebrew/asdf shadowing issues after upgrades:
|
|
1501
|
+
\x05 localvault doctor
|
|
1502
|
+
\x05 which -a localvault
|
|
1503
|
+
DESC
|
|
1504
|
+
def doctor
|
|
1505
|
+
paths = localvault_paths
|
|
1506
|
+
warnings = localvault_path_warnings(paths)
|
|
1507
|
+
|
|
1508
|
+
$stdout.puts "LocalVault doctor"
|
|
1509
|
+
$stdout.puts "Version: localvault #{VERSION}"
|
|
1510
|
+
$stdout.puts "Home: #{Config.root_path}"
|
|
1511
|
+
|
|
1512
|
+
if paths.empty?
|
|
1513
|
+
$stdout.puts "Executable selected by PATH: not found"
|
|
1514
|
+
else
|
|
1515
|
+
$stdout.puts "Executable selected by PATH: #{paths.first}"
|
|
1516
|
+
$stdout.puts "All localvault executables on PATH:"
|
|
1517
|
+
paths.each_with_index { |path, index| $stdout.puts " #{index + 1}. #{path}" }
|
|
1518
|
+
end
|
|
1519
|
+
|
|
1520
|
+
if warnings.empty?
|
|
1521
|
+
$stdout.puts "PATH: ok"
|
|
1522
|
+
CommandStatus.ok
|
|
1523
|
+
else
|
|
1524
|
+
$stdout.puts
|
|
1525
|
+
warnings.each { |warning| $stdout.puts "Warning: #{warning}" }
|
|
1526
|
+
$stdout.puts
|
|
1527
|
+
$stdout.puts "Suggested checks:"
|
|
1528
|
+
$stdout.puts " asdf reshim ruby"
|
|
1529
|
+
$stdout.puts " hash -r"
|
|
1530
|
+
$stdout.puts " which -a localvault"
|
|
1531
|
+
CommandStatus.error
|
|
1532
|
+
end
|
|
1533
|
+
end
|
|
1534
|
+
|
|
1466
1535
|
def self.exit_on_failure?
|
|
1467
1536
|
false
|
|
1468
1537
|
end
|
|
@@ -1489,6 +1558,22 @@ module LocalVault
|
|
|
1489
1558
|
|
|
1490
1559
|
private
|
|
1491
1560
|
|
|
1561
|
+
def validate_secret_value_source!(value)
|
|
1562
|
+
if options[:stdin] && !value.nil?
|
|
1563
|
+
raise SetValueSourceError.new(:multiple, "Use either a positional VALUE or --stdin, not both.")
|
|
1564
|
+
end
|
|
1565
|
+
|
|
1566
|
+
return if options[:stdin] || !value.nil?
|
|
1567
|
+
|
|
1568
|
+
raise SetValueSourceError.new(:missing, "Provide a VALUE or read one with --stdin.")
|
|
1569
|
+
end
|
|
1570
|
+
|
|
1571
|
+
def read_secret_value(value)
|
|
1572
|
+
return value unless options[:stdin]
|
|
1573
|
+
|
|
1574
|
+
StdinSecretInput.read($stdin)
|
|
1575
|
+
end
|
|
1576
|
+
|
|
1492
1577
|
def canonical_group_name(vault, supplied)
|
|
1493
1578
|
groups = GroupCatalog.new(vault.all).groups
|
|
1494
1579
|
return supplied if groups.any? { |group| group.name == supplied }
|
|
@@ -1903,14 +1988,47 @@ module LocalVault
|
|
|
1903
1988
|
|
|
1904
1989
|
no_commands do
|
|
1905
1990
|
def find_binary(name)
|
|
1906
|
-
|
|
1907
|
-
path.empty? ? nil : path
|
|
1991
|
+
executable_paths_for(name).first
|
|
1908
1992
|
end
|
|
1909
1993
|
|
|
1910
1994
|
def system_command_exists?(cmd)
|
|
1911
1995
|
!find_binary(cmd).nil?
|
|
1912
1996
|
end
|
|
1913
1997
|
|
|
1998
|
+
def localvault_paths
|
|
1999
|
+
executable_paths_for("localvault")
|
|
2000
|
+
end
|
|
2001
|
+
|
|
2002
|
+
def executable_paths_for(name)
|
|
2003
|
+
ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).filter_map do |directory|
|
|
2004
|
+
next if directory.empty?
|
|
2005
|
+
|
|
2006
|
+
path = File.join(directory, name)
|
|
2007
|
+
path if File.executable?(path) && !File.directory?(path)
|
|
2008
|
+
end.uniq
|
|
2009
|
+
end
|
|
2010
|
+
|
|
2011
|
+
def localvault_path_warnings(paths)
|
|
2012
|
+
warnings = []
|
|
2013
|
+
if paths.empty?
|
|
2014
|
+
warnings << "localvault is not on PATH. Brew upgrades may be installed but unreachable."
|
|
2015
|
+
return warnings
|
|
2016
|
+
end
|
|
2017
|
+
|
|
2018
|
+
if paths.first.include?("/.asdf/shims/") && paths.any? { |path| homebrew_localvault_path?(path) }
|
|
2019
|
+
warnings << "PATH selects an asdf shim before Homebrew localvault. " \
|
|
2020
|
+
"A stale shim can hide the upgraded brew executable."
|
|
2021
|
+
elsif paths.length > 1
|
|
2022
|
+
warnings << "Multiple localvault executables are on PATH. Confirm the first entry is the one you intend."
|
|
2023
|
+
end
|
|
2024
|
+
|
|
2025
|
+
warnings
|
|
2026
|
+
end
|
|
2027
|
+
|
|
2028
|
+
def homebrew_localvault_path?(path)
|
|
2029
|
+
path.start_with?("/opt/homebrew/bin/", "/usr/local/bin/")
|
|
2030
|
+
end
|
|
2031
|
+
|
|
1914
2032
|
def cursor_settings_path
|
|
1915
2033
|
File.expand_path("~/.cursor/mcp.json")
|
|
1916
2034
|
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module LocalVault
|
|
2
|
+
class StdinSecretInput
|
|
3
|
+
class InteractiveInput < StandardError; end
|
|
4
|
+
class InvalidEncoding < StandardError; end
|
|
5
|
+
|
|
6
|
+
PIPE_EXAMPLE = %(printf '%s' "$SECRET" | localvault set KEY --stdin).freeze
|
|
7
|
+
|
|
8
|
+
def self.read(input = $stdin)
|
|
9
|
+
if input.respond_to?(:tty?) && input.tty?
|
|
10
|
+
raise InteractiveInput, "Refusing to read secret from interactive stdin. " \
|
|
11
|
+
"Pipe it instead: #{PIPE_EXAMPLE}"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
input.binmode if input.respond_to?(:binmode)
|
|
15
|
+
value = input.read || ""
|
|
16
|
+
value = value.dup.force_encoding(Encoding::UTF_8)
|
|
17
|
+
raise InvalidEncoding, "Secret value must be valid UTF-8" unless value.valid_encoding?
|
|
18
|
+
|
|
19
|
+
value
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
data/lib/localvault/version.rb
CHANGED
data/lib/localvault.rb
CHANGED
|
@@ -5,6 +5,7 @@ require_relative "localvault/store"
|
|
|
5
5
|
require_relative "localvault/env_projection"
|
|
6
6
|
require_relative "localvault/key_lookup"
|
|
7
7
|
require_relative "localvault/vault_resolver"
|
|
8
|
+
require_relative "localvault/stdin_secret_input"
|
|
8
9
|
require_relative "localvault/vault"
|
|
9
10
|
require_relative "localvault/identity"
|
|
10
11
|
require_relative "localvault/share_crypto"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: localvault
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.8.
|
|
4
|
+
version: 1.8.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nauman Tariq
|
|
@@ -126,6 +126,7 @@ files:
|
|
|
126
126
|
- lib/localvault/mcp/tools.rb
|
|
127
127
|
- lib/localvault/session_cache.rb
|
|
128
128
|
- lib/localvault/share_crypto.rb
|
|
129
|
+
- lib/localvault/stdin_secret_input.rb
|
|
129
130
|
- lib/localvault/store.rb
|
|
130
131
|
- lib/localvault/sync_bundle.rb
|
|
131
132
|
- lib/localvault/sync_state.rb
|