agent_settings 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 15e2352fd94c8dbf7fe85836e0c9b48a0dd8cc09e631674949ef301a28083dbc
4
+ data.tar.gz: 8f618aba7e01370d35ec3d5a3cb66c4f7f637788487cb656dea2449c3334b96a
5
+ SHA512:
6
+ metadata.gz: 14c65ca62831a314b49f92157cbe229c51da19d333274f16545a3b459450d7e789a4d21b7a9fd2a9a010d6029f8a27de08920a665bb16007d7b0aaad2641d645
7
+ data.tar.gz: a778b94ef5bcd36571c8e91e8cf7c18cfe2fe7272a4f9bd573624806a94c22d246cd2ced383304d3ca3d41f9657d8d2b724dc0ce539ec575c5c34d6cebd27cc6
data/AGENTS.md ADDED
@@ -0,0 +1,244 @@
1
+ # Agent Settings - Ruby Gem
2
+
3
+ Ruby gem that provides a clean interface to discover config locations for Claude Code, OpenCode, and Codex — including whether each path exists and which path is currently effective based on precedence and overrides.
4
+
5
+ Read @doc/ruby.md before writing any Ruby code.
6
+
7
+ ## Build / Lint / Test Commands
8
+
9
+ ```bash
10
+ # Install dependencies
11
+ bundle install
12
+
13
+ # Run all tests
14
+ bundle exec rake test
15
+
16
+ # Run a single test file
17
+ bundle exec ruby -Ilib:test test/agent_settings/resolver_test.rb
18
+
19
+ # Run a single test by name
20
+ bundle exec ruby -Ilib:test test/agent_settings/resolver_test.rb --name test_global_resolution
21
+
22
+ # Run tests with verbose output
23
+ bundle exec rake test TESTOPTS="--verbose"
24
+
25
+ # Lint with RuboCop (if configured)
26
+ bundle exec rubocop
27
+
28
+ # Lint and auto-correct
29
+ bundle exec rubocop -a
30
+
31
+ # Build the gem
32
+ bundle exec rake build
33
+
34
+ # Install the gem locally
35
+ bundle exec rake install
36
+
37
+ # Run the console for experimentation
38
+ bundle exec console
39
+ ```
40
+
41
+ ## Project Structure
42
+
43
+ ```
44
+ lib/agent_settings.rb # Entry point with Zeitwerk loader
45
+ lib/agent_settings/version.rb # Version constant
46
+ lib/agent_settings/location.rb # Location value object
47
+ lib/agent_settings/env_override.rb # EnvOverride value object
48
+ lib/agent_settings/agent_config_path.rb # AgentConfigPath value object
49
+ lib/agent_settings/location_discovery.rb # Core discovery logic
50
+ lib/agent_settings/registry.rb # Agent-to-adapter mapping
51
+ lib/agent_settings/adapters/ # Per-agent adapters
52
+ claude.rb
53
+ opencode.rb
54
+ codex.rb
55
+ test/ # Minitest tests
56
+ ```
57
+
58
+ ## Public API
59
+
60
+ ```ruby
61
+ AgentSettings.global(agent, env: ENV, dir: Dir.pwd, trusted: true)
62
+ AgentSettings.project(agent, dir:, env: ENV, trusted: true)
63
+ AgentSettings.resolve(agent, dir:, env: ENV, trusted: true)
64
+ AgentSettings.all(dir:, env: ENV, trusted: true)
65
+ ```
66
+
67
+ Supported agents: `:claude`, `:opencode`, `:codex`
68
+
69
+ ## Code Style Guidelines
70
+
71
+ ### Ruby 3.x Features
72
+
73
+ Use modern Ruby features throughout:
74
+
75
+ ```ruby
76
+ # Data objects for immutable value objects
77
+ Location = Data.define(:agent, :scope, :path, :exists, :active) do
78
+ def exists? = exists
79
+ def active? = active
80
+ end
81
+
82
+ # Pattern matching
83
+ case agent
84
+ in :claude then ClaudeAdapter.new
85
+ in :opencode then OpencodeAdapter.new
86
+ in :codex then CodexAdapter.new
87
+ end
88
+
89
+ # Endless methods for simple one-liners
90
+ def global = layers.find { |l| l.scope == :global }
91
+ def custom_config? = variables.any?(&:active?)
92
+
93
+ # Keyword arguments throughout
94
+ def resolve(agent, dir:, env: ENV, trusted: true)
95
+
96
+ # Safe navigation operator
97
+ path = env["CLAUDE_CONFIG_DIR"]&.then { |dir| File.join(dir, "settings.json") }
98
+ ```
99
+
100
+ ### Naming Conventions
101
+
102
+ - **Boolean methods** end with `?`: `exists?`, `active?`, `custom_config?`
103
+ - **Positive names**: use `active` not `not_deleted`
104
+ - **Role-based naming**: name variables after their role, not type
105
+ - **No pattern names in classes**: avoid `ResolverFactory`, `LocationDecorator`
106
+ - **Domain language**: reflect the business domain in names
107
+
108
+ ### Method Design
109
+
110
+ - **Composed methods**: each method does one thing at one abstraction level
111
+ - **Intention-revealing selectors**: name after what, not how
112
+ - **Keyword arguments**: always prefer over positional arguments
113
+ - **Tiny public surface**: expose only what callers need
114
+
115
+ ### Class Design
116
+
117
+ - **Single responsibility**: one sentence description, no "and/or"
118
+ - **Cohesion**: everything in a class shares one idea
119
+ - **Composition over inheritance**: prefer composition
120
+ - **Law of Demeter**: only talk to immediate neighbors
121
+ - **Tell, don't ask**: send messages, avoid train wrecks
122
+
123
+ ### Value Objects
124
+
125
+ Use `Data.define` for immutable value objects:
126
+
127
+ ```ruby
128
+ module AgentSettings
129
+ Location = Data.define(:agent, :scope, :source, :path, :exists, :active, :note) do
130
+ def exists? = exists
131
+ def active? = active
132
+ end
133
+
134
+ EnvOverride = Data.define(:agent, :name, :value, :path, :exists, :active, :note) do
135
+ def exists? = exists
136
+ def active? = active
137
+ end
138
+
139
+ AgentConfigPath = Data.define(:agent, :effective, :global, :project, :layers, :env_overrides, :warnings) do
140
+ def custom_config? = env_overrides.any?(&:active?)
141
+ end
142
+ end
143
+ ```
144
+
145
+ ### Error Handling
146
+
147
+ Use custom error classes for domain errors:
148
+
149
+ ```ruby
150
+ module AgentSettings
151
+ class Error < StandardError; end
152
+ class UnknownAgentError < Error; end
153
+ class InvalidConfigError < Error; end
154
+ end
155
+
156
+ # Raise with clear messages
157
+ raise UnknownAgentError, "Unknown agent: #{agent}. Supported: #{Registry::AGENTS.join(', ')}"
158
+ ```
159
+
160
+ ### Adapter Protocol
161
+
162
+ Each adapter must implement:
163
+
164
+ ```ruby
165
+ class SomeAdapter
166
+ # Returns array of Location objects, ordered by precedence (highest first)
167
+ def layers(dir:, env:, trusted:) = [...]
168
+
169
+ # Returns array of EnvOverride objects for env overrides
170
+ def env_overrides(dir:, env:, trusted:) = [...]
171
+
172
+ # Returns array of warning strings
173
+ def warnings(dir:, env:, trusted:) = []
174
+ end
175
+ ```
176
+
177
+ ## Agent Config Rules
178
+
179
+ ### Claude Code
180
+ - Global: `~/.claude/settings.json`
181
+ - Project: `.claude/settings.json`
182
+ - Local override: `.claude/settings.local.json`
183
+ - Managed: `/Library/Application Support/ClaudeCode/managed-settings.json` (macOS)
184
+ - Env override: `CLAUDE_CONFIG_DIR`
185
+ - Precedence: managed > CLI > local project > project > user
186
+
187
+ ### OpenCode
188
+ - Global: `~/.config/opencode/opencode.json`
189
+ - Project: `opencode.json` (discovered upward from dir)
190
+ - Env overrides: `OPENCODE_CONFIG`, `OPENCODE_CONFIG_DIR`, `OPENCODE_CONFIG_CONTENT`
191
+
192
+ ### Codex
193
+ - User: `~/.codex/config.toml`
194
+ - Project: `.codex/config.toml` (trusted projects only)
195
+ - System: `/etc/codex/config.toml`
196
+ - Env override: `CODEX_HOME`
197
+ - Trust caveat: project config ignored for untrusted projects
198
+
199
+ ## Testing Strategy
200
+
201
+ - **Test public interfaces only**: test what callers see
202
+ - **Test private methods indirectly**: through public API
203
+ - **Cover these scenarios**:
204
+ - Global/project resolution per agent
205
+ - Override variable detection and activation
206
+ - Effective path precedence
207
+ - `exists?` true/false for paths
208
+ - Trusted vs untrusted Codex behavior
209
+
210
+ ```ruby
211
+ class LocationDiscoveryTest < Minitest::Test
212
+ def test_global_resolution_for_claude
213
+ result = AgentSettings.global(:claude)
214
+ assert_equal :global, result.scope
215
+ assert_predicate result, :exists?
216
+ end
217
+
218
+ def test_project_config_ignored_for_untrusted_codex
219
+ result = AgentSettings.resolve(:codex, dir: "/untrusted/repo", trusted: false)
220
+ assert_nil result.project
221
+ end
222
+ end
223
+ ```
224
+
225
+ ## Dependencies
226
+
227
+ - **Runtime**: `zeitwerk` (autoloading)
228
+ - **Development**: `minitest`, `rake`, `rubocop` (optional)
229
+
230
+ Use Zeitwerk at entry point:
231
+
232
+ ```ruby
233
+ # lib/agent_settings.rb
234
+ require "zeitwerk"
235
+ Zeitwerk::Loader.for_gem.setup
236
+ ```
237
+
238
+ ## Design Principles
239
+
240
+ 1. **Messaging over objects**: focus on messages passing between objects
241
+ 2. **Behavior over data**: depend on what objects do, not what they are
242
+ 3. **Duck typing**: type is defined by behavior, not class
243
+ 4. **Dependency injection**: pass collaborators, don't hard-code class names
244
+ 5. **Late binding**: allow objects to hide internal state-process
data/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 - 2025-02-24
4
+
5
+ Initial release.
6
+
7
+ - Discover config locations for Claude Code, OpenCode, and Codex
8
+ - Global and project config resolution
9
+ - Environment variable override detection
10
+ - Precedence-based active config determination
11
+ - Trusted/untrusted project handling for Codex
data/CLAUDE.md ADDED
@@ -0,0 +1,95 @@
1
+ # agent_settings
2
+
3
+ Ruby gem that provides a clean interface to discover config locations for Claude Code, OpenCode, and Codex — including whether each path exists and which path is currently effective based on precedence and overrides.
4
+
5
+ Read @doc/ruby.md before writing any Ruby code.
6
+
7
+ ## Public API
8
+
9
+ ```ruby
10
+ AgentSettings.global(agent, env: ENV, dir: Dir.pwd, trusted: true)
11
+ AgentSettings.project(agent, dir:, env: ENV, trusted: true)
12
+ AgentSettings.resolve(agent, dir:, env: ENV, trusted: true)
13
+ AgentSettings.all(dir:, env: ENV, trusted: true)
14
+ ```
15
+
16
+ ## File Layout
17
+
18
+ ```
19
+ lib/agent_settings.rb
20
+ lib/agent_settings/version.rb
21
+ lib/agent_settings/location.rb
22
+ lib/agent_settings/variable.rb
23
+ lib/agent_settings/resolution.rb
24
+ lib/agent_settings/resolver.rb
25
+ lib/agent_settings/registry.rb
26
+ lib/agent_settings/adapters/claude.rb
27
+ lib/agent_settings/adapters/opencode.rb
28
+ lib/agent_settings/adapters/codex.rb
29
+ test/
30
+ ```
31
+
32
+ ## Core Value Objects
33
+
34
+ - `Location` — fields: `agent, scope, source, path, exists, active, note`; methods: `exists?`, `active?`
35
+ - `Variable` — fields: `agent, name, value, path, exists, active, note`; methods: `exists?`, `active?`
36
+ - `Resolution` — fields: `agent, effective, global, project, layers, variables, warnings`; method: `custom_config?`
37
+
38
+ ## Core Classes
39
+
40
+ - `Resolver` — builds final `Resolution`, picks effective location from precedence layers
41
+ - `Registry` — maps agent symbols to adapters
42
+ - `Adapters::Claude`, `Adapters::Opencode`, `Adapters::Codex` — each exposes `layers(dir:, env:, trusted:)`, `variables(dir:, env:, trusted:)`, `warnings(dir:, env:, trusted:)`
43
+
44
+ ## Agent Config Rules
45
+
46
+ ### Claude Code
47
+ - Global: `~/.claude/settings.json`
48
+ - Project: `.claude/settings.json`
49
+ - Local project override: `.claude/settings.local.json`
50
+ - Managed/system: `/Library/Application Support/ClaudeCode/managed-settings.json` (macOS), `/etc/claude-code/managed-settings.json` (Linux)
51
+ - Env override: `CLAUDE_CONFIG_DIR`
52
+ - Precedence (high→low): managed > CLI > local project > project > user
53
+
54
+ ### OpenCode
55
+ - Global: `~/.config/opencode/opencode.json`
56
+ - Project: `opencode.json` (discovered from dir toward project root)
57
+ - Env overrides: `OPENCODE_CONFIG`, `OPENCODE_CONFIG_DIR`, `OPENCODE_CONFIG_CONTENT`
58
+
59
+ ### Codex
60
+ - User: `~/.codex/config.toml`
61
+ - Project: `.codex/config.toml` (trusted projects only)
62
+ - System: `/etc/codex/config.toml`
63
+ - Env base override: `CODEX_HOME`
64
+
65
+ ## Ruby Conventions
66
+
67
+ Use Ruby 3.x features:
68
+ - `Data` objects for immutable value objects (`Location`, `Variable`, `Resolution`)
69
+ - Pattern matching (`case/in`, `=>`)
70
+ - Endless methods for simple one-liners
71
+ - Keyword arguments throughout
72
+ - Safe navigation operator (`&.`)
73
+
74
+ Design principles (Sandi Metz / OOD):
75
+ - Tiny public surface — expose only what callers need
76
+ - Intention-revealing names — reflect domain, not implementation
77
+ - Single responsibility — one sentence description per class, no "and/or"
78
+ - Tell, don't ask — send messages, avoid train wrecks
79
+ - Depend on behavior (duck types), not class names
80
+ - Composed methods — each method does one thing at one level of abstraction
81
+
82
+ Naming rules:
83
+ - Boolean methods end with `?` (e.g., `exists?`, `active?`, `custom_config?`)
84
+ - Positive names (`active` not `not_deleted`)
85
+ - Variables named for their role, not their type
86
+ - No design pattern names in class names
87
+
88
+ ## Testing (Minitest)
89
+
90
+ - Test public interfaces only; test private methods indirectly
91
+ - Cover: global/project resolution per agent, override variable detection, effective path precedence, `exists?` true/false, trusted vs untrusted Codex behavior
92
+
93
+ ## Dependencies
94
+
95
+ - `zeitwerk` runtime dependency; use `Zeitwerk::Loader.for_gem` in entrypoint
data/README.md ADDED
@@ -0,0 +1,177 @@
1
+ # agent_settings
2
+
3
+ Discover config locations for Claude Code, OpenCode, and Codex
4
+
5
+ If you are an LLM/AI Agent read [./llm.md](llm.md) in this repository.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's **Gemfile**:
10
+
11
+ ```ruby
12
+ gem "agent_settings"
13
+ ```
14
+
15
+ And run:
16
+
17
+ ```bash
18
+ bundle install
19
+ ```
20
+
21
+ Or install it directly:
22
+
23
+ ```bash
24
+ gem install agent_settings
25
+ ```
26
+
27
+ ## Quick Start
28
+
29
+ Get the effective config for an agent:
30
+
31
+ ```ruby
32
+ require "agent_settings"
33
+
34
+ result = AgentSettings.resolve(:claude, dir: Dir.pwd)
35
+ puts result.effective.path
36
+ ```
37
+
38
+ ## Usage
39
+
40
+ ### Get Global Config
41
+
42
+ Retrieve the global config location for an agent:
43
+
44
+ ```ruby
45
+ location = AgentSettings.global(:claude)
46
+ location.path # => "/Users/me/.claude/settings.json"
47
+ location.exists? # => true
48
+ ```
49
+
50
+ ### Get Project Config
51
+
52
+ Retrieve the project-level config location:
53
+
54
+ ```ruby
55
+ location = AgentSettings.project(:codex, dir: "/work/my_app")
56
+ location.path # => "/work/my_app/.codex/config.toml"
57
+ location.exists? # => false
58
+ ```
59
+
60
+ ### Resolve Effective Config
61
+
62
+ Get the active config with full details:
63
+
64
+ ```ruby
65
+ result = AgentSettings.resolve(:opencode, dir: "/work/my_app")
66
+
67
+ result.effective.path # the active config path
68
+ result.global # global location
69
+ result.project # project location
70
+ result.layers # all config layers by precedence
71
+ result.env_overrides # environment variable overrides
72
+ result.warnings # any warnings
73
+ result.custom_config? # true if env override is active
74
+ ```
75
+
76
+ ### Resolve All Agents
77
+
78
+ Get config paths for all supported agents:
79
+
80
+ ```ruby
81
+ results = AgentSettings.all(dir: "/work/my_app")
82
+
83
+ results.each do |agent, config_path|
84
+ puts "#{agent}: #{config_path.effective.path}"
85
+ end
86
+ ```
87
+
88
+ ### Detect Environment Overrides
89
+
90
+ Check if environment variables override config paths:
91
+
92
+ ```ruby
93
+ result = AgentSettings.resolve(:opencode, dir: Dir.pwd, env: ENV)
94
+
95
+ result.env_overrides.each do |override|
96
+ puts "#{override.name}=#{override.value}"
97
+ puts " active: #{override.active?}"
98
+ end
99
+ ```
100
+
101
+ ### Handle Untrusted Projects
102
+
103
+ Codex ignores project config for untrusted projects:
104
+
105
+ ```ruby
106
+ # trusted (default) - includes project config
107
+ result = AgentSettings.resolve(:codex, dir: dir, trusted: true)
108
+
109
+ # untrusted - excludes project config
110
+ result = AgentSettings.resolve(:codex, dir: dir, trusted: false)
111
+ result.warnings # => ["Project config ignored for untrusted project"]
112
+ ```
113
+
114
+ ## Supported Agents
115
+
116
+ | Agent | Global Config | Project Config |
117
+ |-------|---------------|----------------|
118
+ | Claude | `~/.claude/settings.json` | `.claude/settings.json` |
119
+ | OpenCode | `~/.config/opencode/opencode.json` | `opencode.json` |
120
+ | Codex | `~/.codex/config.toml` | `.codex/config.toml` |
121
+
122
+ ## Options
123
+
124
+ All methods accept these keyword arguments:
125
+
126
+ | Option | Type | Default | Description |
127
+ |--------|------|---------|-------------|
128
+ | `agent` | Symbol | required | `:claude`, `:opencode`, or `:codex` |
129
+ | `dir` | String | `Dir.pwd` | Project directory path |
130
+ | `env` | Hash | `ENV` | Environment variables hash |
131
+ | `trusted` | Boolean | `true` | Trust project for Codex |
132
+
133
+ ## Value Objects
134
+
135
+ ### Location
136
+
137
+ Represents a config file location:
138
+
139
+ ```ruby
140
+ location.agent # => :claude
141
+ location.scope # => :global or :project
142
+ location.path # => "/Users/me/.claude/settings.json"
143
+ location.exists? # => true
144
+ location.active? # => true
145
+ ```
146
+
147
+ ### EnvOverride
148
+
149
+ Represents an environment variable override:
150
+
151
+ ```ruby
152
+ override.name # => "CLAUDE_CONFIG_DIR"
153
+ override.value # => "/custom/path"
154
+ override.path # => "/custom/path/settings.json"
155
+ override.active? # => true
156
+ ```
157
+
158
+ ### AgentConfigPath
159
+
160
+ The result of resolving an agent's config:
161
+
162
+ ```ruby
163
+ config.effective # => Location (highest precedence)
164
+ config.global # => Location or nil
165
+ config.project # => Location or nil
166
+ config.layers # => Array of Location
167
+ config.env_overrides # => Array of EnvOverride
168
+ config.warnings # => Array of String
169
+ config.custom_config? # => true if env override active
170
+ ```
171
+
172
+ ## Contributing
173
+
174
+ Bug reports and pull requests are welcome on GitHub.
175
+
176
+ ## License
177
+ The gem is available as open source under the terms of the Apache 2.0 License.
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AgentSettings
4
+ module Adapters
5
+ # Adapter for Claude Code agent configuration.
6
+ #
7
+ # Claude Code stores configuration in JSON files at various locations:
8
+ #
9
+ # - Global: ~/.claude/settings.json (user-level config)
10
+ # - Project: .claude/settings.json (project-level config)
11
+ # - Local: .claude/settings.local.json (local overrides, not committed)
12
+ # - Managed: /Library/Application Support/ClaudeCode/managed-settings.json (macOS system-level)
13
+ #
14
+ # Precedence (highest to lowest): managed > local > project > global
15
+ #
16
+ # Environment variable CLAUDE_CONFIG_DIR can override the global config location.
17
+ #
18
+ # @example
19
+ # adapter = Claude.new
20
+ # layers = adapter.layers(dir: "/work/my_app", env: ENV, trusted: true)
21
+ # env_overrides = adapter.env_overrides(dir: "/work/my_app", env: ENV, trusted: true)
22
+ class Claude
23
+ # Get all config layers for Claude Code.
24
+ #
25
+ # Returns an array of Location objects representing all possible
26
+ # config file locations, ordered by precedence (highest first).
27
+ # Only one layer will be marked as active (the first existing one).
28
+ #
29
+ # @param dir [String] project directory path
30
+ # @param env [Hash] environment variables hash
31
+ # @param trusted [Boolean] whether the project is trusted (unused for Claude)
32
+ # @return [Array<Location>] config layers in precedence order
33
+ def layers(dir:, env:, trusted:) # rubocop:disable Lint/UnusedMethodArgument
34
+ build_layers(dir, env)
35
+ end
36
+
37
+ # Get environment variable overrides for Claude Code.
38
+ #
39
+ # Checks for CLAUDE_CONFIG_DIR environment variable and returns
40
+ # an EnvOverride if it's set. The override is active if the
41
+ # resulting settings.json file exists.
42
+ #
43
+ # @param dir [String] project directory path (unused)
44
+ # @param env [Hash] environment variables hash
45
+ # @param trusted [Boolean] whether the project is trusted (unused)
46
+ # @return [Array<EnvOverride>] detected environment overrides
47
+ def env_overrides(dir:, env:, trusted:) # rubocop:disable Lint/UnusedMethodArgument
48
+ build_env_overrides(env)
49
+ end
50
+
51
+ # Get warnings for Claude Code configuration.
52
+ #
53
+ # Claude Code doesn't produce any warnings currently.
54
+ #
55
+ # @return [Array<String>] empty array
56
+ def warnings(*) = []
57
+
58
+ private
59
+
60
+ # Build the list of config layers.
61
+ #
62
+ # Creates Location objects for each possible config location,
63
+ # ordered by precedence. The managed config (system-level) has
64
+ # highest precedence, followed by local project overrides,
65
+ # then project config, then user global config.
66
+ #
67
+ # @param dir [String] project directory path
68
+ # @param env [Hash] environment variables hash
69
+ # @return [Array<Location>] config layers with active flag set
70
+ def build_layers(dir, env) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength -- builds all layer types in one coherent method
71
+ home = Dir.home
72
+ config_dir = env["CLAUDE_CONFIG_DIR"]
73
+
74
+ managed_path = managed_config_path
75
+ global_path = config_dir ? File.join(config_dir, "settings.json") : File.join(home, ".claude", "settings.json")
76
+ project_path = File.join(dir, ".claude", "settings.json")
77
+ local_path = File.join(dir, ".claude", "settings.local.json")
78
+
79
+ layers = []
80
+
81
+ layers << build_layer(:managed, "system", managed_path) if managed_path
82
+ layers << build_layer(:global, "user", global_path)
83
+ layers << build_layer(:project, "local", local_path)
84
+ layers << build_layer(:project, "file", project_path)
85
+
86
+ mark_active(layers)
87
+ end
88
+
89
+ # Build environment variable overrides.
90
+ #
91
+ # Checks if CLAUDE_CONFIG_DIR is set and creates an EnvOverride
92
+ # for it. The override's path is the settings.json file within
93
+ # the specified directory.
94
+ #
95
+ # @param env [Hash] environment variables hash
96
+ # @return [Array<EnvOverride>] detected overrides
97
+ def build_env_overrides(env) # rubocop:disable Metrics/MethodLength -- constructs EnvOverride with all required attributes
98
+ overrides = []
99
+ value = env["CLAUDE_CONFIG_DIR"]
100
+
101
+ if value
102
+ path = File.join(value, "settings.json")
103
+ overrides << EnvOverride.new(
104
+ agent: :claude,
105
+ name: "CLAUDE_CONFIG_DIR",
106
+ value: value,
107
+ path: path,
108
+ exists: File.exist?(path),
109
+ active: File.exist?(path),
110
+ note: "Config directory override"
111
+ )
112
+ end
113
+
114
+ overrides
115
+ end
116
+
117
+ # Get the managed (system-level) config path.
118
+ #
119
+ # Managed config is typically set by system administrators and
120
+ # has the highest precedence. Location varies by OS:
121
+ # - macOS: /Library/Application Support/ClaudeCode/managed-settings.json
122
+ # - Linux: /etc/claude-code/managed-settings.json
123
+ #
124
+ # @return [String, nil] the managed config path if it exists, nil otherwise
125
+ def managed_config_path
126
+ case RbConfig::CONFIG["host_os"]
127
+ when /darwin/
128
+ path = "/Library/Application Support/ClaudeCode/managed-settings.json"
129
+ File.exist?(path) ? path : nil
130
+ when /linux/
131
+ path = "/etc/claude-code/managed-settings.json"
132
+ File.exist?(path) ? path : nil
133
+ end
134
+ end
135
+
136
+ # Build a Location object for a config layer.
137
+ #
138
+ # @param scope [Symbol] the scope (:global, :project, :managed)
139
+ # @param source [String] where this config comes from
140
+ # @param path [String] the file path
141
+ # @return [Location] a Location object (not yet marked active)
142
+ def build_layer(scope, source, path)
143
+ Location.new(
144
+ agent: :claude,
145
+ scope: scope,
146
+ source: source,
147
+ path: path,
148
+ exists: File.exist?(path),
149
+ active: false,
150
+ note: nil
151
+ )
152
+ end
153
+
154
+ # Mark the first existing layer as active.
155
+ #
156
+ # Iterates through layers and marks the first one that exists
157
+ # as active. This implements the precedence rule where the
158
+ # first existing config file wins.
159
+ #
160
+ # @param layers [Array<Location>] config layers
161
+ # @return [Array<Location>] layers with active flag set
162
+ def mark_active(layers)
163
+ found = layers.find(&:exists?)
164
+ layers.map do |layer|
165
+ layer.with(active: layer == found)
166
+ end
167
+ end
168
+ end
169
+ end
170
+ end