Class: LocalVault::CLI::Sync
- Inherits:
-
Thor
- Object
- Thor
- LocalVault::CLI::Sync
- Defined in:
- lib/localvault/cli/sync.rb
Class Method Summary collapse
Instance Method Summary collapse
-
#all ⇒ Object
Smart bidirectional sync for all vaults.
- #pull(vault_name = nil) ⇒ Object
- #push(vault_name = nil) ⇒ Object
- #status ⇒ Object
Class Method Details
.exit_on_failure? ⇒ Boolean
168 169 170 |
# File 'lib/localvault/cli/sync.rb', line 168 def self.exit_on_failure? true end |
Instance Method Details
#all ⇒ Object
Smart bidirectional sync for all vaults.
Uses per-vault .sync_state files (written by push/pull) to track the last-synced checksum and detect what changed on each side:
-
Local-only vault → push
-
Remote-only vault → pull
-
Both exist, only local changed → push
-
Both exist, only remote changed → pull
-
Both exist, neither changed → skip
-
Both exist, no baseline but secrets identical → adopt (record baseline)
-
Both exist, both changed → CONFLICT (manual resolution)
-
Shared vault (not owned by you) → pull-only
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 |
# File 'lib/localvault/cli/sync.rb', line 23 def all return unless logged_in? client = ApiClient.new(token: Config.token) my_handle = Config.inventlist_handle result = client.list_vaults remote_map = (result["vaults"] || []).each_with_object({}) { |v, h| h[v["name"]] = v } local_set = Store.list_vaults.to_set all_names = (remote_map.keys + local_set.to_a).uniq.sort if all_names.empty? $stdout.puts "No vaults to sync." return end plan = all_names.map { |name| classify_vault(name, local_set, remote_map, my_handle, client) } # Print plan max_name = (["Vault"] + plan.map { |p| p[:name] }).map(&:length).max max_action = (["Action"] + plan.map { |p| p[:action].to_s }).map(&:length).max $stdout.puts $stdout.puts " #{"Vault".ljust(max_name)} #{"Action".ljust(max_action)} Reason" $stdout.puts " #{"─" * max_name} #{"─" * max_action} ──────" plan.each do |p| label = p[:action] == :conflict ? "CONFLICT" : p[:action].to_s $stdout.puts " #{p[:name].ljust(max_name)} #{label.ljust(max_action)} #{p[:reason]}" end $stdout.puts if [:dry_run] $stdout.puts "Dry run — no changes made." return end # Execute pushed = pulled = skipped = adopted = conflicts = errors = 0 plan.each do |entry| case entry[:action] when :push if perform_push(entry[:name], client) pushed += 1 else errors += 1 end when :pull if perform_pull(entry[:name], client, force: true) pulled += 1 else errors += 1 end when :adopt if perform_adopt(entry[:name]) adopted += 1 else errors += 1 end when :skip skipped += 1 when :conflict conflicts += 1 end end # Summary parts = [] parts << "#{pushed} pushed" if pushed > 0 parts << "#{pulled} pulled" if pulled > 0 parts << "#{adopted} baselined" if adopted > 0 parts << "#{skipped} up to date" if skipped > 0 parts << "#{errors} failed" if errors > 0 parts << "#{conflicts} conflict#{conflicts == 1 ? "" : "s"}" if conflicts > 0 $stdout.puts "Summary: #{parts.join(", ")}" # Conflict guidance if conflicts > 0 $stdout.puts plan.select { |p| p[:action] == :conflict }.each do |p| $stderr.puts " #{p[:name]} — #{p[:reason]}" $stderr.puts " Resolve with:" $stderr.puts " localvault sync push #{p[:name]} (keep local, overwrite remote)" $stderr.puts " localvault sync pull #{p[:name]} --force (keep remote, overwrite local)" end end rescue ApiClient::ApiError => e $stderr.puts "Error: #{e.}" end |
#pull(vault_name = nil) ⇒ Object
123 124 125 126 127 128 |
# File 'lib/localvault/cli/sync.rb', line 123 def pull(vault_name = nil) return unless logged_in? vault_name ||= Config.default_vault client = ApiClient.new(token: Config.token) perform_pull(vault_name, client, force: [:force]) end |
#push(vault_name = nil) ⇒ Object
114 115 116 117 118 119 |
# File 'lib/localvault/cli/sync.rb', line 114 def push(vault_name = nil) return unless logged_in? vault_name ||= Config.default_vault client = ApiClient.new(token: Config.token) perform_push(vault_name, client) end |
#status ⇒ Object
131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 |
# File 'lib/localvault/cli/sync.rb', line 131 def status return unless logged_in? client = ApiClient.new(token: Config.token) result = client.list_vaults remote = (result["vaults"] || []).each_with_object({}) { |v, h| h[v["name"]] = v } local_set = Store.list_vaults.to_set all_names = (remote.keys + local_set.to_a).uniq.sort if all_names.empty? $stdout.puts "No vaults found locally or in cloud." return end rows = all_names.map do |name| r = remote[name] l_exists = local_set.include?(name) row_status = if r && l_exists then "synced" elsif r then "remote only" else "local only" end synced_at = r ? (r["synced_at"]&.slice(0, 10) || "—") : "—" [name, row_status, synced_at] end max_name = (["Vault"] + rows.map { |r| r[0] }).map(&:length).max max_status = (["Status"] + rows.map { |r| r[1] }).map(&:length).max $stdout.puts "#{"Vault".ljust(max_name)} #{"Status".ljust(max_status)} Synced At" $stdout.puts "#{"─" * max_name} #{"─" * max_status} ─────────" rows.each do |name, row_status, synced_at| $stdout.puts "#{name.ljust(max_name)} #{row_status.ljust(max_status)} #{synced_at}" end rescue ApiClient::ApiError => e $stderr.puts "Error: #{e.}" end |