localvault 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/localvault/cli.rb +177 -0
- data/lib/localvault/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1c3bc70e3ad51b1b7ddebc905fac13e29e3a9c817e4da80e6bf137ab0ae5a0d3
|
|
4
|
+
data.tar.gz: 1db6e6edb29546b72ae8105f1e811e655bae036f37af5a1c3a7a362189c0e089
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c10706f2b8bb9e7471ff4cb6e03dc59fe26e6ea87d64c4438e3183a4917b03989e862b5ee76639f108e6e7ae445f29c3246b97a7cacdfa90517a369a2b613b5a
|
|
7
|
+
data.tar.gz: 9ebdb6b7c4918335e938508b8e48e3a0d7874b6d7eec6320aa2f1178643c77440d9987779836b5ca48018b0531772120d0283e1e1106764c8387a2a8f23d0041
|
data/lib/localvault/cli.rb
CHANGED
|
@@ -48,6 +48,7 @@ module LocalVault
|
|
|
48
48
|
shell.say " localvault sync SUBCOMMAND See `localvault help sync` for full sync reference"
|
|
49
49
|
shell.say ""
|
|
50
50
|
shell.say "TEAM SHARING (requires localvault login)"
|
|
51
|
+
shell.say " localvault dashboard Aggregate view: owned vaults, vaults shared with you, legacy shares"
|
|
51
52
|
shell.say " localvault verify @HANDLE Check if a person has a published public key"
|
|
52
53
|
shell.say " localvault add @HANDLE Add teammate (use --scope KEY... for partial access)"
|
|
53
54
|
shell.say " localvault remove @HANDLE Remove teammate (--scope KEY to strip one key, --rotate to re-key)"
|
|
@@ -979,6 +980,125 @@ module LocalVault
|
|
|
979
980
|
$stderr.puts "Error: #{e.message}"
|
|
980
981
|
end
|
|
981
982
|
|
|
983
|
+
desc "dashboard", "Show who has access to which vaults, and what's shared with you"
|
|
984
|
+
long_desc <<~DESC
|
|
985
|
+
Aggregate view across every vault you can see on InventList. Sections:
|
|
986
|
+
|
|
987
|
+
\x05OWNED BY YOU — team vaults where you are the owner, with all members + their scopes
|
|
988
|
+
\x05SHARED WITH YOU — team vaults where someone else is the owner and you're a member
|
|
989
|
+
\x05LEGACY DIRECT SHARES — pre-v1.2 one-shot direct shares (outgoing + incoming)
|
|
990
|
+
|
|
991
|
+
Unlike `team list [VAULT]`, which shows a single vault's members, this
|
|
992
|
+
gives you one screen that answers: who can see my stuff, and whose
|
|
993
|
+
stuff can I see.
|
|
994
|
+
DESC
|
|
995
|
+
def dashboard
|
|
996
|
+
unless Config.token
|
|
997
|
+
$stderr.puts "Error: Not logged in."
|
|
998
|
+
$stderr.puts "\n localvault login YOUR_TOKEN\n"
|
|
999
|
+
$stderr.puts "Get your token at: https://inventlist.com/@YOUR_HANDLE/edit#developer"
|
|
1000
|
+
return
|
|
1001
|
+
end
|
|
1002
|
+
|
|
1003
|
+
client = ApiClient.new(token: Config.token)
|
|
1004
|
+
my_handle = Config.inventlist_handle
|
|
1005
|
+
|
|
1006
|
+
begin
|
|
1007
|
+
list = client.list_vaults
|
|
1008
|
+
rescue ApiClient::ApiError => e
|
|
1009
|
+
$stderr.puts "Error: #{e.message}"
|
|
1010
|
+
return
|
|
1011
|
+
end
|
|
1012
|
+
|
|
1013
|
+
vaults = list["vaults"] || []
|
|
1014
|
+
owned = []
|
|
1015
|
+
shared = []
|
|
1016
|
+
skipped = []
|
|
1017
|
+
|
|
1018
|
+
vaults.each do |v|
|
|
1019
|
+
name = v["name"]
|
|
1020
|
+
next unless name
|
|
1021
|
+
|
|
1022
|
+
begin
|
|
1023
|
+
blob = client.pull_vault(name)
|
|
1024
|
+
rescue ApiClient::ApiError => e
|
|
1025
|
+
skipped << [name, e.message]
|
|
1026
|
+
next
|
|
1027
|
+
end
|
|
1028
|
+
|
|
1029
|
+
next if blob.nil? || blob.empty?
|
|
1030
|
+
|
|
1031
|
+
begin
|
|
1032
|
+
data = SyncBundle.unpack(blob)
|
|
1033
|
+
rescue SyncBundle::UnpackError => e
|
|
1034
|
+
skipped << [name, e.message]
|
|
1035
|
+
next
|
|
1036
|
+
end
|
|
1037
|
+
|
|
1038
|
+
owner = data[:owner] || v["owner_handle"]
|
|
1039
|
+
row = {
|
|
1040
|
+
name: name,
|
|
1041
|
+
owner: owner,
|
|
1042
|
+
key_slots: data[:key_slots] || {},
|
|
1043
|
+
is_team: !owner.nil?,
|
|
1044
|
+
remote_shared: v["shared"] == true
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
if owner && owner == my_handle
|
|
1048
|
+
owned << row
|
|
1049
|
+
elsif v["shared"] == true || (owner && owner != my_handle)
|
|
1050
|
+
shared << row
|
|
1051
|
+
else
|
|
1052
|
+
# v1 personal vault (no owner) — treat as owned (it's yours)
|
|
1053
|
+
owned << row
|
|
1054
|
+
end
|
|
1055
|
+
end
|
|
1056
|
+
|
|
1057
|
+
# ── OWNED BY YOU ──
|
|
1058
|
+
$stdout.puts
|
|
1059
|
+
$stdout.puts VAULT_STYLE.render("OWNED BY YOU") + " " + COUNT_STYLE.render("(#{owned.size} vault#{owned.size == 1 ? "" : "s"})")
|
|
1060
|
+
$stdout.puts
|
|
1061
|
+
if owned.empty?
|
|
1062
|
+
$stdout.puts " " + COUNT_STYLE.render("No vaults owned yet. Create one with `localvault init NAME`.")
|
|
1063
|
+
else
|
|
1064
|
+
owned.sort_by { |r| r[:name] }.each { |row| render_dashboard_vault(row, my_handle: my_handle) }
|
|
1065
|
+
end
|
|
1066
|
+
|
|
1067
|
+
# ── SHARED WITH YOU ──
|
|
1068
|
+
$stdout.puts
|
|
1069
|
+
$stdout.puts VAULT_STYLE.render("SHARED WITH YOU") + " " + COUNT_STYLE.render("(#{shared.size} vault#{shared.size == 1 ? "" : "s"})")
|
|
1070
|
+
$stdout.puts
|
|
1071
|
+
if shared.empty?
|
|
1072
|
+
$stdout.puts " " + COUNT_STYLE.render("No vaults shared with you.")
|
|
1073
|
+
else
|
|
1074
|
+
shared.sort_by { |r| r[:name] }.each { |row| render_dashboard_vault(row, my_handle: my_handle) }
|
|
1075
|
+
end
|
|
1076
|
+
|
|
1077
|
+
# ── LEGACY DIRECT SHARES ──
|
|
1078
|
+
sent = safe_fetch_shares { client.sent_shares }
|
|
1079
|
+
pending = safe_fetch_shares { client.pending_shares }
|
|
1080
|
+
outgoing_count = (sent["shares"] || []).reject { |s| s["status"] == "revoked" }.size
|
|
1081
|
+
pending_count = (pending["shares"] || []).size
|
|
1082
|
+
|
|
1083
|
+
$stdout.puts
|
|
1084
|
+
$stdout.puts VAULT_STYLE.render("LEGACY DIRECT SHARES") + " " + COUNT_STYLE.render("(pre-v1.2 fallback)")
|
|
1085
|
+
$stdout.puts " outgoing: #{outgoing_count} pending: #{pending_count}"
|
|
1086
|
+
if outgoing_count + pending_count > 0
|
|
1087
|
+
$stdout.puts " " + COUNT_STYLE.render("Manage with `localvault receive`, `localvault revoke SHARE_ID`.")
|
|
1088
|
+
end
|
|
1089
|
+
|
|
1090
|
+
# ── Skipped ──
|
|
1091
|
+
unless skipped.empty?
|
|
1092
|
+
$stdout.puts
|
|
1093
|
+
$stderr.puts "Note: #{skipped.size} vault(s) could not be loaded:"
|
|
1094
|
+
skipped.each do |name, reason|
|
|
1095
|
+
$stderr.puts " #{name}: #{reason}"
|
|
1096
|
+
end
|
|
1097
|
+
end
|
|
1098
|
+
|
|
1099
|
+
$stdout.puts
|
|
1100
|
+
end
|
|
1101
|
+
|
|
982
1102
|
desc "import FILE", "Bulk-import secrets from a .env, .json, or .yml file"
|
|
983
1103
|
long_desc <<~DESC
|
|
984
1104
|
Import all secrets from a file into a vault. Supports .env, .json, and .yml.
|
|
@@ -1269,6 +1389,63 @@ module LocalVault
|
|
|
1269
1389
|
end
|
|
1270
1390
|
end
|
|
1271
1391
|
|
|
1392
|
+
# Render one vault row for `localvault dashboard`, as a Lipgloss table
|
|
1393
|
+
# of its members (handle, access, scopes). Style mirrors `show`'s
|
|
1394
|
+
# render_table — rounded border, purple header, alternating rows.
|
|
1395
|
+
def render_dashboard_vault(row, my_handle:)
|
|
1396
|
+
slots = row[:key_slots]
|
|
1397
|
+
valid = slots.select { |_, v| v.is_a?(Hash) && v["pub"].is_a?(String) }
|
|
1398
|
+
|
|
1399
|
+
count_label = "#{valid.size} member#{valid.size == 1 ? "" : "s"}"
|
|
1400
|
+
owner_label = row[:owner] ? "owner @#{row[:owner]}" : "personal (v1 bundle, no team access)"
|
|
1401
|
+
$stdout.puts " " + GROUP_STYLE.render(row[:name]) + " " + COUNT_STYLE.render("#{count_label} · #{owner_label}")
|
|
1402
|
+
|
|
1403
|
+
if !row[:is_team]
|
|
1404
|
+
# v1 personal bundle — no members to list
|
|
1405
|
+
$stdout.puts " " + COUNT_STYLE.render("No team members. Convert with `localvault team init #{row[:name]}` to share.")
|
|
1406
|
+
$stdout.puts
|
|
1407
|
+
return
|
|
1408
|
+
end
|
|
1409
|
+
|
|
1410
|
+
if valid.empty?
|
|
1411
|
+
$stdout.puts " " + COUNT_STYLE.render("No members yet. Add someone with `localvault add @HANDLE -v #{row[:name]}`.")
|
|
1412
|
+
$stdout.puts
|
|
1413
|
+
return
|
|
1414
|
+
end
|
|
1415
|
+
|
|
1416
|
+
rows = valid.sort.map do |handle, slot|
|
|
1417
|
+
marker = handle == my_handle ? " (you)" : ""
|
|
1418
|
+
access = slot["scopes"].is_a?(Array) ? "scoped" : "full"
|
|
1419
|
+
scopes = slot["scopes"].is_a?(Array) ? slot["scopes"].join(", ") : "—"
|
|
1420
|
+
["@#{handle}#{marker}", access, scopes]
|
|
1421
|
+
end
|
|
1422
|
+
|
|
1423
|
+
require "lipgloss"
|
|
1424
|
+
table = Lipgloss::Table.new
|
|
1425
|
+
.headers(["Member", "Access", "Scopes"])
|
|
1426
|
+
.rows(rows)
|
|
1427
|
+
.border(:rounded)
|
|
1428
|
+
.style_func(rows: rows.size, columns: 3) do |row_idx, _col|
|
|
1429
|
+
if row_idx == Lipgloss::Table::HEADER_ROW
|
|
1430
|
+
HEADER_STYLE
|
|
1431
|
+
else
|
|
1432
|
+
row_idx.odd? ? ODD_STYLE : EVEN_STYLE
|
|
1433
|
+
end
|
|
1434
|
+
end
|
|
1435
|
+
.render
|
|
1436
|
+
|
|
1437
|
+
$stdout.puts table
|
|
1438
|
+
$stdout.puts
|
|
1439
|
+
end
|
|
1440
|
+
|
|
1441
|
+
# Fetch shares, swallowing API errors so the dashboard never bails
|
|
1442
|
+
# because the legacy endpoint hiccuped.
|
|
1443
|
+
def safe_fetch_shares
|
|
1444
|
+
yield
|
|
1445
|
+
rescue ApiClient::ApiError
|
|
1446
|
+
{ "shares" => [] }
|
|
1447
|
+
end
|
|
1448
|
+
|
|
1272
1449
|
def parse_import_file(file)
|
|
1273
1450
|
ext = File.extname(file).downcase
|
|
1274
1451
|
case ext
|
data/lib/localvault/version.rb
CHANGED