vaultkit 1.0.6 → 1.0.7

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: c99e29a96bcd6b84bf3de1322b79f256118f5025918ef7573563d66f42ad9b93
4
- data.tar.gz: bb09e5336124994c39127859bc9cb978b86cb6c4d5a2a610080f608f262f4c6a
3
+ metadata.gz: cba4a6b29805455820a7137e864da2d9637bd59e2f4be377bd51875836ee6d5f
4
+ data.tar.gz: 33672d087e2cf391855bcd839162b706fab6908daebbf45504d67f7ed4093ff3
5
5
  SHA512:
6
- metadata.gz: 1899ddef1628cf6d67e394b5d2c7e616e25a6c17e7ee86c609beff0cf02c8339e2611e2b78a55438312a59e3e51389cf5163e55089efc5a4a498548077837f63
7
- data.tar.gz: 0a2b23c898735fc6b706549d8e45872bc976a00bc4750843115f3eaf825b587595a40ed5095b72dc57f8c61e292ef489e478754bd92f5459e57aa97bf5048e73
6
+ metadata.gz: 3e13e955409aa362de4bb76a526698c0157378dc77c803e80cd0d5c7d5bab39296d4dde4e04b55592c2e1f1ba6d4b20de9ad847263536c69baea791d643a6c1d
7
+ data.tar.gz: b355ce713edba89b7d293499489c0b0f4aaae614ca5f3af0249e31a043d1efbb25aa4359f710b86d9dca585bbe7b4b1f7651ac9d2e454197323a6a7bae347b61
@@ -169,6 +169,33 @@ module Vkit
169
169
  )
170
170
  end
171
171
 
172
+ # REGISTRY
173
+ desc "registry SUBCOMMAND ...ARGS", "Manage local registry.yaml"
174
+ subcommand "registry", Class.new(Thor) {
175
+
176
+ desc "export", "Export runtime registry to datasets/registry.yaml"
177
+ option :dir, type: :string, default: "."
178
+ option :out, type: :string, desc: "Custom output path"
179
+ option :force, type: :boolean, default: false
180
+ def export
181
+ Commands::RegistryExportCommand.new.call(
182
+ dir: options[:dir],
183
+ out: options[:out],
184
+ force: options[:force]
185
+ )
186
+ end
187
+
188
+ desc "diff", "Diff local registry.yaml against runtime registry"
189
+ option :dir, type: :string, default: "."
190
+ option :format, type: :string, default: "human", enum: %w[human json]
191
+ def diff
192
+ Commands::RegistryDiffCommand.new.call(
193
+ dir: options[:dir],
194
+ format: options[:format]
195
+ )
196
+ end
197
+ }
198
+
172
199
  # POLICY
173
200
  desc "policy SUBCOMMAND ...ARGS", "Manage policy bundles"
174
201
  subcommand "policy", Class.new(Thor) {
@@ -59,15 +59,25 @@ module Vkit
59
59
  FileUtils.mkdir_p(File.join(dir, "datasets"))
60
60
  FileUtils.mkdir_p(File.join(dir, "dist"))
61
61
  FileUtils.mkdir_p(File.join(dir, ".vkit"))
62
-
62
+
63
63
  registry_path = File.join(dir, "datasets", "registry.yaml")
64
- unless File.exist?(registry_path)
64
+
65
+ if File.exist?(registry_path)
66
+ puts "ℹ️ datasets/registry.yaml already exists — leaving unchanged."
67
+ else
65
68
  File.write(
66
69
  registry_path,
67
- "# Dataset registry\n# Populate with: vkit scan <datasource> --apply\n"
70
+ <<~YAML
71
+ # VaultKit Dataset Registry
72
+ #
73
+ # Populate with:
74
+ # vkit scan <datasource> --apply
75
+ # vkit registry export
76
+ #
77
+ YAML
68
78
  )
69
79
  end
70
-
80
+
71
81
  gitignore_path = File.join(dir, ".gitignore")
72
82
  unless File.exist?(gitignore_path)
73
83
  File.write(
@@ -75,7 +85,7 @@ module Vkit
75
85
  "dist/\n.vkit/\n"
76
86
  )
77
87
  end
78
- end
88
+ end
79
89
  end
80
90
  end
81
91
  end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "json"
5
+ require_relative "../../core/registry_diff"
6
+ require_relative "../../core/registry_diff_printer"
7
+
8
+ module Vkit
9
+ module CLI
10
+ module Commands
11
+ class RegistryDiffCommand < BaseCommand
12
+ DEFAULT_PATH = File.join("datasets", "registry.yaml")
13
+
14
+ def call(dir:, format: "human")
15
+ ensure_project!(dir)
16
+
17
+ dir = File.expand_path(dir)
18
+ path = File.join(dir, DEFAULT_PATH)
19
+
20
+ unless File.exist?(path)
21
+ puts "❌ No local registry.yaml found."
22
+ exit 2
23
+ end
24
+
25
+ with_auth do
26
+ user = credential_store.user
27
+ org = user["organization_slug"]
28
+
29
+ local = YAML.safe_load(
30
+ File.read(path),
31
+ permitted_classes: [],
32
+ permitted_symbols: [],
33
+ aliases: false
34
+ )
35
+ runtime = authenticated_client.get(
36
+ "/api/v1/orgs/#{org}/registries/export"
37
+ )
38
+
39
+ diff = Vkit::Core::RegistryDiff.compute(
40
+ local: local,
41
+ remote: runtime
42
+ )
43
+
44
+ if format == "json"
45
+ puts JSON.pretty_generate(diff)
46
+ exit diff["datasets"].empty? ? 0 : 1
47
+ else
48
+ Vkit::Core::RegistryDiffPrinter.print(diff)
49
+ exit diff["datasets"].empty? ? 0 : 1
50
+ end
51
+ end
52
+
53
+ rescue StandardError => e
54
+ puts "❌ Error: #{e.message}"
55
+ exit 2
56
+ end
57
+
58
+ private
59
+
60
+ def ensure_project!(dir)
61
+ raise "Not a VaultKit project (missing .vkit/)" unless Dir.exist?(File.join(dir, ".vkit"))
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require_relative "base_command"
5
+
6
+ module Vkit
7
+ module CLI
8
+ module Commands
9
+ class RegistryExportCommand < BaseCommand
10
+ DEFAULT_PATH = File.join("datasets", "registry.yaml")
11
+
12
+ def call(dir:, out: nil, force: false)
13
+ with_auth do
14
+ ensure_project!(dir)
15
+
16
+ dir = File.expand_path(dir)
17
+ path = out ? File.expand_path(out, dir) : File.join(dir, DEFAULT_PATH)
18
+
19
+ if File.exist?(path) && !force
20
+ decision = handle_existing_registry(dir: dir, path: path)
21
+ return if decision == :abort
22
+ end
23
+
24
+ user = credential_store.user
25
+ org = user["organization_slug"]
26
+
27
+ registry_data = authenticated_client.get(
28
+ "/api/v1/orgs/#{org}/registries/export"
29
+ )
30
+
31
+ FileUtils.mkdir_p(File.dirname(path))
32
+ File.write(path, registry_data.to_yaml)
33
+
34
+ puts "✅ Registry exported to:"
35
+ puts " #{relative(dir, path)}"
36
+ end
37
+ rescue StandardError => e
38
+ puts "❌ Error: #{e.message}"
39
+ exit 1
40
+ end
41
+
42
+ private
43
+
44
+ def ensure_project!(dir)
45
+ raise "Not a VaultKit project (missing .vkit/)" unless Dir.exist?(File.join(dir, ".vkit"))
46
+ end
47
+
48
+ def handle_existing_registry(dir:, path:)
49
+ puts "\n⚠️ Existing registry.yaml detected at:"
50
+ puts " #{relative(dir, path)}\n\n"
51
+
52
+ puts "Choose an option:"
53
+ puts " 1) Overwrite entirely"
54
+ puts " 2) Show diff"
55
+ puts " 3) Abort"
56
+ print "\nEnter choice [1-3]: "
57
+
58
+ choice = STDIN.gets&.strip
59
+
60
+ case choice
61
+ when "1"
62
+ puts "\nOverwriting existing registry..."
63
+ :overwrite
64
+
65
+ when "2"
66
+ show_diff(dir: dir)
67
+ puts "\nAborting."
68
+ :abort
69
+
70
+ else
71
+ puts "\nAborting."
72
+ :abort
73
+ end
74
+ end
75
+
76
+ def relative(root, abs)
77
+ abs.sub(root + File::SEPARATOR, "")
78
+ end
79
+
80
+ def show_diff(dir:)
81
+ Vkit::CLI::Commands::RegistryDiffCommand.new.call(dir: dir)
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vkit
4
+ module Core
5
+ module Ansi
6
+ COLORS = {
7
+ red: 31,
8
+ green: 32,
9
+ yellow: 33,
10
+ blue: 34,
11
+ gray: 90
12
+ }.freeze
13
+
14
+ def self.color(text, color)
15
+ return text unless $stdout.tty?
16
+
17
+ code = COLORS[color]
18
+ "\e[#{code}m#{text}\e[0m"
19
+ end
20
+
21
+ def self.green(text) = color(text, :green)
22
+ def self.red(text) = color(text, :red)
23
+ def self.yellow(text) = color(text, :yellow)
24
+ def self.blue(text) = color(text, :blue)
25
+ def self.gray(text) = color(text, :gray)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vkit
4
+ module Core
5
+ class RegistryDiff
6
+ def self.compute(local:, remote:)
7
+ local ||= {}
8
+ remote ||= {}
9
+
10
+ local_datasets = normalize(local)
11
+ remote_datasets = normalize(remote)
12
+
13
+ out = { "datasets" => [] }
14
+
15
+ all_names = (local_datasets.keys | remote_datasets.keys).sort
16
+
17
+ all_names.each do |name|
18
+ local_fields = local_datasets[name] || []
19
+ remote_fields = remote_datasets[name] || []
20
+
21
+ changes = diff_fields(local_fields, remote_fields)
22
+
23
+ next if changes.values.all?(&:empty?)
24
+
25
+ out["datasets"] << {
26
+ "name" => name,
27
+ "changes" => changes
28
+ }
29
+ end
30
+
31
+ out
32
+ end
33
+
34
+ def self.normalize(registry)
35
+ return {} unless registry.is_a?(Hash)
36
+
37
+ # CASE 1: runtime export format
38
+ if registry.key?("datasets")
39
+ return Array(registry["datasets"]).each_with_object({}) do |ds, h|
40
+ h[ds["name"]] =
41
+ Array(ds["fields"]).map do |f|
42
+ {
43
+ "name" => f["name"].to_s,
44
+ "type" => f["type"].to_s,
45
+ "sensitivity" => f["sensitivity"].to_s,
46
+ "tags" => Array(f["tags"]).map(&:to_s).sort
47
+ }
48
+ end.sort_by { |f| f["name"] }
49
+ end
50
+ end
51
+
52
+ # CASE 2: local YAML format
53
+ registry.each_with_object({}) do |(dataset_name, ds), h|
54
+ fields = ds["fields"] || {}
55
+
56
+ normalized_fields =
57
+ fields.map do |field_name, meta|
58
+ {
59
+ "name" => field_name.to_s,
60
+ "type" => meta["type"].to_s,
61
+ "sensitivity" => meta["sensitivity"].to_s,
62
+ "tags" => [meta["category"]].compact.map(&:to_s).sort
63
+ }
64
+ end.sort_by { |f| f["name"] }
65
+
66
+ h[dataset_name.to_s] = normalized_fields
67
+ end
68
+ end
69
+
70
+
71
+ def self.diff_fields(local, remote)
72
+ l = local.each_with_object({}) { |f, h| h[f["name"]] = f }
73
+ r = remote.each_with_object({}) { |f, h| h[f["name"]] = f }
74
+
75
+ {
76
+ "added_fields" =>
77
+ (r.keys - l.keys).map { |k| r[k] },
78
+
79
+ "removed_fields" =>
80
+ (l.keys - r.keys).map { |k| l[k] },
81
+
82
+ "changed_fields" =>
83
+ (l.keys & r.keys).filter_map do |k|
84
+ next if l[k] == r[k]
85
+ { "name" => k, "local" => l[k], "remote" => r[k] }
86
+ end
87
+ }
88
+ end
89
+
90
+ private_class_method :diff_fields, :normalize
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "ansi"
4
+
5
+ module Vkit
6
+ module Core
7
+ class RegistryDiffPrinter
8
+ def self.print(diff)
9
+ datasets = diff["datasets"] || []
10
+
11
+ summary = summarize(datasets)
12
+
13
+ if summary[:total_changes].zero?
14
+ puts Ansi.green("✓ No differences between local registry and runtime.")
15
+ return
16
+ end
17
+
18
+ print_summary(summary)
19
+ puts
20
+
21
+ datasets.each do |ds|
22
+ changes = ds["changes"]
23
+ next if changes.values.all?(&:empty?)
24
+
25
+ puts Ansi.blue("Dataset: #{ds['name']}")
26
+ puts
27
+
28
+ print_added(changes["added_fields"])
29
+ print_removed(changes["removed_fields"])
30
+ print_changed(changes["changed_fields"])
31
+
32
+ puts "-" * 50
33
+ end
34
+ end
35
+
36
+ def self.summarize(datasets)
37
+ datasets.each_with_object(
38
+ datasets_changed: 0,
39
+ added: 0,
40
+ removed: 0,
41
+ changed: 0,
42
+ total_changes: 0
43
+ ) do |ds, acc|
44
+ changes = ds["changes"] || {}
45
+
46
+ a = Array(changes["added_fields"]).size
47
+ r = Array(changes["removed_fields"]).size
48
+ c = Array(changes["changed_fields"]).size
49
+
50
+ next if a + r + c == 0
51
+
52
+ acc[:datasets_changed] += 1
53
+ acc[:added] += a
54
+ acc[:removed] += r
55
+ acc[:changed] += c
56
+ acc[:total_changes] += (a + r + c)
57
+ end
58
+ end
59
+
60
+ def self.print_summary(summary)
61
+ line = [
62
+ "#{summary[:datasets_changed]} datasets changed",
63
+ "#{summary[:added]} added",
64
+ "#{summary[:removed]} removed",
65
+ "#{summary[:changed]} modified"
66
+ ].join(" | ")
67
+
68
+ puts Ansi.yellow(line)
69
+ end
70
+
71
+ def self.print_added(fields)
72
+ return if fields.empty?
73
+
74
+ puts Ansi.green(" + Added Fields:")
75
+ fields.each do |f|
76
+ puts Ansi.green(" + #{f['name']} (#{f['type']})")
77
+ end
78
+ puts
79
+ end
80
+
81
+ def self.print_removed(fields)
82
+ return if fields.empty?
83
+
84
+ puts Ansi.red(" - Removed Fields:")
85
+ fields.each do |f|
86
+ puts Ansi.red(" - #{f['name']} (#{f['type']})")
87
+ end
88
+ puts
89
+ end
90
+
91
+ def self.print_changed(fields)
92
+ return if fields.empty?
93
+
94
+ puts Ansi.yellow(" ~ Changed Fields:")
95
+ fields.each do |f|
96
+ puts Ansi.yellow(" ~ #{f['name']}")
97
+ puts Ansi.gray(" From: #{f['from']}")
98
+ puts Ansi.gray(" To: #{f['to']}")
99
+ end
100
+ puts
101
+ end
102
+
103
+ private_class_method :summarize,
104
+ :print_summary,
105
+ :print_added,
106
+ :print_removed,
107
+ :print_changed
108
+ end
109
+ end
110
+ end
data/lib/vkit/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Vkit
4
- VERSION = "1.0.6"
4
+ VERSION = "1.0.7"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vaultkit
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.6
4
+ version: 1.0.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nnamdi Ogundu
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-02-16 00:00:00.000000000 Z
11
+ date: 2026-02-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -61,6 +61,8 @@ files:
61
61
  - lib/vkit/cli/commands/policy_pack_upgrade_command.rb
62
62
  - lib/vkit/cli/commands/policy_revoke_command.rb
63
63
  - lib/vkit/cli/commands/policy_validate_command.rb
64
+ - lib/vkit/cli/commands/registry_diff_command.rb
65
+ - lib/vkit/cli/commands/registry_export_command.rb
64
66
  - lib/vkit/cli/commands/request_command.rb
65
67
  - lib/vkit/cli/commands/requests_list_command.rb
66
68
  - lib/vkit/cli/commands/reset_command.rb
@@ -70,9 +72,12 @@ files:
70
72
  - lib/vkit/cli/policy_bundle_validator.rb
71
73
  - lib/vkit/cli/policy_pack/manager.rb
72
74
  - lib/vkit/cli/requests_cli.rb
75
+ - lib/vkit/core/ansi.rb
73
76
  - lib/vkit/core/auth_client.rb
74
77
  - lib/vkit/core/credential_resolver.rb
75
78
  - lib/vkit/core/credential_store.rb
79
+ - lib/vkit/core/registry_diff.rb
80
+ - lib/vkit/core/registry_diff_printer.rb
76
81
  - lib/vkit/core/table_formatter.rb
77
82
  - lib/vkit/policy/bundle_compiler.rb
78
83
  - lib/vkit/policy/packs/ai_safety/metadata.yaml