ace-support-config 0.9.2 → 0.10.2

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: 20f6ee6bfb7995646918fb15521f6dda200991c386457e5eec4bce45cb210ad1
4
- data.tar.gz: 7acffaaac26242bf93809168b2adabb08af4e1b448faed0d5cb02c8dd0ab3ac4
3
+ metadata.gz: 16e2e0c5255bb10848420c2985d022bdd807e268e8221131fda820d132539d9a
4
+ data.tar.gz: adebbb9e9f34fb1bb04d6afd408bf2bcff4d64a18634198051f911e57014e623
5
5
  SHA512:
6
- metadata.gz: 7a4116d64e58de732db67e2d7d3f9f38eeef23c94c3aa27697081f06db5b3aae6eb9baad2973bab1520489b24941a6bfac96abd36e9d8f63789f5afe48e2a3d9
7
- data.tar.gz: f6c7667642fd4cd3d0e9f99795694f3c389c7bd6c86dbc3e762aab83287bd5fa04540a7c880b6a41306d66a2face1a0ef0d4d5699c8f1c8ebdb8f0bdb89e9395
6
+ metadata.gz: bf334d64a77eb96a0286b77be99ef38567adda7d0fe93044aa8341eabf5da36465b5bfbf5c2341eb4e07fdab134bfda06c2d1ea998547daf2b5fba17d35d21e2
7
+ data.tar.gz: 4ee061b82ac2011c8dcd217c747659baad216cbf615f31c9b8a1883c14f89748a87f9b2e747eec52387367678e85e4ed67c4b08962bd72ad04f77f60a298c822
data/CHANGELOG.md CHANGED
@@ -7,6 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.10.2] - 2026-03-31
11
+
12
+ ### Technical
13
+ - Added integration test coverage that stubs config template discovery in CLI flows to keep package tests deterministic across environments.
14
+
15
+ ## [0.10.1] - 2026-03-31
16
+
17
+ ### Fixed
18
+ - Initialize project-root handling in RubyGems verify-install workflow usage paths to avoid brittle Gemfile resolution.
19
+ - Remove Bundler runtime dependency from `ace-config` executable startup.
20
+ - Wire `ConfigDiff` local/verbose behavior through CLI execution paths.
21
+
22
+ ### Technical
23
+ - Added reset support for `ConfigTemplates` cache state and regression coverage for CLI/config diff behavior.
24
+
25
+ ## [0.10.0] - 2026-03-31
26
+
27
+ ### Added
28
+ - Introduced `ace-config` CLI as the canonical config command with parity for `init`, `diff`, `list`, `version`, and `help`.
29
+ - Added package executable `exe/ace-config` and repo wrapper `bin/ace-config`.
30
+
31
+ ### Changed
32
+ - Migrated config CLI runtime to `Ace::Support::Config` with in-package modules for CLI dispatch, template discovery, initialization, and diff operations.
33
+ - Updated package docs to present `ace-config` as the primary interface.
34
+
35
+ ### Technical
36
+ - Added integration tests for `ace-config` CLI behavior and bootstrap/config initialization flows.
37
+
10
38
  ## [0.9.2] - 2026-03-29
11
39
 
12
40
  ### Technical
data/README.md CHANGED
@@ -17,6 +17,18 @@
17
17
  [Usage Guide](docs/usage.md)
18
18
  `ace-support-config` provides layered configuration loading and merging for ACE, resolving values from `.ace` project files, user home defaults, and gem-bundled defaults with deterministic precedence. Used by [ace-llm](../ace-llm), [ace-search](../ace-search), [ace-review](../ace-review), and most other ACE packages.
19
19
 
20
+ ## ace-config CLI
21
+
22
+ This package now owns the `ace-config` executable for managing `.ace` configuration files and templates.
23
+
24
+ ```bash
25
+ ace-config init [GEM] [--force] [--dry-run] [--global] [--verbose]
26
+ ace-config diff [GEM] [--global] [--local] [--file PATH] [--one-line]
27
+ ace-config list [--verbose]
28
+ ace-config version
29
+ ace-config help
30
+ ```
31
+
20
32
  ## How It Works
21
33
 
22
34
  1. A resolver builds a configuration cascade from the nearest `.ace` directory up to user-home and gem-default layers.
@@ -0,0 +1,30 @@
1
+ ---
2
+ description: Showcase ace-config CLI for listing, initializing, and diffing ace-* gem configurations
3
+ tags:
4
+ - ace-config
5
+ - ace-support-config
6
+ - getting-started
7
+ settings:
8
+ font_size: 16
9
+ width: 1200
10
+ height: 700
11
+ format: gif
12
+ scenes:
13
+ - name: List available gem configs
14
+ commands:
15
+ - type: ace-config list
16
+ sleep: 3s
17
+ - name: Preview config initialization
18
+ commands:
19
+ - type: ace-config init --dry-run
20
+ sleep: 4s
21
+ - name: Diff configs against defaults
22
+ commands:
23
+ - type: ace-config diff --one-line
24
+ sleep: 3s
25
+ teardown:
26
+ - cleanup
27
+ setup:
28
+ - sandbox
29
+ - git-init
30
+ - copy-fixtures
data/docs/usage.md ADDED
@@ -0,0 +1,200 @@
1
+ ---
2
+ doc-type: user
3
+ title: ace-support-config Usage Guide
4
+ purpose: Documentation for ace-support-config/docs/usage.md
5
+ ace-docs:
6
+ last-updated: '2026-03-31'
7
+ last-checked: '2026-03-31'
8
+ ---
9
+
10
+ # ace-support-config Usage Guide
11
+
12
+ ## Configuration Cascade
13
+
14
+ The `ace-support-config` gem provides a generic configuration cascade system that merges configuration from multiple sources with priority-based resolution.
15
+
16
+ ## ace-config Command
17
+
18
+ `ace-support-config` ships the `ace-config` CLI for template discovery, initialization, and drift checks.
19
+
20
+ ```bash
21
+ ace-config init [GEM] [--force] [--dry-run] [--global] [--verbose]
22
+ ace-config diff [GEM] [--global] [--local] [--file PATH] [--one-line]
23
+ ace-config list [--verbose]
24
+ ace-config version
25
+ ace-config help
26
+ ```
27
+
28
+ ### Cascade Priority (highest to lowest)
29
+
30
+ 1. **Project level** - `<project_root>/.ace/`
31
+ 2. **User level** - `~/.ace/`
32
+ 3. **Gem defaults** - `.ace-defaults/` (lowest priority)
33
+
34
+ ### Basic Usage
35
+
36
+ ```ruby
37
+ require 'ace/support/config'
38
+
39
+ # Create a configuration resolver
40
+ config = Ace::Support::Config.create
41
+
42
+ # Resolve configuration from all sources
43
+ resolved = config.resolve
44
+
45
+ # Get nested values
46
+ value = resolved.get("key", "nested", "path")
47
+ ```
48
+
49
+ ## Deep Merging
50
+
51
+ The gem provides several array merge strategies when combining configurations:
52
+
53
+ ### Strategies
54
+
55
+ - **`:replace`** (default) - Overlay array replaces base array
56
+ - **`:concat`** - Concatenate arrays
57
+ - **`:union`** - Set union (deduplicated)
58
+ - **`:coerce_union`** - Coerce scalars to arrays, union, filter blanks
59
+
60
+ ```ruby
61
+ # Replace strategy (default)
62
+ config = Ace::Support::Config.create(merge_strategy: :replace)
63
+
64
+ # Concatenate arrays
65
+ config = Ace::Support::Config.create(merge_strategy: :concat)
66
+
67
+ # Set union
68
+ config = Ace::Support::Config.create(merge_strategy: :union)
69
+
70
+ # Coerce union - scalars become arrays, then union
71
+ config = Ace::Support::Config.create(merge_strategy: :coerce_union)
72
+ ```
73
+
74
+ ### Custom Merge
75
+
76
+ ```ruby
77
+ # Use Config.wrap for one-liner merging
78
+ base_config = { "key" => "default" }
79
+ user_config = { "key" => "override" }
80
+
81
+ merged = Ace::Support::Config::Models::Config.wrap(base_config, user_config)
82
+ # => { "key" => "override" }
83
+ ```
84
+
85
+ ## Namespace-Based Configuration
86
+
87
+ Load configuration for a specific namespace (e.g., per-gem configuration):
88
+
89
+ ```ruby
90
+ resolver = Ace::Support::Config.create
91
+
92
+ # Resolves: .ace/gem_name/config.yml or .ace/gem_name/config.yaml
93
+ gem_config = resolver.resolve_namespace("gem_name")
94
+
95
+ # With custom filename
96
+ # Resolves: .ace/docs/config.yml or .ace/docs/config.yaml
97
+ docs_config = resolver.resolve_namespace("docs", filename: "settings")
98
+ ```
99
+
100
+ ## Test Mode
101
+
102
+ For faster test execution, enable test mode to skip filesystem searches:
103
+
104
+ ### Thread-Local Test Mode
105
+
106
+ ```ruby
107
+ # Enable test mode
108
+ Ace::Support::Config.test_mode = true
109
+
110
+ # Create config (returns empty config immediately)
111
+ config = Ace::Support::Config.create
112
+
113
+ # Provide mock data
114
+ Ace::Support::Config.default_mock = { "key" => "value" }
115
+
116
+ # Create config with mock data
117
+ config = Ace::Support::Config.create
118
+ ```
119
+
120
+ ### Environment Variable Test Mode
121
+
122
+ ```bash
123
+ # Enable test mode via environment variable
124
+ ACE_CONFIG_TEST_MODE=1 ruby my_script.rb
125
+ ```
126
+
127
+ ## Virtual Filesystem View
128
+
129
+ The `virtual_resolver` provides a "virtual filesystem" view where the nearest config file wins:
130
+
131
+ ```ruby
132
+ resolver = Ace::Support::Config.virtual_resolver
133
+
134
+ # Find all config files matching a pattern
135
+ resolver.glob("presets/*.yml").each do |relative, absolute|
136
+ puts "Found: #{relative} at #{absolute}"
137
+ end
138
+
139
+ # Check if a file exists anywhere in the cascade
140
+ if resolver.exists?("templates/default.md")
141
+ path = resolver.resolve_path("templates/default.md")
142
+ # Use the file...
143
+ end
144
+ ```
145
+
146
+ ## Custom Folder Names
147
+
148
+ Use custom configuration folder names instead of the default `.ace`:
149
+
150
+ ```ruby
151
+ config = Ace::Support::Config.create(
152
+ config_dir: ".my-app", # instead of .ace
153
+ defaults_dir: ".my-app-defaults" # instead of .ace-defaults
154
+ )
155
+ ```
156
+
157
+ ## Gem Defaults
158
+
159
+ To provide default configuration from your gem:
160
+
161
+ ```ruby
162
+ # In your gem's lib/your_gem.rb
163
+ require 'ace/support/config'
164
+
165
+ module YourGem
166
+ def self.config
167
+ @config ||= Ace::Support::Config.create(
168
+ gem_path: __dir__, # Path to your gem root
169
+ defaults_dir: ".your-gem-defaults"
170
+ )
171
+ end
172
+ end
173
+ ```
174
+
175
+ ## Path Expansion
176
+
177
+ The gem integrates with `ace-support-fs` for path expansion:
178
+
179
+ ```ruby
180
+ expander = Ace::Support::Config.path_expander(
181
+ source_dir: File.expand_path("../config", __FILE__),
182
+ project_root: Dir.pwd
183
+ )
184
+
185
+ # Expand paths relative to source directory
186
+ absolute_path = expander.expand("../data/file.yml")
187
+ ```
188
+
189
+ ## Reset Configuration State
190
+
191
+ Clear all cached configuration (useful for tests):
192
+
193
+ ```ruby
194
+ Ace::Support::Config.reset_config!
195
+ ```
196
+
197
+ This clears:
198
+ - Project root cache
199
+ - Thread-local test mode state
200
+ - Thread-local mock data
data/exe/ace-config ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "ace/support/config"
5
+ require "ace/support/config/cli"
6
+
7
+ begin
8
+ Ace::Support::Config::CLI.start(ARGV)
9
+ rescue SystemExit => e
10
+ exit(e.status)
11
+ end
@@ -0,0 +1,192 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+ require_relative "organisms/config_initializer"
5
+ require_relative "organisms/config_diff"
6
+ require_relative "models/config_templates"
7
+
8
+ module Ace
9
+ module Support
10
+ module Config
11
+ class CLI
12
+ def self.start(argv)
13
+ new.run(argv)
14
+ end
15
+
16
+ def run(argv)
17
+ return show_help if argv.empty?
18
+
19
+ command = argv.shift
20
+
21
+ case command
22
+ when "init"
23
+ run_init(argv)
24
+ when "diff"
25
+ run_diff(argv)
26
+ when "list"
27
+ run_list(argv)
28
+ when "version", "--version"
29
+ show_version
30
+ when "help", "--help", "-h"
31
+ show_help
32
+ else
33
+ puts "Unknown command: #{command}"
34
+ puts ""
35
+ show_help
36
+ exit 1
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def run_init(argv)
43
+ options = {}
44
+
45
+ parser = OptionParser.new do |opts|
46
+ opts.banner = <<~BANNER.chomp
47
+ NAME
48
+ ace-config init - Initialize configuration for ace-* gems
49
+
50
+ USAGE
51
+ ace-config init [GEM] [OPTIONS]
52
+
53
+ OPTIONS
54
+ BANNER
55
+ opts.on("--force", "Overwrite existing files") { options[:force] = true }
56
+ opts.on("--dry-run", "Show what would be done") { options[:dry_run] = true }
57
+ opts.on("--global", "Use ~/.ace instead of ./.ace") { options[:global] = true }
58
+ opts.on("--verbose", "Show verbose output") { options[:verbose] = true }
59
+ opts.on("-h", "--help", "Show this help") do
60
+ puts opts
61
+ exit
62
+ end
63
+ end
64
+
65
+ parser.parse!(argv)
66
+ gem_name = argv.shift
67
+
68
+ initializer = Organisms::ConfigInitializer.new(**options)
69
+
70
+ if gem_name
71
+ initializer.init_gem(gem_name)
72
+ else
73
+ initializer.init_all
74
+ end
75
+ end
76
+
77
+ def run_diff(argv)
78
+ options = {}
79
+
80
+ parser = OptionParser.new do |opts|
81
+ opts.banner = <<~BANNER.chomp
82
+ NAME
83
+ ace-config diff - Compare configs with examples
84
+
85
+ USAGE
86
+ ace-config diff [GEM] [OPTIONS]
87
+
88
+ OPTIONS
89
+ BANNER
90
+ opts.on("--global", "Compare global configs") { options[:global] = true }
91
+ opts.on("--local", "Compare local configs (default)") { options[:local] = true }
92
+ opts.on("--file PATH", "Compare specific file") { |f| options[:file] = f }
93
+ opts.on("--one-line", "One-line summary per file") { options[:one_line] = true }
94
+ opts.on("--verbose", "Include unchanged files in one-line summary") { options[:verbose] = true }
95
+ opts.on("-h", "--help", "Show this help") do
96
+ puts opts
97
+ exit
98
+ end
99
+ end
100
+
101
+ parser.parse!(argv)
102
+ gem_name = argv.shift
103
+
104
+ differ = Organisms::ConfigDiff.new(**options)
105
+
106
+ if gem_name
107
+ differ.diff_gem(gem_name)
108
+ else
109
+ differ.run
110
+ end
111
+ end
112
+
113
+ def run_list(argv)
114
+ verbose = false
115
+
116
+ parser = OptionParser.new do |opts|
117
+ opts.banner = <<~BANNER.chomp
118
+ NAME
119
+ ace-config list - List available ace-* gems with example configs
120
+
121
+ USAGE
122
+ ace-config list [OPTIONS]
123
+
124
+ OPTIONS
125
+ BANNER
126
+ opts.on("--verbose", "Show detailed information") { verbose = true }
127
+ opts.on("-h", "--help", "Show this help") do
128
+ puts opts
129
+ exit
130
+ end
131
+ end
132
+
133
+ parser.parse!(argv)
134
+
135
+ puts "Available ace-* gems with example configurations:\n\n"
136
+
137
+ if Models::ConfigTemplates.all_gems.empty?
138
+ puts "No ace-* gems with example configurations found."
139
+ return
140
+ end
141
+
142
+ Models::ConfigTemplates.all_gems.each do |gem_name|
143
+ info = Models::ConfigTemplates.gem_info[gem_name]
144
+ source_label = case info[:source]
145
+ when :local then "[local]"
146
+ when :gem then "[gem]"
147
+ when :both then "[local+gem]"
148
+ end
149
+
150
+ puts " #{gem_name} #{source_label}"
151
+
152
+ next unless verbose
153
+
154
+ puts " Path: #{info[:path]}"
155
+ puts " Gem: #{info[:gem_path]}" if info[:gem_path]
156
+ example_dir = Models::ConfigTemplates.example_dir_for(gem_name)
157
+ if example_dir && File.exist?(example_dir)
158
+ example_files = Dir.glob("#{example_dir}/**/*").reject { |f| File.directory?(f) }
159
+ puts " Example files: #{example_files.size}"
160
+ end
161
+ end
162
+
163
+ puts "\nUse 'ace-config init [GEM]' to initialize a specific gem's configuration"
164
+ puts "Use 'ace-config init' to initialize all configurations"
165
+ end
166
+
167
+ def show_version
168
+ puts "ace-config #{Ace::Support::Config::VERSION}"
169
+ end
170
+
171
+ def show_help
172
+ puts <<~HELP
173
+ NAME
174
+ ace-config - Configuration management for ace-* gems
175
+
176
+ USAGE
177
+ ace-config COMMAND [OPTIONS]
178
+
179
+ COMMANDS
180
+ init [GEM] Initialize configuration for specific gem or all
181
+ diff [GEM] Compare configs with examples
182
+ list List available ace-* gems with example configs
183
+ version Show version
184
+ help Show this help
185
+
186
+ Run 'ace-config COMMAND --help' for more information on a command.
187
+ HELP
188
+ end
189
+ end
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+ require "rubygems"
5
+
6
+ module Ace
7
+ module Support
8
+ module Config
9
+ module Models
10
+ class ConfigTemplates
11
+ class << self
12
+ def all_gems
13
+ gem_info.keys.sort
14
+ end
15
+
16
+ def gem_exists?(gem_name)
17
+ gem_info.key?(gem_name)
18
+ end
19
+
20
+ def example_dir_for(gem_name)
21
+ info = gem_info[gem_name]
22
+ return nil unless info
23
+
24
+ path = (info[:source] == :gem) ? info[:path] : (info[:path] || info[:gem_path])
25
+ resolve_defaults_dir(path)
26
+ end
27
+
28
+ def gem_info
29
+ @gem_info ||= build_gem_info
30
+ end
31
+
32
+ def reset!
33
+ @gem_info = nil
34
+ end
35
+
36
+ def build_gem_info
37
+ gems = {}
38
+
39
+ parent_dir = File.expand_path("../../../../../../../", __FILE__)
40
+ Dir.glob("#{parent_dir}/ace-*").each do |dir|
41
+ next unless File.directory?(dir)
42
+
43
+ gem_name = File.basename(dir)
44
+ gems[gem_name] = {source: :local, path: dir} if has_example_dir?(dir)
45
+ end
46
+
47
+ begin
48
+ Gem::Specification.each do |spec|
49
+ next unless spec.name.start_with?("ace-")
50
+
51
+ gem_path = spec.gem_dir
52
+ next unless has_example_dir?(gem_path)
53
+
54
+ if gems.key?(spec.name)
55
+ gems[spec.name][:source] = :both
56
+ gems[spec.name][:gem_path] = gem_path
57
+ else
58
+ gems[spec.name] = {source: :gem, path: gem_path}
59
+ end
60
+ end
61
+ rescue StandardError
62
+ # Fall back to local gems only when RubyGems traversal is unavailable.
63
+ end
64
+
65
+ gems
66
+ end
67
+
68
+ def docs_file_for(gem_name)
69
+ info = gem_info[gem_name]
70
+ return nil unless info
71
+
72
+ path = (info[:source] == :gem) ? info[:path] : (info[:path] || info[:gem_path])
73
+ File.join(path, "docs", "config.md")
74
+ end
75
+
76
+ private
77
+
78
+ def resolve_defaults_dir(gem_path)
79
+ File.join(gem_path, ".ace-defaults")
80
+ end
81
+
82
+ def has_example_dir?(gem_dir)
83
+ Dir.exist?(File.join(gem_dir, ".ace-defaults"))
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require "pathname"
5
+ require_relative "../models/config_templates"
6
+
7
+ module Ace
8
+ module Support
9
+ module Config
10
+ module Organisms
11
+ class ConfigDiff
12
+ def initialize(global: false, local: false, file: nil, one_line: false, verbose: false)
13
+ @global = global
14
+ @local = local
15
+ @file = file
16
+ @one_line = one_line
17
+ @verbose = verbose
18
+ @diffs = []
19
+ end
20
+
21
+ def run
22
+ if @file
23
+ diff_file(@file)
24
+ else
25
+ diff_all_configs
26
+ end
27
+
28
+ print_results
29
+ end
30
+
31
+ def diff_gem(gem_name)
32
+ normalized_name = gem_name.start_with?("ace-") ? gem_name : "ace-#{gem_name}"
33
+
34
+ unless Models::ConfigTemplates.gem_exists?(normalized_name)
35
+ puts "Error: Gem '#{normalized_name}' not found or has no example configurations"
36
+ exit 1
37
+ end
38
+
39
+ diff_gem_configs(normalized_name)
40
+ print_results
41
+ end
42
+
43
+ private
44
+
45
+ def config_directory
46
+ return ".ace" if @local
47
+
48
+ @global ? File.expand_path("~/.ace") : ".ace"
49
+ end
50
+
51
+ def diff_all_configs
52
+ Models::ConfigTemplates.all_gems.each do |gem_name|
53
+ diff_gem_configs(gem_name)
54
+ end
55
+ end
56
+
57
+ def diff_gem_configs(gem_name)
58
+ source_dir = Models::ConfigTemplates.example_dir_for(gem_name)
59
+ return unless source_dir && File.exist?(source_dir)
60
+
61
+ Dir.glob("#{source_dir}/**/*").each do |source_file|
62
+ next if File.directory?(source_file)
63
+
64
+ relative_path = Pathname.new(source_file).relative_path_from(Pathname.new(source_dir))
65
+ target_file = File.join(config_directory, relative_path.to_s)
66
+
67
+ compare_files(source_file, target_file, gem_name)
68
+ end
69
+ end
70
+
71
+ def diff_file(file_path)
72
+ unless file_path.start_with?(config_directory)
73
+ puts "File #{file_path} is not in a configuration directory"
74
+ return
75
+ end
76
+
77
+ relative_path = Pathname.new(file_path).relative_path_from(Pathname.new(config_directory))
78
+ parts = relative_path.to_s.split(File::SEPARATOR)
79
+ return if parts.empty?
80
+
81
+ config_subdir = parts.first
82
+ gem_name = "ace-#{config_subdir}"
83
+ return unless Models::ConfigTemplates.gem_exists?(gem_name)
84
+
85
+ source_dir = Models::ConfigTemplates.example_dir_for(gem_name)
86
+ relative_file = parts[1..].join(File::SEPARATOR)
87
+ source_file = File.join(source_dir, relative_file)
88
+
89
+ if File.exist?(source_file)
90
+ compare_files(source_file, file_path, gem_name)
91
+ else
92
+ puts "No example file found for #{file_path}"
93
+ end
94
+ end
95
+
96
+ def compare_files(source_file, target_file, gem_name)
97
+ @diffs << if !File.exist?(target_file)
98
+ {
99
+ gem: gem_name,
100
+ file: target_file,
101
+ status: :missing,
102
+ source: source_file
103
+ }
104
+ elsif files_differ?(source_file, target_file)
105
+ {
106
+ gem: gem_name,
107
+ file: target_file,
108
+ status: :different,
109
+ source: source_file,
110
+ diff_output: get_diff_output(source_file, target_file)
111
+ }
112
+ else
113
+ {
114
+ gem: gem_name,
115
+ file: target_file,
116
+ status: :same,
117
+ source: source_file
118
+ }
119
+ end
120
+ end
121
+
122
+ def files_differ?(file1, file2)
123
+ File.read(file1) != File.read(file2)
124
+ rescue StandardError
125
+ true
126
+ end
127
+
128
+ def get_diff_output(source_file, target_file)
129
+ output, _status = Open3.capture2("diff", "-u", target_file, source_file)
130
+ output
131
+ rescue StandardError
132
+ "Unable to generate diff"
133
+ end
134
+
135
+ def print_results
136
+ if @one_line
137
+ print_one_line_summary
138
+ else
139
+ print_detailed_diffs
140
+ end
141
+ end
142
+
143
+ def print_one_line_summary
144
+ @diffs.each do |diff|
145
+ case diff[:status]
146
+ when :missing
147
+ puts "MISSING: #{diff[:file]}"
148
+ when :different
149
+ puts "CHANGED: #{diff[:file]}"
150
+ when :same
151
+ puts "SAME: #{diff[:file]}" if @verbose
152
+ end
153
+ end
154
+
155
+ puts "\nSummary:"
156
+ puts " Missing: #{@diffs.count { |d| d[:status] == :missing }}"
157
+ puts " Changed: #{@diffs.count { |d| d[:status] == :different }}"
158
+ puts " Same: #{@diffs.count { |d| d[:status] == :same }}"
159
+ end
160
+
161
+ def print_detailed_diffs
162
+ missing = @diffs.select { |d| d[:status] == :missing }
163
+ changed = @diffs.select { |d| d[:status] == :different }
164
+
165
+ if missing.any?
166
+ puts "Missing configuration files:"
167
+ missing.each do |diff|
168
+ puts " #{diff[:file]}"
169
+ puts " -> Example: #{diff[:source]}"
170
+ end
171
+ puts
172
+ end
173
+
174
+ if changed.any?
175
+ puts "Changed configuration files:"
176
+ changed.each do |diff|
177
+ puts "\n#{diff[:file]}:"
178
+ puts diff[:diff_output]
179
+ end
180
+ end
181
+
182
+ if missing.empty? && changed.empty?
183
+ puts "All configuration files match the examples."
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "pathname"
5
+ require_relative "../models/config_templates"
6
+
7
+ module Ace
8
+ module Support
9
+ module Config
10
+ module Organisms
11
+ class ConfigInitializer
12
+ def initialize(force: false, dry_run: false, global: false, verbose: false)
13
+ @force = force
14
+ @dry_run = dry_run
15
+ @global = global
16
+ @verbose = verbose
17
+ @copied_files = []
18
+ @skipped_files = []
19
+ end
20
+
21
+ def init_all
22
+ puts "Initializing all ace-* gem configurations..." if @verbose
23
+
24
+ Models::ConfigTemplates.all_gems.each do |gem_name|
25
+ init_gem(gem_name)
26
+ end
27
+
28
+ print_summary
29
+ end
30
+
31
+ def init_gem(gem_name)
32
+ gem_name = normalize_gem_name(gem_name)
33
+
34
+ unless Models::ConfigTemplates.gem_exists?(gem_name)
35
+ puts "Warning: No configuration found for #{gem_name}"
36
+ return
37
+ end
38
+
39
+ puts "\nInitializing #{gem_name}..." if @verbose
40
+
41
+ source_dir = Models::ConfigTemplates.example_dir_for(gem_name)
42
+ target_dir = target_directory
43
+
44
+ unless File.exist?(source_dir)
45
+ puts "Warning: No .ace-defaults directory found for #{gem_name}"
46
+ return
47
+ end
48
+
49
+ show_config_docs_if_needed(gem_name, target_dir)
50
+ copy_config_files(source_dir, target_dir)
51
+ end
52
+
53
+ private
54
+
55
+ def normalize_gem_name(name)
56
+ name.start_with?("ace-") ? name : "ace-#{name}"
57
+ end
58
+
59
+ def target_directory
60
+ @global ? File.expand_path("~/.ace") : ".ace"
61
+ end
62
+
63
+ def show_config_docs_if_needed(gem_name, target_dir)
64
+ config_subdir = gem_name.sub("ace-", "")
65
+ existing_configs = Dir.glob("#{target_dir}/#{config_subdir}/**/*").reject { |f| File.directory?(f) }
66
+
67
+ return if existing_configs.any? || @dry_run
68
+
69
+ docs_file = Models::ConfigTemplates.docs_file_for(gem_name)
70
+ puts "\n#{File.read(docs_file)}\n" if docs_file && File.exist?(docs_file)
71
+ end
72
+
73
+ def copy_config_files(source_dir, target_dir)
74
+ Dir.glob("#{source_dir}/**/*").each do |source_file|
75
+ next if File.directory?(source_file)
76
+
77
+ relative_path = Pathname.new(source_file).relative_path_from(Pathname.new(source_dir))
78
+ target_file = File.join(target_dir, relative_path.to_s)
79
+
80
+ copy_file(source_file, target_file)
81
+ end
82
+ end
83
+
84
+ def copy_file(source, target)
85
+ if File.exist?(target) && !@force
86
+ @skipped_files << target
87
+ puts " Skipped: #{target} (already exists)" if @verbose
88
+ return
89
+ end
90
+
91
+ if @dry_run
92
+ puts " Would copy: #{source} -> #{target}"
93
+ else
94
+ FileUtils.mkdir_p(File.dirname(target))
95
+ FileUtils.cp(source, target)
96
+ @copied_files << target
97
+ puts " Copied: #{target}" if @verbose
98
+ end
99
+ end
100
+
101
+ def print_summary
102
+ return if @dry_run
103
+
104
+ puts "\nConfiguration initialization complete:"
105
+ puts " Files copied: #{@copied_files.size}"
106
+ puts " Files skipped: #{@skipped_files.size}"
107
+
108
+ if @skipped_files.any? && !@force
109
+ puts "\nUse --force to overwrite existing files"
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -3,7 +3,7 @@
3
3
  module Ace
4
4
  module Support
5
5
  module Config
6
- VERSION = '0.9.2'
6
+ VERSION = '0.10.2'
7
7
  end
8
8
  end
9
9
  end
@@ -26,6 +26,9 @@ require_relative "config/molecules/project_config_scanner"
26
26
  # Load organisms (depend on molecules)
27
27
  require_relative "config/organisms/config_resolver"
28
28
  require_relative "config/organisms/virtual_config_resolver"
29
+ require_relative "config/organisms/config_initializer"
30
+ require_relative "config/organisms/config_diff"
31
+ require_relative "config/models/config_templates"
29
32
 
30
33
  module Ace
31
34
  # Generic configuration cascade management
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ace-support-config
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.2
4
+ version: 0.10.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michal Czyz
8
- bindir: bin
8
+ bindir: exe
9
9
  cert_chain: []
10
- date: 2026-03-29 00:00:00.000000000 Z
10
+ date: 2026-04-01 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: ace-support-fs
@@ -56,7 +56,8 @@ description: Reusable configuration cascade with customizable folder names. Supp
56
56
  resolution.
57
57
  email:
58
58
  - mc@cs3b.com
59
- executables: []
59
+ executables:
60
+ - ace-config
60
61
  extensions: []
61
62
  extra_rdoc_files: []
62
63
  files:
@@ -65,20 +66,27 @@ files:
65
66
  - LICENSE
66
67
  - README.md
67
68
  - Rakefile
69
+ - docs/demo/ace-config-getting-started.tape.yml
70
+ - docs/usage.md
71
+ - exe/ace-config
68
72
  - lib/ace/support.rb
69
73
  - lib/ace/support/config.rb
70
74
  - lib/ace/support/config/atoms/deep_merger.rb
71
75
  - lib/ace/support/config/atoms/path_rule_matcher.rb
72
76
  - lib/ace/support/config/atoms/path_validator.rb
73
77
  - lib/ace/support/config/atoms/yaml_parser.rb
78
+ - lib/ace/support/config/cli.rb
74
79
  - lib/ace/support/config/errors.rb
75
80
  - lib/ace/support/config/models/cascade_path.rb
76
81
  - lib/ace/support/config/models/config.rb
77
82
  - lib/ace/support/config/models/config_group.rb
83
+ - lib/ace/support/config/models/config_templates.rb
78
84
  - lib/ace/support/config/molecules/config_finder.rb
79
85
  - lib/ace/support/config/molecules/file_config_resolver.rb
80
86
  - lib/ace/support/config/molecules/project_config_scanner.rb
81
87
  - lib/ace/support/config/molecules/yaml_loader.rb
88
+ - lib/ace/support/config/organisms/config_diff.rb
89
+ - lib/ace/support/config/organisms/config_initializer.rb
82
90
  - lib/ace/support/config/organisms/config_resolver.rb
83
91
  - lib/ace/support/config/organisms/virtual_config_resolver.rb
84
92
  - lib/ace/support/config/version.rb