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 +4 -4
- data/CHANGELOG.md +28 -0
- data/README.md +12 -0
- data/docs/demo/ace-config-getting-started.tape.yml +30 -0
- data/docs/usage.md +200 -0
- data/exe/ace-config +11 -0
- data/lib/ace/support/config/cli.rb +192 -0
- data/lib/ace/support/config/models/config_templates.rb +90 -0
- data/lib/ace/support/config/organisms/config_diff.rb +190 -0
- data/lib/ace/support/config/organisms/config_initializer.rb +116 -0
- data/lib/ace/support/config/version.rb +1 -1
- data/lib/ace/support/config.rb +3 -0
- metadata +12 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 16e2e0c5255bb10848420c2985d022bdd807e268e8221131fda820d132539d9a
|
|
4
|
+
data.tar.gz: adebbb9e9f34fb1bb04d6afd408bf2bcff4d64a18634198051f911e57014e623
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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,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
|
data/lib/ace/support/config.rb
CHANGED
|
@@ -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.
|
|
4
|
+
version: 0.10.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Michal Czyz
|
|
8
|
-
bindir:
|
|
8
|
+
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-
|
|
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
|