ace-support-config 0.9.2 → 0.11.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: 33ca9302021e76335a714f27321b3075dd172056154ed5d1769b6f7b9ea7d6c7
4
+ data.tar.gz: e06462886d8d07fa3104367a96d129957b8554b8ac4419651f51c220dcd09167
5
5
  SHA512:
6
- metadata.gz: 7a4116d64e58de732db67e2d7d3f9f38eeef23c94c3aa27697081f06db5b3aae6eb9baad2973bab1520489b24941a6bfac96abd36e9d8f63789f5afe48e2a3d9
7
- data.tar.gz: f6c7667642fd4cd3d0e9f99795694f3c389c7bd6c86dbc3e762aab83287bd5fa04540a7c880b6a41306d66a2face1a0ef0d4d5699c8f1c8ebdb8f0bdb89e9395
6
+ metadata.gz: 5f6377fe1c334cdf394a619c0548966317167789dd3214c87d7cbef778fe069d7e059af7db77d53fcc86aeb0acb445ef4eb644cbfb29e1f2006f73f74aa671ef
7
+ data.tar.gz: d21e340eb555a7457aabcf55e7ea26120b54b9ed282f7227658810377274501e5fac8ce1b1d0519f8a9868330502a8d0dbae4e2b69afd8805f8b37ac033de699
data/CHANGELOG.md CHANGED
@@ -7,6 +7,65 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.11.2] - 2026-04-13
11
+
12
+ ### Fixed
13
+ - Made bootstrap `.gitignore` detection line-aware so commented or negated `.ace-local/` mentions no longer suppress appending the real ignore rule.
14
+
15
+ ### Technical
16
+ - Added regression coverage for commented and negated `.gitignore` mentions during bootstrap merges.
17
+
18
+ ## [0.11.1] - 2026-04-13
19
+
20
+ ### Fixed
21
+ - Preserve existing `.gitignore` rules even when `ace-config init --force` refreshes bootstrap files.
22
+ - Anchor project-root bootstrap files (`.gitignore`, `AGENTS.md`, `CLAUDE.md`) to the detected repository root so subdirectory runs do not seed them into the wrong location.
23
+
24
+ ## [0.11.0] - 2026-04-13
25
+
26
+ ### Changed
27
+ - **ace-support-config v0.11.0**: Taught `ace-config init` to bootstrap project-root guidance files, include dotfile defaults, and append `.ace-local/` to existing `.gitignore` files without overwriting user-owned `AGENTS.md` or `CLAUDE.md`.
28
+
29
+ ## [0.10.4] - 2026-04-13
30
+
31
+ ### Changed
32
+ - **ace-support-config v0.10.4**: Standardized shared package tests to the fast-only layout and updated testing flow defaults.
33
+
34
+
35
+ ## [0.10.3] - 2026-04-11
36
+
37
+ ### Technical
38
+ - Migrated package tests to the `fast`/`feat` layout by moving deterministic ATOM coverage to `test/fast/` and former `test/integration/` suites to `test/feat/`.
39
+ - Updated package docs to publish the `ace-test ace-support-config` (`fast`), `feat`, and `all` contract and keep package scope deterministic-only.
40
+
41
+ ## [0.10.2] - 2026-03-31
42
+
43
+ ### Technical
44
+ - Added integration test coverage that stubs config template discovery in CLI flows to keep package tests deterministic across environments.
45
+
46
+ ## [0.10.1] - 2026-03-31
47
+
48
+ ### Fixed
49
+ - Initialize project-root handling in RubyGems verify-install workflow usage paths to avoid brittle Gemfile resolution.
50
+ - Remove Bundler runtime dependency from `ace-config` executable startup.
51
+ - Wire `ConfigDiff` local/verbose behavior through CLI execution paths.
52
+
53
+ ### Technical
54
+ - Added reset support for `ConfigTemplates` cache state and regression coverage for CLI/config diff behavior.
55
+
56
+ ## [0.10.0] - 2026-03-31
57
+
58
+ ### Added
59
+ - Introduced `ace-config` CLI as the canonical config command with parity for `init`, `diff`, `list`, `version`, and `help`.
60
+ - Added package executable `exe/ace-config` and repo wrapper `bin/ace-config`.
61
+
62
+ ### Changed
63
+ - Migrated config CLI runtime to `Ace::Support::Config` with in-package modules for CLI dispatch, template discovery, initialization, and diff operations.
64
+ - Updated package docs to present `ace-config` as the primary interface.
65
+
66
+ ### Technical
67
+ - Added integration tests for `ace-config` CLI behavior and bootstrap/config initialization flows.
68
+
10
69
  ## [0.9.2] - 2026-03-29
11
70
 
12
71
  ### Technical
data/README.md CHANGED
@@ -17,6 +17,30 @@
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
+
32
+ ## Testing
33
+
34
+ `ace-support-config` uses the ACE deterministic test taxonomy:
35
+
36
+ ```bash
37
+ ace-test ace-support-config # fast (default)
38
+ ace-test ace-support-config feat # deterministic feature coverage
39
+ ace-test ace-support-config all # fast + feat
40
+ ```
41
+
42
+ This package does not define a package-owned `e2e` layer in this migration.
43
+
20
44
  ## How It Works
21
45
 
22
46
  1. A resolver builds a configuration cascade from the nearest `.ace` directory up to user-home and gem-default layers.
@@ -0,0 +1,29 @@
1
+ ---
2
+ description: Showcase ace-config init bootstrapping project-root guidance files from a subdirectory
3
+ tags:
4
+ - ace-config
5
+ - ace-support-config
6
+ - bootstrap
7
+ settings:
8
+ font_size: 16
9
+ width: 1200
10
+ height: 700
11
+ format: gif
12
+ scenes:
13
+ - name: Initialize config from a nested working directory
14
+ commands:
15
+ - type: mkdir -p repo/subdir && cd repo && git init
16
+ sleep: 3s
17
+ - type: cd subdir && ace-config init support-core
18
+ sleep: 5s
19
+ - name: Verify bootstrap files landed at the repository root
20
+ commands:
21
+ - type: cd ../ && find . -maxdepth 2 \\( -name '.gitignore' -o -name 'AGENTS.md' -o -name 'CLAUDE.md' \\) | sort
22
+ sleep: 4s
23
+ - type: printf '\n.gitignore contents:\n' && cat .gitignore
24
+ sleep: 4s
25
+ setup:
26
+ - sandbox
27
+ - copy-fixtures
28
+ teardown:
29
+ - cleanup
@@ -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,215 @@
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
+ ## Testing Contract
29
+
30
+ `ace-support-config` follows the ACE deterministic package targets:
31
+
32
+ ```bash
33
+ ace-test ace-support-config
34
+ ace-test ace-support-config feat
35
+ ace-test ace-support-config all
36
+ ```
37
+
38
+ - `fast` (default) runs isolated deterministic package tests under `test/fast/`.
39
+ - `feat` runs deterministic feature tests under `test/feat/`.
40
+ - `all` runs `fast` then `feat`.
41
+ - This package does not publish a package-owned `e2e` target in this migration.
42
+
43
+ ### Cascade Priority (highest to lowest)
44
+
45
+ 1. **Project level** - `<project_root>/.ace/`
46
+ 2. **User level** - `~/.ace/`
47
+ 3. **Gem defaults** - `.ace-defaults/` (lowest priority)
48
+
49
+ ### Basic Usage
50
+
51
+ ```ruby
52
+ require 'ace/support/config'
53
+
54
+ # Create a configuration resolver
55
+ config = Ace::Support::Config.create
56
+
57
+ # Resolve configuration from all sources
58
+ resolved = config.resolve
59
+
60
+ # Get nested values
61
+ value = resolved.get("key", "nested", "path")
62
+ ```
63
+
64
+ ## Deep Merging
65
+
66
+ The gem provides several array merge strategies when combining configurations:
67
+
68
+ ### Strategies
69
+
70
+ - **`:replace`** (default) - Overlay array replaces base array
71
+ - **`:concat`** - Concatenate arrays
72
+ - **`:union`** - Set union (deduplicated)
73
+ - **`:coerce_union`** - Coerce scalars to arrays, union, filter blanks
74
+
75
+ ```ruby
76
+ # Replace strategy (default)
77
+ config = Ace::Support::Config.create(merge_strategy: :replace)
78
+
79
+ # Concatenate arrays
80
+ config = Ace::Support::Config.create(merge_strategy: :concat)
81
+
82
+ # Set union
83
+ config = Ace::Support::Config.create(merge_strategy: :union)
84
+
85
+ # Coerce union - scalars become arrays, then union
86
+ config = Ace::Support::Config.create(merge_strategy: :coerce_union)
87
+ ```
88
+
89
+ ### Custom Merge
90
+
91
+ ```ruby
92
+ # Use Config.wrap for one-liner merging
93
+ base_config = { "key" => "default" }
94
+ user_config = { "key" => "override" }
95
+
96
+ merged = Ace::Support::Config::Models::Config.wrap(base_config, user_config)
97
+ # => { "key" => "override" }
98
+ ```
99
+
100
+ ## Namespace-Based Configuration
101
+
102
+ Load configuration for a specific namespace (e.g., per-gem configuration):
103
+
104
+ ```ruby
105
+ resolver = Ace::Support::Config.create
106
+
107
+ # Resolves: .ace/gem_name/config.yml or .ace/gem_name/config.yaml
108
+ gem_config = resolver.resolve_namespace("gem_name")
109
+
110
+ # With custom filename
111
+ # Resolves: .ace/docs/config.yml or .ace/docs/config.yaml
112
+ docs_config = resolver.resolve_namespace("docs", filename: "settings")
113
+ ```
114
+
115
+ ## Test Mode
116
+
117
+ For faster test execution, enable test mode to skip filesystem searches:
118
+
119
+ ### Thread-Local Test Mode
120
+
121
+ ```ruby
122
+ # Enable test mode
123
+ Ace::Support::Config.test_mode = true
124
+
125
+ # Create config (returns empty config immediately)
126
+ config = Ace::Support::Config.create
127
+
128
+ # Provide mock data
129
+ Ace::Support::Config.default_mock = { "key" => "value" }
130
+
131
+ # Create config with mock data
132
+ config = Ace::Support::Config.create
133
+ ```
134
+
135
+ ### Environment Variable Test Mode
136
+
137
+ ```bash
138
+ # Enable test mode via environment variable
139
+ ACE_CONFIG_TEST_MODE=1 ruby my_script.rb
140
+ ```
141
+
142
+ ## Virtual Filesystem View
143
+
144
+ The `virtual_resolver` provides a "virtual filesystem" view where the nearest config file wins:
145
+
146
+ ```ruby
147
+ resolver = Ace::Support::Config.virtual_resolver
148
+
149
+ # Find all config files matching a pattern
150
+ resolver.glob("presets/*.yml").each do |relative, absolute|
151
+ puts "Found: #{relative} at #{absolute}"
152
+ end
153
+
154
+ # Check if a file exists anywhere in the cascade
155
+ if resolver.exists?("templates/default.md")
156
+ path = resolver.resolve_path("templates/default.md")
157
+ # Use the file...
158
+ end
159
+ ```
160
+
161
+ ## Custom Folder Names
162
+
163
+ Use custom configuration folder names instead of the default `.ace`:
164
+
165
+ ```ruby
166
+ config = Ace::Support::Config.create(
167
+ config_dir: ".my-app", # instead of .ace
168
+ defaults_dir: ".my-app-defaults" # instead of .ace-defaults
169
+ )
170
+ ```
171
+
172
+ ## Gem Defaults
173
+
174
+ To provide default configuration from your gem:
175
+
176
+ ```ruby
177
+ # In your gem's lib/your_gem.rb
178
+ require 'ace/support/config'
179
+
180
+ module YourGem
181
+ def self.config
182
+ @config ||= Ace::Support::Config.create(
183
+ gem_path: __dir__, # Path to your gem root
184
+ defaults_dir: ".your-gem-defaults"
185
+ )
186
+ end
187
+ end
188
+ ```
189
+
190
+ ## Path Expansion
191
+
192
+ The gem integrates with `ace-support-fs` for path expansion:
193
+
194
+ ```ruby
195
+ expander = Ace::Support::Config.path_expander(
196
+ source_dir: File.expand_path("../config", __FILE__),
197
+ project_root: Dir.pwd
198
+ )
199
+
200
+ # Expand paths relative to source directory
201
+ absolute_path = expander.expand("../data/file.yml")
202
+ ```
203
+
204
+ ## Reset Configuration State
205
+
206
+ Clear all cached configuration (useful for tests):
207
+
208
+ ```ruby
209
+ Ace::Support::Config.reset_config!
210
+ ```
211
+
212
+ This clears:
213
+ - Project root cache
214
+ - Thread-local test mode state
215
+ - 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
@@ -21,7 +21,7 @@ module Ace
21
21
  class ProjectConfigScanner
22
22
  # Directories to skip during traversal
23
23
  SKIP_DIRS = %w[.git .cache vendor node_modules tmp coverage
24
- .bundle _legacy .ace-local .ace-tasks .ace-taskflow].freeze
24
+ .bundle _legacy .ace-local .ace-tasks .ace-task].freeze
25
25
 
26
26
  # @param project_root [String, nil] Root directory to scan (default: Dir.pwd)
27
27
  # @param config_dir [String] Config folder name (default: ".ace")
@@ -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,197 @@
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
+ PROJECT_ROOT_DIR = "project-root"
13
+ GITIGNORE_ACE_LOCAL_ENTRY = ".ace-local/"
14
+
15
+ def initialize(force: false, dry_run: false, global: false, verbose: false)
16
+ @force = force
17
+ @dry_run = dry_run
18
+ @global = global
19
+ @verbose = verbose
20
+ @copied_files = []
21
+ @skipped_files = []
22
+ end
23
+
24
+ def init_all
25
+ puts "Initializing all ace-* gem configurations..." if @verbose
26
+
27
+ Models::ConfigTemplates.all_gems.each do |gem_name|
28
+ init_gem(gem_name)
29
+ end
30
+
31
+ print_summary
32
+ end
33
+
34
+ def init_gem(gem_name)
35
+ gem_name = normalize_gem_name(gem_name)
36
+
37
+ unless Models::ConfigTemplates.gem_exists?(gem_name)
38
+ puts "Warning: No configuration found for #{gem_name}"
39
+ return
40
+ end
41
+
42
+ puts "\nInitializing #{gem_name}..." if @verbose
43
+
44
+ source_dir = Models::ConfigTemplates.example_dir_for(gem_name)
45
+ target_dir = target_directory
46
+
47
+ unless File.exist?(source_dir)
48
+ puts "Warning: No .ace-defaults directory found for #{gem_name}"
49
+ return
50
+ end
51
+
52
+ show_config_docs_if_needed(gem_name, target_dir)
53
+ copy_config_files(source_dir, target_dir)
54
+ end
55
+
56
+ private
57
+
58
+ def normalize_gem_name(name)
59
+ name.start_with?("ace-") ? name : "ace-#{name}"
60
+ end
61
+
62
+ def target_directory
63
+ return File.expand_path("~/.ace") if @global
64
+
65
+ File.join(project_root, ".ace")
66
+ end
67
+
68
+ def show_config_docs_if_needed(gem_name, target_dir)
69
+ config_subdir = gem_name.sub("ace-", "")
70
+ existing_configs = Dir.glob("#{target_dir}/#{config_subdir}/**/*").reject { |f| File.directory?(f) }
71
+
72
+ return if existing_configs.any? || @dry_run
73
+
74
+ docs_file = Models::ConfigTemplates.docs_file_for(gem_name)
75
+ puts "\n#{File.read(docs_file)}\n" if docs_file && File.exist?(docs_file)
76
+ end
77
+
78
+ def copy_config_files(source_dir, target_dir)
79
+ glob_pattern = File.join(source_dir, "**", "*")
80
+
81
+ Dir.glob(glob_pattern, File::FNM_DOTMATCH).each do |source_file|
82
+ basename = File.basename(source_file)
83
+ next if basename == "." || basename == ".."
84
+ next if File.directory?(source_file)
85
+
86
+ relative_path = Pathname.new(source_file).relative_path_from(Pathname.new(source_dir))
87
+ target_file = target_file_for(relative_path, target_dir)
88
+ next unless target_file
89
+
90
+ copy_file(source_file, target_file)
91
+ end
92
+ end
93
+
94
+ def copy_file(source, target)
95
+ if File.basename(target) == ".gitignore"
96
+ merge_gitignore(source, target)
97
+ return
98
+ end
99
+
100
+ if File.exist?(target) && !@force
101
+ @skipped_files << target
102
+ puts " Skipped: #{target} (already exists)" if @verbose
103
+ return
104
+ end
105
+
106
+ if @dry_run
107
+ puts " Would copy: #{source} -> #{target}"
108
+ else
109
+ FileUtils.mkdir_p(File.dirname(target))
110
+ FileUtils.cp(source, target)
111
+ @copied_files << target
112
+ puts " Copied: #{target}" if @verbose
113
+ end
114
+ end
115
+
116
+ def target_file_for(relative_path, target_dir)
117
+ relative_str = relative_path.to_s
118
+
119
+ if relative_str.start_with?("#{PROJECT_ROOT_DIR}/")
120
+ return nil if @global
121
+
122
+ return File.join(project_root, relative_str.delete_prefix("#{PROJECT_ROOT_DIR}/"))
123
+ end
124
+
125
+ File.join(target_dir, relative_str)
126
+ end
127
+
128
+ def merge_gitignore(source, target)
129
+ target_exists = File.exist?(target)
130
+
131
+ if target_exists
132
+ merge_gitignore_entry_if_missing(source, target)
133
+ return
134
+ end
135
+
136
+ if @dry_run
137
+ puts " Would copy: #{source} -> #{target}"
138
+ else
139
+ FileUtils.mkdir_p(File.dirname(target))
140
+ FileUtils.cp(source, target)
141
+ @copied_files << target
142
+ puts " Copied: #{target}" if @verbose
143
+ end
144
+ end
145
+
146
+ def merge_gitignore_entry_if_missing(source, target)
147
+ existing_content = File.read(target)
148
+ return if gitignore_entry_present?(existing_content, GITIGNORE_ACE_LOCAL_ENTRY)
149
+
150
+ line = File.readlines(source, chomp: true).find do |entry|
151
+ entry == GITIGNORE_ACE_LOCAL_ENTRY
152
+ end
153
+ return unless line
154
+
155
+ if @dry_run
156
+ puts " Would append: #{line} -> #{target}"
157
+ return
158
+ end
159
+
160
+ File.open(target, "a") do |file|
161
+ file.write("\n") unless existing_content.end_with?("\n") || existing_content.empty?
162
+ file.puts line
163
+ end
164
+
165
+ @copied_files << target
166
+ puts " Appended: #{line} -> #{target}" if @verbose
167
+ end
168
+
169
+ def gitignore_entry_present?(content, entry)
170
+ content.each_line.any? do |line|
171
+ normalized = line.strip
172
+ next false if normalized.empty? || normalized.start_with?("#")
173
+
174
+ normalized == entry
175
+ end
176
+ end
177
+
178
+ def project_root
179
+ @project_root ||= Ace::Support::Config.find_project_root(start_path: Dir.pwd) || Dir.pwd
180
+ end
181
+
182
+ def print_summary
183
+ return if @dry_run
184
+
185
+ puts "\nConfiguration initialization complete:"
186
+ puts " Files copied: #{@copied_files.size}"
187
+ puts " Files skipped: #{@skipped_files.size}"
188
+
189
+ if @skipped_files.any? && !@force
190
+ puts "\nUse --force to overwrite existing files"
191
+ end
192
+ end
193
+ end
194
+ end
195
+ end
196
+ end
197
+ 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.11.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.11.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-20 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,28 @@ files:
65
66
  - LICENSE
66
67
  - README.md
67
68
  - Rakefile
69
+ - docs/demo/ace-config-bootstrap-root-files.tape.yml
70
+ - docs/demo/ace-config-getting-started.tape.yml
71
+ - docs/usage.md
72
+ - exe/ace-config
68
73
  - lib/ace/support.rb
69
74
  - lib/ace/support/config.rb
70
75
  - lib/ace/support/config/atoms/deep_merger.rb
71
76
  - lib/ace/support/config/atoms/path_rule_matcher.rb
72
77
  - lib/ace/support/config/atoms/path_validator.rb
73
78
  - lib/ace/support/config/atoms/yaml_parser.rb
79
+ - lib/ace/support/config/cli.rb
74
80
  - lib/ace/support/config/errors.rb
75
81
  - lib/ace/support/config/models/cascade_path.rb
76
82
  - lib/ace/support/config/models/config.rb
77
83
  - lib/ace/support/config/models/config_group.rb
84
+ - lib/ace/support/config/models/config_templates.rb
78
85
  - lib/ace/support/config/molecules/config_finder.rb
79
86
  - lib/ace/support/config/molecules/file_config_resolver.rb
80
87
  - lib/ace/support/config/molecules/project_config_scanner.rb
81
88
  - lib/ace/support/config/molecules/yaml_loader.rb
89
+ - lib/ace/support/config/organisms/config_diff.rb
90
+ - lib/ace/support/config/organisms/config_initializer.rb
82
91
  - lib/ace/support/config/organisms/config_resolver.rb
83
92
  - lib/ace/support/config/organisms/virtual_config_resolver.rb
84
93
  - lib/ace/support/config/version.rb