devex 0.3.5
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 +7 -0
- data/.obsidian/app.json +6 -0
- data/.obsidian/appearance.json +4 -0
- data/.obsidian/community-plugins.json +5 -0
- data/.obsidian/core-plugins.json +33 -0
- data/.obsidian/plugins/obsidian-minimal-settings/data.json +34 -0
- data/.obsidian/plugins/obsidian-minimal-settings/main.js +8 -0
- data/.obsidian/plugins/obsidian-minimal-settings/manifest.json +11 -0
- data/.obsidian/plugins/obsidian-style-settings/data.json +15 -0
- data/.obsidian/plugins/obsidian-style-settings/main.js +165 -0
- data/.obsidian/plugins/obsidian-style-settings/manifest.json +10 -0
- data/.obsidian/plugins/obsidian-style-settings/styles.css +243 -0
- data/.obsidian/plugins/table-editor-obsidian/data.json +6 -0
- data/.obsidian/plugins/table-editor-obsidian/main.js +236 -0
- data/.obsidian/plugins/table-editor-obsidian/manifest.json +17 -0
- data/.obsidian/plugins/table-editor-obsidian/styles.css +78 -0
- data/.obsidian/themes/AnuPpuccin/manifest.json +7 -0
- data/.obsidian/themes/AnuPpuccin/theme.css +9080 -0
- data/.obsidian/themes/Minimal/manifest.json +8 -0
- data/.obsidian/themes/Minimal/theme.css +2251 -0
- data/.rubocop.yml +231 -0
- data/CHANGELOG.md +97 -0
- data/LICENSE +21 -0
- data/README.md +314 -0
- data/Rakefile +13 -0
- data/devex-logo.jpg +0 -0
- data/docs/developing-tools.md +1000 -0
- data/docs/ref/agent-mode.md +46 -0
- data/docs/ref/cli-interface.md +60 -0
- data/docs/ref/configuration.md +46 -0
- data/docs/ref/design-philosophy.md +17 -0
- data/docs/ref/error-handling.md +38 -0
- data/docs/ref/io-handling.md +88 -0
- data/docs/ref/signals.md +141 -0
- data/docs/ref/temporal-software-theory.md +790 -0
- data/exe/dx +52 -0
- data/lib/devex/builtins/.index.rb +10 -0
- data/lib/devex/builtins/debug.rb +43 -0
- data/lib/devex/builtins/format.rb +44 -0
- data/lib/devex/builtins/gem.rb +77 -0
- data/lib/devex/builtins/lint.rb +61 -0
- data/lib/devex/builtins/test.rb +76 -0
- data/lib/devex/builtins/version.rb +156 -0
- data/lib/devex/cli.rb +340 -0
- data/lib/devex/context.rb +433 -0
- data/lib/devex/core/configuration.rb +136 -0
- data/lib/devex/core.rb +79 -0
- data/lib/devex/dirs.rb +210 -0
- data/lib/devex/dsl.rb +100 -0
- data/lib/devex/exec/controller.rb +245 -0
- data/lib/devex/exec/result.rb +229 -0
- data/lib/devex/exec.rb +662 -0
- data/lib/devex/loader.rb +136 -0
- data/lib/devex/output.rb +257 -0
- data/lib/devex/project_paths.rb +309 -0
- data/lib/devex/support/ansi.rb +437 -0
- data/lib/devex/support/core_ext.rb +560 -0
- data/lib/devex/support/global.rb +68 -0
- data/lib/devex/support/path.rb +357 -0
- data/lib/devex/support.rb +71 -0
- data/lib/devex/template_helpers.rb +136 -0
- data/lib/devex/templates/debug.erb +24 -0
- data/lib/devex/tool.rb +374 -0
- data/lib/devex/version.rb +5 -0
- data/lib/devex/working_dir.rb +99 -0
- data/lib/devex.rb +158 -0
- data/ruby-project-template/.gitignore +0 -0
- data/ruby-project-template/Gemfile +0 -0
- data/ruby-project-template/README.md +0 -0
- data/ruby-project-template/docs/README.md +0 -0
- data/sig/devex.rbs +4 -0
- metadata +122 -0
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Devex
|
|
4
|
+
# Runtime context detection for CLI applications.
|
|
5
|
+
#
|
|
6
|
+
# Detects whether we're running in a terminal, CI, agent mode, etc.
|
|
7
|
+
# This informs output formatting, interactivity, and behavior.
|
|
8
|
+
#
|
|
9
|
+
# Detection hierarchy (highest to lowest priority):
|
|
10
|
+
# 1. Programmatic overrides (for testing)
|
|
11
|
+
# 2. Explicit environment variables ({PREFIX}_AGENT_MODE, etc.)
|
|
12
|
+
# 3. CI environment detection
|
|
13
|
+
# 4. Terminal/stream auto-detection
|
|
14
|
+
#
|
|
15
|
+
# Also tracks:
|
|
16
|
+
# - Task invocation call tree (which task invoked which)
|
|
17
|
+
# - Environment (development, test, staging, production)
|
|
18
|
+
#
|
|
19
|
+
# @example Core usage with custom env prefix
|
|
20
|
+
# config = Devex::Core::Configuration.new(env_prefix: "MYCLI")
|
|
21
|
+
# Devex::Context.configure(config)
|
|
22
|
+
# # Now checks MYCLI_AGENT_MODE, MYCLI_ENV, etc.
|
|
23
|
+
#
|
|
24
|
+
# See docs/ref/agent-mode.md and docs/ref/io-handling.md for rationale.
|
|
25
|
+
#
|
|
26
|
+
module Context
|
|
27
|
+
# Default environment variable names (dx-specific, for backward compatibility)
|
|
28
|
+
DEFAULT_ENV_AGENT_MODE = %w[DX_AGENT_MODE DEVEX_AGENT_MODE].freeze
|
|
29
|
+
DEFAULT_ENV_BATCH = %w[DX_BATCH DEVEX_BATCH].freeze
|
|
30
|
+
DEFAULT_ENV_INTERACTIVE = %w[DX_INTERACTIVE DEVEX_INTERACTIVE].freeze
|
|
31
|
+
DEFAULT_ENV_NO_COLOR = %w[NO_COLOR DX_NO_COLOR].freeze
|
|
32
|
+
DEFAULT_ENV_FORCE_COLOR = %w[FORCE_COLOR DX_FORCE_COLOR].freeze
|
|
33
|
+
DEFAULT_ENV_ENVIRONMENT = %w[DX_ENV DEVEX_ENV RAILS_ENV RACK_ENV].freeze
|
|
34
|
+
DEFAULT_ENV_CALL_TREE = "DX_CALL_TREE"
|
|
35
|
+
|
|
36
|
+
# Backward compatibility aliases
|
|
37
|
+
ENV_AGENT_MODE = DEFAULT_ENV_AGENT_MODE
|
|
38
|
+
ENV_BATCH = DEFAULT_ENV_BATCH
|
|
39
|
+
ENV_INTERACTIVE = DEFAULT_ENV_INTERACTIVE
|
|
40
|
+
ENV_NO_COLOR = DEFAULT_ENV_NO_COLOR
|
|
41
|
+
ENV_FORCE_COLOR = DEFAULT_ENV_FORCE_COLOR
|
|
42
|
+
ENV_ENVIRONMENT = DEFAULT_ENV_ENVIRONMENT
|
|
43
|
+
ENV_CALL_TREE = DEFAULT_ENV_CALL_TREE
|
|
44
|
+
|
|
45
|
+
DEFAULT_ENVIRONMENT = "development"
|
|
46
|
+
|
|
47
|
+
# Canonical environment names and their aliases
|
|
48
|
+
ENVIRONMENT_ALIASES = {
|
|
49
|
+
"dev" => "development",
|
|
50
|
+
"develop" => "development",
|
|
51
|
+
"test" => "test",
|
|
52
|
+
"testing" => "test",
|
|
53
|
+
"stage" => "staging",
|
|
54
|
+
"stg" => "staging",
|
|
55
|
+
"prod" => "production",
|
|
56
|
+
"live" => "production"
|
|
57
|
+
}.freeze
|
|
58
|
+
|
|
59
|
+
# Common CI environment variables
|
|
60
|
+
CI_ENV_VARS = %w[
|
|
61
|
+
CI
|
|
62
|
+
CONTINUOUS_INTEGRATION
|
|
63
|
+
GITHUB_ACTIONS
|
|
64
|
+
GITLAB_CI
|
|
65
|
+
CIRCLECI
|
|
66
|
+
TRAVIS
|
|
67
|
+
JENKINS_URL
|
|
68
|
+
BUILDKITE
|
|
69
|
+
DRONE
|
|
70
|
+
TEAMCITY_VERSION
|
|
71
|
+
].freeze
|
|
72
|
+
|
|
73
|
+
# Thread-local call stack for tracking task invocations within a process
|
|
74
|
+
# Format: Array of task names, e.g., ["pre-commit", "test"]
|
|
75
|
+
@call_stack = []
|
|
76
|
+
@call_stack_mutex = Mutex.new
|
|
77
|
+
|
|
78
|
+
# Programmatic overrides for testing and debugging
|
|
79
|
+
# Keys: :agent_mode, :interactive, :color, :ci, :terminal, :env
|
|
80
|
+
@overrides = {}
|
|
81
|
+
@overrides_mutex = Mutex.new
|
|
82
|
+
|
|
83
|
+
# Configuration for custom env prefix (nil = use defaults)
|
|
84
|
+
@config = nil
|
|
85
|
+
@config_mutex = Mutex.new
|
|
86
|
+
|
|
87
|
+
class << self
|
|
88
|
+
# Configure Context with a Core::Configuration
|
|
89
|
+
# @param config [Core::Configuration, nil] configuration with env_prefix
|
|
90
|
+
def configure(config)
|
|
91
|
+
@config_mutex.synchronize { @config = config }
|
|
92
|
+
reset_env! # Clear cached environment
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Get current configuration
|
|
96
|
+
# @return [Core::Configuration, nil]
|
|
97
|
+
def configuration
|
|
98
|
+
@config_mutex.synchronize { @config }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Reset configuration (for testing)
|
|
102
|
+
def reset_configuration!
|
|
103
|
+
@config_mutex.synchronize { @config = nil }
|
|
104
|
+
reset_env!
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Is stdout connected to a terminal?
|
|
108
|
+
def stdout_tty? = $stdout.tty?
|
|
109
|
+
|
|
110
|
+
# Is stderr connected to a terminal?
|
|
111
|
+
def stderr_tty? = $stderr.tty?
|
|
112
|
+
|
|
113
|
+
# Is stdin connected to a terminal?
|
|
114
|
+
def stdin_tty? = $stdin.tty?
|
|
115
|
+
|
|
116
|
+
# Are we in a full interactive terminal? (all three streams are ttys)
|
|
117
|
+
def terminal?
|
|
118
|
+
override = override_or(:terminal)
|
|
119
|
+
return override unless override.nil?
|
|
120
|
+
|
|
121
|
+
stdin_tty? && stdout_tty? && stderr_tty?
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Are stdout and stderr merged (pointing to same file descriptor)?
|
|
125
|
+
# This happens with 2>&1 redirection, common in agent/scripted usage.
|
|
126
|
+
#
|
|
127
|
+
# IMPORTANT: When both streams are TTYs pointing to the same terminal device,
|
|
128
|
+
# that's normal terminal behavior, NOT merging. We only consider streams
|
|
129
|
+
# "merged" when they're redirected to the same non-TTY destination.
|
|
130
|
+
def streams_merged?
|
|
131
|
+
# If either is a TTY, streams aren't "merged" in the problematic sense
|
|
132
|
+
# (a terminal naturally has stdout/stderr going to the same device)
|
|
133
|
+
return false if $stdout.tty? || $stderr.tty?
|
|
134
|
+
|
|
135
|
+
return false unless $stdout.respond_to?(:stat) && $stderr.respond_to?(:stat)
|
|
136
|
+
|
|
137
|
+
begin
|
|
138
|
+
stdout_stat = $stdout.stat
|
|
139
|
+
stderr_stat = $stderr.stat
|
|
140
|
+
stdout_stat.dev == stderr_stat.dev && stdout_stat.ino == stderr_stat.ino
|
|
141
|
+
rescue IOError, Errno::EBADF
|
|
142
|
+
# If we can't stat the streams, assume not merged
|
|
143
|
+
false
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Is agent mode explicitly enabled via environment?
|
|
148
|
+
def agent_mode_env?
|
|
149
|
+
env_vars_for(:agent_mode).any? { |var| truthy_env?(var) }
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Is batch mode explicitly enabled via environment?
|
|
153
|
+
def batch_mode_env?
|
|
154
|
+
env_vars_for(:batch).any? { |var| truthy_env?(var) }
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Is interactive mode explicitly forced via environment?
|
|
158
|
+
def interactive_forced?
|
|
159
|
+
env_vars_for(:interactive).any? { |var| truthy_env?(var) }
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Are we running in a CI environment?
|
|
163
|
+
def ci?
|
|
164
|
+
override = override_or(:ci)
|
|
165
|
+
return override unless override.nil?
|
|
166
|
+
|
|
167
|
+
CI_ENV_VARS.any? { |var| ENV.key?(var) && ENV[var] != "" && ENV[var] != "false" }
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Is color output explicitly disabled?
|
|
171
|
+
def no_color?
|
|
172
|
+
env_vars_for(:no_color).any? { |var| ENV.key?(var) }
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Is color output explicitly forced on?
|
|
176
|
+
def force_color?
|
|
177
|
+
env_vars_for(:force_color).any? { |var| truthy_env?(var) }
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Is data being piped in or out?
|
|
181
|
+
# True if stdin is not a tty (data piped in) OR stdout is not a tty (data piped out)
|
|
182
|
+
def piped? = !stdin_tty? || !stdout_tty?
|
|
183
|
+
|
|
184
|
+
# --- Composite detection methods ---
|
|
185
|
+
|
|
186
|
+
# Should we behave as if an AI agent is invoking us?
|
|
187
|
+
# True if:
|
|
188
|
+
# - Agent mode explicitly set, OR
|
|
189
|
+
# - Streams are merged (2>&1), OR
|
|
190
|
+
# - Not a terminal AND not explicitly interactive
|
|
191
|
+
def agent_mode?
|
|
192
|
+
override = override_or(:agent_mode)
|
|
193
|
+
return override unless override.nil?
|
|
194
|
+
|
|
195
|
+
return true if agent_mode_env?
|
|
196
|
+
return false if interactive_forced?
|
|
197
|
+
return true if streams_merged?
|
|
198
|
+
return true if !terminal? && !ci? # Non-tty, non-CI likely means agent
|
|
199
|
+
|
|
200
|
+
false
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Should we allow interactive prompts and rich output?
|
|
204
|
+
# True only when we have a real terminal and nothing forces non-interactive
|
|
205
|
+
def interactive?
|
|
206
|
+
override = override_or(:interactive)
|
|
207
|
+
return override unless override.nil?
|
|
208
|
+
|
|
209
|
+
return true if interactive_forced?
|
|
210
|
+
return false if agent_mode_env? || batch_mode_env? || ci?
|
|
211
|
+
|
|
212
|
+
terminal?
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Should we use colors in output?
|
|
216
|
+
def color?
|
|
217
|
+
override = override_or(:color)
|
|
218
|
+
return override unless override.nil?
|
|
219
|
+
|
|
220
|
+
return false if no_color?
|
|
221
|
+
return true if force_color?
|
|
222
|
+
|
|
223
|
+
# Default: color if stdout is a tty and not in agent mode
|
|
224
|
+
stdout_tty? && !agent_mode?
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# --- Environment detection (Rails-style) ---
|
|
228
|
+
|
|
229
|
+
# Get the current environment name (development, test, staging, production)
|
|
230
|
+
# Checks DX_ENV, DEVEX_ENV, RAILS_ENV, RACK_ENV in that order
|
|
231
|
+
def env = @env ||= detect_environment
|
|
232
|
+
|
|
233
|
+
# Reset cached environment (useful for testing)
|
|
234
|
+
def reset_env! = @env = nil
|
|
235
|
+
|
|
236
|
+
def development? = env == "development"
|
|
237
|
+
|
|
238
|
+
def test? = env == "test"
|
|
239
|
+
|
|
240
|
+
def staging? = env == "staging"
|
|
241
|
+
|
|
242
|
+
def production? = env == "production"
|
|
243
|
+
|
|
244
|
+
# Is this a "safe" environment where destructive operations are okay?
|
|
245
|
+
# Development and test are considered safe; staging and production are not.
|
|
246
|
+
def safe_env? = %w[development test].include?(env)
|
|
247
|
+
|
|
248
|
+
# --- Call tree tracking ---
|
|
249
|
+
|
|
250
|
+
# Get the full call tree as an array of task names
|
|
251
|
+
# Combines inherited tree from parent process (via env) with current process stack
|
|
252
|
+
def call_tree = inherited_tree + current_call_stack
|
|
253
|
+
|
|
254
|
+
# Get just the current process's call stack
|
|
255
|
+
def current_call_stack = @call_stack_mutex.synchronize { @call_stack.dup }
|
|
256
|
+
|
|
257
|
+
# Get the call tree inherited from parent process via environment
|
|
258
|
+
def inherited_tree
|
|
259
|
+
tree_str = ENV.fetch(call_tree_env_var, nil)
|
|
260
|
+
return [] if tree_str.nil? || tree_str.empty?
|
|
261
|
+
|
|
262
|
+
tree_str.split(":")
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Is this task being invoked from another task?
|
|
266
|
+
def invoked_from_task? = !call_tree.empty?
|
|
267
|
+
|
|
268
|
+
# Get the name of the task that invoked this one (immediate parent)
|
|
269
|
+
def invoking_task
|
|
270
|
+
tree = call_tree
|
|
271
|
+
tree[-1] if tree.any?
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# Get the root task that started the chain
|
|
275
|
+
def root_task
|
|
276
|
+
tree = call_tree
|
|
277
|
+
tree[0] if tree.any?
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Push a task onto the call stack (called when a task starts)
|
|
281
|
+
def push_task(task_name) = @call_stack_mutex.synchronize { @call_stack.push(task_name) }
|
|
282
|
+
|
|
283
|
+
# Pop a task from the call stack (called when a task completes)
|
|
284
|
+
def pop_task = @call_stack_mutex.synchronize { @call_stack.pop }
|
|
285
|
+
|
|
286
|
+
# Execute a block with a task on the call stack
|
|
287
|
+
def with_task(task_name)
|
|
288
|
+
push_task(task_name)
|
|
289
|
+
yield
|
|
290
|
+
ensure
|
|
291
|
+
pop_task
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Reset the call stack (useful for testing)
|
|
295
|
+
def reset_call_stack! = @call_stack_mutex.synchronize { @call_stack.clear }
|
|
296
|
+
|
|
297
|
+
# Summary of current context for debugging/logging
|
|
298
|
+
def summary
|
|
299
|
+
{
|
|
300
|
+
terminal: terminal?,
|
|
301
|
+
stdin_tty: stdin_tty?,
|
|
302
|
+
stdout_tty: stdout_tty?,
|
|
303
|
+
stderr_tty: stderr_tty?,
|
|
304
|
+
streams_merged: streams_merged?,
|
|
305
|
+
ci: ci?,
|
|
306
|
+
piped: piped?,
|
|
307
|
+
agent_mode: agent_mode?,
|
|
308
|
+
interactive: interactive?,
|
|
309
|
+
color: color?,
|
|
310
|
+
env: env,
|
|
311
|
+
call_tree: call_tree,
|
|
312
|
+
invoked_from_task: invoked_from_task?
|
|
313
|
+
}
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Machine-readable context for passing to subprocesses
|
|
317
|
+
# Include call tree so child processes know their invocation chain
|
|
318
|
+
def to_env
|
|
319
|
+
prefix = env_prefix
|
|
320
|
+
tree = call_tree
|
|
321
|
+
{
|
|
322
|
+
"#{prefix}_AGENT_MODE" => agent_mode? ? "1" : "0",
|
|
323
|
+
"#{prefix}_INTERACTIVE" => interactive? ? "1" : "0",
|
|
324
|
+
"#{prefix}_CI" => ci? ? "1" : "0",
|
|
325
|
+
"#{prefix}_ENV" => env,
|
|
326
|
+
call_tree_env_var => tree.any? ? tree.join(":") : nil
|
|
327
|
+
}.compact
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# Get the configured env prefix (or default)
|
|
331
|
+
def env_prefix
|
|
332
|
+
cfg = configuration
|
|
333
|
+
cfg&.env_prefix || "DX"
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# Get the call tree environment variable name
|
|
337
|
+
def call_tree_env_var
|
|
338
|
+
"#{env_prefix}_CALL_TREE"
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# --- Programmatic overrides for testing ---
|
|
342
|
+
|
|
343
|
+
# Set an override value
|
|
344
|
+
# Valid keys: :agent_mode, :interactive, :color, :ci, :terminal, :env
|
|
345
|
+
def set_override(key, value) = @overrides_mutex.synchronize { @overrides[key] = value }
|
|
346
|
+
|
|
347
|
+
# Clear a specific override
|
|
348
|
+
def clear_override(key) = @overrides_mutex.synchronize { @overrides.delete(key) }
|
|
349
|
+
|
|
350
|
+
# Clear all overrides
|
|
351
|
+
def clear_all_overrides! = @overrides_mutex.synchronize { @overrides.clear }
|
|
352
|
+
|
|
353
|
+
# Get current overrides (for debugging)
|
|
354
|
+
def overrides = @overrides_mutex.synchronize { @overrides.dup }
|
|
355
|
+
|
|
356
|
+
# Execute a block with temporary overrides
|
|
357
|
+
# Example: Context.with_overrides(agent_mode: true, color: false) { ... }
|
|
358
|
+
def with_overrides(**overrides_hash)
|
|
359
|
+
old_overrides = @overrides_mutex.synchronize { @overrides.dup }
|
|
360
|
+
@overrides_mutex.synchronize { @overrides.merge!(overrides_hash) }
|
|
361
|
+
yield
|
|
362
|
+
ensure
|
|
363
|
+
@overrides_mutex.synchronize do
|
|
364
|
+
@overrides.clear
|
|
365
|
+
@overrides.merge!(old_overrides)
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
private
|
|
370
|
+
|
|
371
|
+
# Check for override first, then fall back to detection
|
|
372
|
+
def override_or(key)
|
|
373
|
+
@overrides_mutex.synchronize do
|
|
374
|
+
return @overrides[key] if @overrides.key?(key)
|
|
375
|
+
end
|
|
376
|
+
nil
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def truthy_env?(var)
|
|
380
|
+
val = ENV.fetch(var, nil)
|
|
381
|
+
val && !val.empty? && val != "0" && val.downcase != "false"
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
# Get environment variable names to check for a given setting.
|
|
385
|
+
# If configuration is set, uses configured prefix only.
|
|
386
|
+
# Otherwise, falls back to default (dx-specific) names.
|
|
387
|
+
def env_vars_for(setting)
|
|
388
|
+
cfg = configuration
|
|
389
|
+
if cfg
|
|
390
|
+
# Use only the configured prefix
|
|
391
|
+
prefix = cfg.env_prefix
|
|
392
|
+
case setting
|
|
393
|
+
when :agent_mode then ["#{prefix}_AGENT_MODE"]
|
|
394
|
+
when :batch then ["#{prefix}_BATCH"]
|
|
395
|
+
when :interactive then ["#{prefix}_INTERACTIVE"]
|
|
396
|
+
when :no_color then ["NO_COLOR", "#{prefix}_NO_COLOR"]
|
|
397
|
+
when :force_color then ["FORCE_COLOR", "#{prefix}_FORCE_COLOR"]
|
|
398
|
+
when :env then ["#{prefix}_ENV", "RAILS_ENV", "RACK_ENV"]
|
|
399
|
+
else []
|
|
400
|
+
end
|
|
401
|
+
else
|
|
402
|
+
# Default (dx-specific) names for backward compatibility
|
|
403
|
+
case setting
|
|
404
|
+
when :agent_mode then DEFAULT_ENV_AGENT_MODE
|
|
405
|
+
when :batch then DEFAULT_ENV_BATCH
|
|
406
|
+
when :interactive then DEFAULT_ENV_INTERACTIVE
|
|
407
|
+
when :no_color then DEFAULT_ENV_NO_COLOR
|
|
408
|
+
when :force_color then DEFAULT_ENV_FORCE_COLOR
|
|
409
|
+
when :env then DEFAULT_ENV_ENVIRONMENT
|
|
410
|
+
else []
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def detect_environment
|
|
416
|
+
# Check override first
|
|
417
|
+
override = override_or(:env)
|
|
418
|
+
return override if override
|
|
419
|
+
|
|
420
|
+
env_vars_for(:env).each do |var|
|
|
421
|
+
val = ENV.fetch(var, nil)
|
|
422
|
+
next if val.nil? || val.empty?
|
|
423
|
+
|
|
424
|
+
# Normalize the value
|
|
425
|
+
normalized = val.downcase.strip
|
|
426
|
+
return ENVIRONMENT_ALIASES.fetch(normalized, normalized)
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
DEFAULT_ENVIRONMENT
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
end
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Devex
|
|
4
|
+
module Core
|
|
5
|
+
# Configuration for a CLI application built on Devex::Core.
|
|
6
|
+
#
|
|
7
|
+
# Holds all the customizable values that distinguish one CLI from another:
|
|
8
|
+
# executable name, flag prefixes, project markers, environment variable
|
|
9
|
+
# prefixes, and path conventions.
|
|
10
|
+
#
|
|
11
|
+
# @example Building a custom CLI
|
|
12
|
+
# config = Devex::Core::Configuration.new(
|
|
13
|
+
# executable_name: "mycli",
|
|
14
|
+
# project_markers: %w[.mycli.yml .git Gemfile],
|
|
15
|
+
# env_prefix: "MYCLI"
|
|
16
|
+
# )
|
|
17
|
+
# cli = Devex::Core::CLI.new(config: config)
|
|
18
|
+
#
|
|
19
|
+
class Configuration
|
|
20
|
+
# Name of the executable (used in help text, error messages)
|
|
21
|
+
# @return [String]
|
|
22
|
+
attr_accessor :executable_name
|
|
23
|
+
|
|
24
|
+
# Prefix for framework flags (--{prefix}-version, --{prefix}-agent-mode)
|
|
25
|
+
# Defaults to executable_name if not set
|
|
26
|
+
# @return [String]
|
|
27
|
+
attr_writer :flag_prefix
|
|
28
|
+
|
|
29
|
+
# Files/directories that indicate project root, checked in order
|
|
30
|
+
# @return [Array<String>]
|
|
31
|
+
attr_accessor :project_markers
|
|
32
|
+
|
|
33
|
+
# Primary config file name (e.g., ".dx.yml", ".mycli.yml")
|
|
34
|
+
# Set to nil to disable config file support
|
|
35
|
+
# @return [String, nil]
|
|
36
|
+
attr_accessor :config_file
|
|
37
|
+
|
|
38
|
+
# Directory for organized mode (e.g., ".dx", ".mycli")
|
|
39
|
+
# Set to nil to disable organized mode support
|
|
40
|
+
# @return [String, nil]
|
|
41
|
+
attr_accessor :organized_dir
|
|
42
|
+
|
|
43
|
+
# Default tools directory name
|
|
44
|
+
# @return [String]
|
|
45
|
+
attr_accessor :tools_dir
|
|
46
|
+
|
|
47
|
+
# Prefix for environment variables ({PREFIX}_AGENT_MODE, {PREFIX}_ENV)
|
|
48
|
+
# Defaults to executable_name.upcase if not set
|
|
49
|
+
# @return [String]
|
|
50
|
+
attr_writer :env_prefix
|
|
51
|
+
|
|
52
|
+
# Custom path conventions for ProjectPaths
|
|
53
|
+
# Merged with defaults; keys are symbols, values are strings or arrays
|
|
54
|
+
# @return [Hash{Symbol => String, Array<String>}]
|
|
55
|
+
attr_accessor :path_conventions
|
|
56
|
+
|
|
57
|
+
# File that triggers delegation to bundled version (e.g., ".dx-use-local")
|
|
58
|
+
# Set to nil to disable delegation support
|
|
59
|
+
# @return [String, nil]
|
|
60
|
+
attr_accessor :delegation_file
|
|
61
|
+
|
|
62
|
+
def initialize(
|
|
63
|
+
executable_name: "cli",
|
|
64
|
+
flag_prefix: nil,
|
|
65
|
+
project_markers: %w[.git Gemfile Rakefile],
|
|
66
|
+
config_file: nil,
|
|
67
|
+
organized_dir: nil,
|
|
68
|
+
tools_dir: "tools",
|
|
69
|
+
env_prefix: nil,
|
|
70
|
+
path_conventions: {},
|
|
71
|
+
delegation_file: nil
|
|
72
|
+
)
|
|
73
|
+
@executable_name = executable_name
|
|
74
|
+
@flag_prefix = flag_prefix
|
|
75
|
+
@project_markers = project_markers
|
|
76
|
+
@config_file = config_file
|
|
77
|
+
@organized_dir = organized_dir
|
|
78
|
+
@tools_dir = tools_dir
|
|
79
|
+
@env_prefix = env_prefix
|
|
80
|
+
@path_conventions = path_conventions
|
|
81
|
+
@delegation_file = delegation_file
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Flag prefix, defaulting to executable_name
|
|
85
|
+
# @return [String]
|
|
86
|
+
def flag_prefix
|
|
87
|
+
@flag_prefix || executable_name
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Environment variable prefix, defaulting to executable_name.upcase
|
|
91
|
+
# @return [String]
|
|
92
|
+
def env_prefix
|
|
93
|
+
@env_prefix || executable_name.upcase.tr("-", "_")
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Generate an environment variable name with this config's prefix
|
|
97
|
+
# @param name [String, Symbol] variable name (e.g., :agent_mode)
|
|
98
|
+
# @return [String] full env var name (e.g., "DX_AGENT_MODE")
|
|
99
|
+
def env_var(name)
|
|
100
|
+
"#{env_prefix}_#{name.to_s.upcase}"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Generate a flag name with this config's prefix
|
|
104
|
+
# @param name [String, Symbol] flag name (e.g., :version)
|
|
105
|
+
# @return [String] full flag name (e.g., "--dx-version")
|
|
106
|
+
def flag(name)
|
|
107
|
+
"--#{flag_prefix}-#{name.to_s.tr('_', '-')}"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Duplicate this configuration with overrides
|
|
111
|
+
# @param overrides [Hash] values to override
|
|
112
|
+
# @return [Configuration] new configuration instance
|
|
113
|
+
def with(**overrides)
|
|
114
|
+
self.class.new(
|
|
115
|
+
executable_name: overrides.fetch(:executable_name, executable_name),
|
|
116
|
+
flag_prefix: overrides.fetch(:flag_prefix, @flag_prefix),
|
|
117
|
+
project_markers: overrides.fetch(:project_markers, project_markers.dup),
|
|
118
|
+
config_file: overrides.fetch(:config_file, config_file),
|
|
119
|
+
organized_dir: overrides.fetch(:organized_dir, organized_dir),
|
|
120
|
+
tools_dir: overrides.fetch(:tools_dir, tools_dir),
|
|
121
|
+
env_prefix: overrides.fetch(:env_prefix, @env_prefix),
|
|
122
|
+
path_conventions: overrides.fetch(:path_conventions, path_conventions.dup),
|
|
123
|
+
delegation_file: overrides.fetch(:delegation_file, delegation_file)
|
|
124
|
+
)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Freeze this configuration (and nested structures)
|
|
128
|
+
# @return [self]
|
|
129
|
+
def freeze
|
|
130
|
+
@project_markers.freeze
|
|
131
|
+
@path_conventions.freeze
|
|
132
|
+
super
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
data/lib/devex/core.rb
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Devex::Core - The CLI framework without dx-specific configuration.
|
|
4
|
+
#
|
|
5
|
+
# Use this entry point to build your own CLI application using the devex
|
|
6
|
+
# framework. This loads all framework components but does NOT load:
|
|
7
|
+
# - dx-specific builtins (version, test, lint, etc.)
|
|
8
|
+
# - dx-specific configuration (.dx.yml, .dx/ directory conventions)
|
|
9
|
+
#
|
|
10
|
+
# @example Building a custom CLI
|
|
11
|
+
# require "devex/core"
|
|
12
|
+
#
|
|
13
|
+
# config = Devex::Core::Configuration.new(
|
|
14
|
+
# executable_name: "mycli",
|
|
15
|
+
# flag_prefix: "mycli",
|
|
16
|
+
# project_markers: %w[.mycli.yml .git Gemfile],
|
|
17
|
+
# env_prefix: "MYCLI"
|
|
18
|
+
# )
|
|
19
|
+
#
|
|
20
|
+
# cli = Devex::Core::CLI.new(config: config)
|
|
21
|
+
# cli.load_tools("/path/to/my/tools")
|
|
22
|
+
# exit cli.run(ARGV)
|
|
23
|
+
#
|
|
24
|
+
# For the full dx CLI with builtins, use: require "devex"
|
|
25
|
+
|
|
26
|
+
# Load support library first (no dependencies)
|
|
27
|
+
require_relative "support/path"
|
|
28
|
+
require_relative "support/ansi"
|
|
29
|
+
require_relative "support/core_ext"
|
|
30
|
+
|
|
31
|
+
# Load version
|
|
32
|
+
require_relative "version"
|
|
33
|
+
|
|
34
|
+
# Load configuration class
|
|
35
|
+
require_relative "core/configuration"
|
|
36
|
+
|
|
37
|
+
# Load framework components in dependency order
|
|
38
|
+
require_relative "context"
|
|
39
|
+
require_relative "output"
|
|
40
|
+
require_relative "template_helpers"
|
|
41
|
+
require_relative "exec" # Must be before tool.rb (ExecutionContext includes Exec)
|
|
42
|
+
require_relative "tool"
|
|
43
|
+
require_relative "dsl"
|
|
44
|
+
require_relative "loader"
|
|
45
|
+
require_relative "cli"
|
|
46
|
+
require_relative "dirs"
|
|
47
|
+
require_relative "project_paths"
|
|
48
|
+
require_relative "working_dir"
|
|
49
|
+
|
|
50
|
+
module Devex
|
|
51
|
+
# Core module - re-exports framework classes for convenient access.
|
|
52
|
+
#
|
|
53
|
+
# All classes are also available directly under Devex:: namespace.
|
|
54
|
+
# The Core module provides a clean namespace for users who want to
|
|
55
|
+
# be explicit about using the framework vs the dx application.
|
|
56
|
+
#
|
|
57
|
+
# Note: Configuration is already defined in core/configuration.rb
|
|
58
|
+
# and is available as Devex::Core::Configuration.
|
|
59
|
+
#
|
|
60
|
+
module Core
|
|
61
|
+
# Re-export key classes at Core level for convenience
|
|
62
|
+
# (Configuration is already defined in core/configuration.rb)
|
|
63
|
+
CLI = Devex::CLI
|
|
64
|
+
Tool = Devex::Tool
|
|
65
|
+
Dirs = Devex::Dirs
|
|
66
|
+
ProjectPaths = Devex::ProjectPaths
|
|
67
|
+
WorkingDir = Devex::WorkingDir
|
|
68
|
+
Context = Devex::Context
|
|
69
|
+
Output = Devex::Output
|
|
70
|
+
Exec = Devex::Exec
|
|
71
|
+
|
|
72
|
+
# Support library
|
|
73
|
+
Path = Devex::Support::Path
|
|
74
|
+
ANSI = Devex::Support::ANSI
|
|
75
|
+
|
|
76
|
+
# Version
|
|
77
|
+
VERSION = Devex::VERSION
|
|
78
|
+
end
|
|
79
|
+
end
|