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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6ec85a7f67eb8705f2078a55eeeb1a24997f61e965a76284ef9a9eb314eef032
4
- data.tar.gz: d799cbfabdced0595eb91132e35daa2930f19a705cc189357cb9a5c3b794cc05
3
+ metadata.gz: 1c3bc70e3ad51b1b7ddebc905fac13e29e3a9c817e4da80e6bf137ab0ae5a0d3
4
+ data.tar.gz: 1db6e6edb29546b72ae8105f1e811e655bae036f37af5a1c3a7a362189c0e089
5
5
  SHA512:
6
- metadata.gz: 74123bc8d783ace6773c11223486974d7210870903d5901d0f1c3410266d7d9c29d2ffe9f0bead2137629b3132495854c490c1a771becf06355a26b527afa435
7
- data.tar.gz: b95d606abec4fe145d23b7028a5091f4ec1fab3a9b5d6df3a8ae201b238b9a334a8c07033892a6d3ae36d73f1e116cc3f0c72dcde955f1109f09d58405025021
6
+ metadata.gz: c10706f2b8bb9e7471ff4cb6e03dc59fe26e6ea87d64c4438e3183a4917b03989e862b5ee76639f108e6e7ae445f29c3246b97a7cacdfa90517a369a2b613b5a
7
+ data.tar.gz: 9ebdb6b7c4918335e938508b8e48e3a0d7874b6d7eec6320aa2f1178643c77440d9987779836b5ca48018b0531772120d0283e1e1106764c8387a2a8f23d0041
@@ -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
@@ -1,3 +1,3 @@
1
1
  module LocalVault
2
- VERSION = "1.4.0"
2
+ VERSION = "1.5.0"
3
3
  end
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.4.0
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nauman Tariq