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.
Files changed (72) hide show
  1. checksums.yaml +7 -0
  2. data/.obsidian/app.json +6 -0
  3. data/.obsidian/appearance.json +4 -0
  4. data/.obsidian/community-plugins.json +5 -0
  5. data/.obsidian/core-plugins.json +33 -0
  6. data/.obsidian/plugins/obsidian-minimal-settings/data.json +34 -0
  7. data/.obsidian/plugins/obsidian-minimal-settings/main.js +8 -0
  8. data/.obsidian/plugins/obsidian-minimal-settings/manifest.json +11 -0
  9. data/.obsidian/plugins/obsidian-style-settings/data.json +15 -0
  10. data/.obsidian/plugins/obsidian-style-settings/main.js +165 -0
  11. data/.obsidian/plugins/obsidian-style-settings/manifest.json +10 -0
  12. data/.obsidian/plugins/obsidian-style-settings/styles.css +243 -0
  13. data/.obsidian/plugins/table-editor-obsidian/data.json +6 -0
  14. data/.obsidian/plugins/table-editor-obsidian/main.js +236 -0
  15. data/.obsidian/plugins/table-editor-obsidian/manifest.json +17 -0
  16. data/.obsidian/plugins/table-editor-obsidian/styles.css +78 -0
  17. data/.obsidian/themes/AnuPpuccin/manifest.json +7 -0
  18. data/.obsidian/themes/AnuPpuccin/theme.css +9080 -0
  19. data/.obsidian/themes/Minimal/manifest.json +8 -0
  20. data/.obsidian/themes/Minimal/theme.css +2251 -0
  21. data/.rubocop.yml +231 -0
  22. data/CHANGELOG.md +97 -0
  23. data/LICENSE +21 -0
  24. data/README.md +314 -0
  25. data/Rakefile +13 -0
  26. data/devex-logo.jpg +0 -0
  27. data/docs/developing-tools.md +1000 -0
  28. data/docs/ref/agent-mode.md +46 -0
  29. data/docs/ref/cli-interface.md +60 -0
  30. data/docs/ref/configuration.md +46 -0
  31. data/docs/ref/design-philosophy.md +17 -0
  32. data/docs/ref/error-handling.md +38 -0
  33. data/docs/ref/io-handling.md +88 -0
  34. data/docs/ref/signals.md +141 -0
  35. data/docs/ref/temporal-software-theory.md +790 -0
  36. data/exe/dx +52 -0
  37. data/lib/devex/builtins/.index.rb +10 -0
  38. data/lib/devex/builtins/debug.rb +43 -0
  39. data/lib/devex/builtins/format.rb +44 -0
  40. data/lib/devex/builtins/gem.rb +77 -0
  41. data/lib/devex/builtins/lint.rb +61 -0
  42. data/lib/devex/builtins/test.rb +76 -0
  43. data/lib/devex/builtins/version.rb +156 -0
  44. data/lib/devex/cli.rb +340 -0
  45. data/lib/devex/context.rb +433 -0
  46. data/lib/devex/core/configuration.rb +136 -0
  47. data/lib/devex/core.rb +79 -0
  48. data/lib/devex/dirs.rb +210 -0
  49. data/lib/devex/dsl.rb +100 -0
  50. data/lib/devex/exec/controller.rb +245 -0
  51. data/lib/devex/exec/result.rb +229 -0
  52. data/lib/devex/exec.rb +662 -0
  53. data/lib/devex/loader.rb +136 -0
  54. data/lib/devex/output.rb +257 -0
  55. data/lib/devex/project_paths.rb +309 -0
  56. data/lib/devex/support/ansi.rb +437 -0
  57. data/lib/devex/support/core_ext.rb +560 -0
  58. data/lib/devex/support/global.rb +68 -0
  59. data/lib/devex/support/path.rb +357 -0
  60. data/lib/devex/support.rb +71 -0
  61. data/lib/devex/template_helpers.rb +136 -0
  62. data/lib/devex/templates/debug.erb +24 -0
  63. data/lib/devex/tool.rb +374 -0
  64. data/lib/devex/version.rb +5 -0
  65. data/lib/devex/working_dir.rb +99 -0
  66. data/lib/devex.rb +158 -0
  67. data/ruby-project-template/.gitignore +0 -0
  68. data/ruby-project-template/Gemfile +0 -0
  69. data/ruby-project-template/README.md +0 -0
  70. data/ruby-project-template/docs/README.md +0 -0
  71. data/sig/devex.rbs +4 -0
  72. metadata +122 -0
@@ -0,0 +1,357 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+ require "fileutils"
5
+ require "tmpdir"
6
+
7
+ module Devex
8
+ module Support
9
+ # Enhanced Pathname with ergonomic shortcuts for CLI development.
10
+ #
11
+ # Usage:
12
+ # path = Path["~/src/project"]
13
+ # path = "~/src/project".to_p # With String refinement
14
+ # path = Path.pwd
15
+ #
16
+ # # Joining with / operator
17
+ # path / "lib" / "foo.rb"
18
+ #
19
+ # # Permission checks
20
+ # path.r? # readable?
21
+ # path.w? # writable?
22
+ # path.rw? # readable and writable?
23
+ #
24
+ # # Existence
25
+ # path.exist?
26
+ # path.missing?
27
+ # path.existence # Returns self or nil
28
+ #
29
+ # # Relative paths
30
+ # path.rel # Relative to pwd with ~/
31
+ # path.rel(from: project_root) # Relative to specific dir
32
+ # path.short # Shortest representation
33
+ #
34
+ # # Globbing
35
+ # path["**/*.rb"]
36
+ #
37
+ # # Safe directory creation
38
+ # path.dir! # Creates parent dirs, returns self
39
+ #
40
+ # # File I/O
41
+ # path.read
42
+ # path.write(content)
43
+ # path.append(content)
44
+ # path.atomic_write(content)
45
+ #
46
+ class Path < Pathname
47
+ class << self
48
+ # Construct from string: Path["~/src/project"]
49
+ def [](path) = new(expand_user(path.to_s))
50
+
51
+ # Current working directory
52
+ def pwd = new(Dir.pwd)
53
+ alias cwd pwd
54
+ alias getwd pwd
55
+
56
+ # Home directory
57
+ def home = new(Dir.home)
58
+
59
+ # Temporary directory
60
+ def tmp = new(Dir.tmpdir)
61
+ alias tmpdir tmp
62
+
63
+ private
64
+
65
+ # Expand ~ and ~user in path strings
66
+ def expand_user(path)
67
+ return path unless path.start_with?("~")
68
+
69
+ File.expand_path(path)
70
+ end
71
+ end
72
+
73
+ # ─────────────────────────────────────────────────────────────
74
+ # Path Joining
75
+ # ─────────────────────────────────────────────────────────────
76
+
77
+ # Division operator for path joining: path / "subdir" / "file.rb"
78
+ def /(other) = self.class.new(join(other.to_s))
79
+
80
+ # Override join to return Path
81
+ def join(*args)
82
+ return self if args.empty?
83
+
84
+ self.class.new(super(*args.map(&:to_s)))
85
+ end
86
+
87
+ # ─────────────────────────────────────────────────────────────
88
+ # Permission Checks
89
+ # ─────────────────────────────────────────────────────────────
90
+
91
+ def r? = readable_real?
92
+ def w? = writable_real?
93
+ def x? = executable_real?
94
+ def rw? = r? && w?
95
+ def rx? = r? && x?
96
+ def wx? = w? && x?
97
+ def rwx? = r? && w? && x?
98
+
99
+ # ─────────────────────────────────────────────────────────────
100
+ # Type Checks
101
+ # ─────────────────────────────────────────────────────────────
102
+
103
+ def dir? = directory?
104
+ def missing? = !exist?
105
+ alias exists? exist?
106
+
107
+ # ActiveSupport-style: returns self if exists, nil otherwise
108
+ def existence = exist? ? self : nil
109
+
110
+ # ─────────────────────────────────────────────────────────────
111
+ # Memoized Expansions
112
+ # ─────────────────────────────────────────────────────────────
113
+
114
+ # Expanded path (memoized)
115
+ def exp = @exp ||= self.class.new(expand_path)
116
+
117
+ # Real path with fallback to expanded (memoized, safe)
118
+ def real
119
+ @real ||= begin
120
+ self.class.new(realpath)
121
+ rescue Errno::ENOENT
122
+ exp
123
+ end
124
+ end
125
+
126
+ # Clear memoization (if path might have changed)
127
+ def reload!
128
+ @exp = @real = @rc = nil
129
+ self
130
+ end
131
+
132
+ # ─────────────────────────────────────────────────────────────
133
+ # Relative Paths
134
+ # ─────────────────────────────────────────────────────────────
135
+
136
+ # Relative path with ~ substitution for home directory
137
+ # @param from [Path, String] Base directory (default: pwd)
138
+ # @param home [Boolean] Substitute ~ for home directory
139
+ def rel(from: nil, home: true)
140
+ from = self.class.new(from&.to_s || Dir.pwd)
141
+ result = exp.relative_path_from(from.exp)
142
+ result = self.class.new(result)
143
+
144
+ result = self.class.new(result.to_s.sub(Dir.home, "~")) if home && Dir.home && result.to_s.start_with?(Dir.home)
145
+ result
146
+ rescue ArgumentError
147
+ # Can't compute relative path (different drives on Windows, etc.)
148
+ if home && Dir.home
149
+ self.class.new(to_s.sub(Dir.home, "~"))
150
+ else
151
+ self
152
+ end
153
+ end
154
+
155
+ # Shortest representation of path
156
+ def short(from: nil)
157
+ from = self.class.new(from&.to_s || Dir.pwd)
158
+ candidates = []
159
+
160
+ # Try relative from base
161
+ begin
162
+ candidates << exp.relative_path_from(from.exp)
163
+ rescue ArgumentError
164
+ # Ignore - can't compute relative
165
+ end
166
+
167
+ # Try with ~ substitution
168
+ if Dir.home
169
+ home_sub = to_s.sub(Dir.home, "~")
170
+ candidates << self.class.new(home_sub) if home_sub != to_s
171
+ end
172
+
173
+ # Original as fallback
174
+ candidates << self
175
+
176
+ candidates.compact.min_by { |c| c.to_s.length }
177
+ end
178
+
179
+ # Cache for relative path calculations (expensive operation)
180
+ def relative_path_from(base)
181
+ @rc ||= {}
182
+ @rc[base.to_s] ||= self.class.new(super)
183
+ end
184
+
185
+ # ─────────────────────────────────────────────────────────────
186
+ # Globbing
187
+ # ─────────────────────────────────────────────────────────────
188
+
189
+ # Glob from this directory: path["**/*.rb"]
190
+ def [](pattern, include_dotfiles: true)
191
+ base = directory? ? self : dirname
192
+ flags = include_dotfiles ? File::FNM_DOTMATCH : 0
193
+ Pathname.glob((base / pattern).to_s, flags).map { |p| self.class.new(p) }
194
+ end
195
+
196
+ # Alias for more explicit calls
197
+ def glob(pattern, **) = self[pattern, **]
198
+
199
+ # ─────────────────────────────────────────────────────────────
200
+ # Directory Operations
201
+ # ─────────────────────────────────────────────────────────────
202
+
203
+ # Get directory (dirname, but returns self if already a directory)
204
+ def dir = directory? ? exp : self.class.new(dirname)
205
+
206
+ # Create directory (and parents) if missing, return self
207
+ # Safe - returns nil on error instead of raising
208
+ def dir!
209
+ target = directory? ? self : dir
210
+ target.mkpath unless target.exist?
211
+ self
212
+ rescue SystemCallError
213
+ nil
214
+ end
215
+
216
+ # Create this directory (and parents) if missing
217
+ def mkdir!
218
+ mkpath unless exist?
219
+ self
220
+ rescue SystemCallError
221
+ nil
222
+ end
223
+
224
+ # ─────────────────────────────────────────────────────────────
225
+ # File I/O
226
+ # ─────────────────────────────────────────────────────────────
227
+
228
+ # Read entire file contents
229
+ def read(encoding: nil)
230
+ opts = encoding ? { encoding: encoding } : {}
231
+ File.read(to_s, **opts)
232
+ end
233
+ alias contents read
234
+
235
+ # Read as lines
236
+ def lines(chomp: true) = File.readlines(to_s, chomp: chomp)
237
+
238
+ # Write contents (creates parent directories)
239
+ def write(content, encoding: nil)
240
+ dir!
241
+ opts = encoding ? { encoding: encoding } : {}
242
+ File.write(to_s, content, **opts)
243
+ self
244
+ end
245
+
246
+ # Append to file
247
+ def append(content)
248
+ dir!
249
+ File.write(to_s, content, mode: "a")
250
+ self
251
+ end
252
+
253
+ # Atomic write (write to temp, then rename)
254
+ def atomic_write(content)
255
+ dir!
256
+ require "tempfile"
257
+ Tempfile.create([basename.to_s, extname], dirname.to_s) do |temp|
258
+ temp.binmode
259
+ temp.write(content)
260
+ temp.close
261
+ FileUtils.mv(temp.path, to_s)
262
+ end
263
+ self
264
+ end
265
+
266
+ # Delete file (no-op if doesn't exist)
267
+ def rm
268
+ File.delete(to_s) if file?
269
+ self
270
+ end
271
+ alias delete rm
272
+ alias unlink rm
273
+
274
+ # Delete directory recursively (no-op if doesn't exist)
275
+ def rm_rf
276
+ FileUtils.rm_rf(to_s) if exist?
277
+ self
278
+ end
279
+
280
+ # ─────────────────────────────────────────────────────────────
281
+ # Modification Time Comparisons
282
+ # ─────────────────────────────────────────────────────────────
283
+
284
+ def newer_than?(other)
285
+ other = self.class[other] unless other.is_a?(Pathname)
286
+ return true unless other.exist?
287
+ return false unless exist?
288
+
289
+ mtime > other.mtime
290
+ end
291
+
292
+ def older_than?(other)
293
+ other = self.class[other] unless other.is_a?(Pathname)
294
+ return false unless other.exist?
295
+ return true unless exist?
296
+
297
+ mtime < other.mtime
298
+ end
299
+
300
+ # ─────────────────────────────────────────────────────────────
301
+ # Extension Manipulation
302
+ # ─────────────────────────────────────────────────────────────
303
+
304
+ # Replace extension: path.with_ext(".md")
305
+ def with_ext(new_ext)
306
+ new_ext = ".#{new_ext}" unless new_ext.to_s.start_with?(".")
307
+ self.class.new(sub_ext(new_ext.to_s))
308
+ end
309
+
310
+ # Remove extension
311
+ def without_ext = self.class.new(to_s.sub(/#{Regexp.escape(extname)}$/, ""))
312
+
313
+ # ─────────────────────────────────────────────────────────────
314
+ # Siblings and Relatives
315
+ # ─────────────────────────────────────────────────────────────
316
+
317
+ # Sibling with same directory but different name
318
+ def sibling(name) = dir / name.to_s
319
+
320
+ # Override parent to return Path
321
+ def parent = self.class.new(super)
322
+
323
+ # Override dirname to return Path
324
+ def dirname = self.class.new(super)
325
+
326
+ # Override basename to return Path
327
+ def basename(*args) = self.class.new(super)
328
+
329
+ # ─────────────────────────────────────────────────────────────
330
+ # Inspection
331
+ # ─────────────────────────────────────────────────────────────
332
+
333
+ def inspect = "#<Path:#{self}>"
334
+
335
+ # ─────────────────────────────────────────────────────────────
336
+ # String-like Behavior
337
+ # ─────────────────────────────────────────────────────────────
338
+
339
+ # Delegate string methods to to_s
340
+ def method_missing(method, *, &)
341
+ if to_s.respond_to?(method)
342
+ result = to_s.send(method, *, &)
343
+ # Return Path if result looks like a path
344
+ if result.is_a?(String) && result.include?("/")
345
+ self.class.new(result)
346
+ else
347
+ result
348
+ end
349
+ else
350
+ super
351
+ end
352
+ end
353
+
354
+ def respond_to_missing?(method, include_private = false) = to_s.respond_to?(method, include_private) || super
355
+ end
356
+ end
357
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Devex Support Library
4
+ #
5
+ # A zero-dependency collection of Ruby utilities optimized for CLI development.
6
+ # Provides Path manipulation, ANSI colors, and core extensions as refinements.
7
+ #
8
+ # ## Quick Start
9
+ #
10
+ # require "devex/support"
11
+ #
12
+ # # Use refinements (scoped to current file)
13
+ # using Devex::Support::CoreExt
14
+ # using Devex::Support::ANSI::StringMethods
15
+ #
16
+ # # Or for CLI tools, load globally:
17
+ # require "devex/support/global"
18
+ #
19
+ # ## Components
20
+ #
21
+ # ### Path - Enhanced Pathname for CLI tools
22
+ #
23
+ # path = Path["~/src/project"]
24
+ # path = Path.pwd / "lib" / "foo.rb"
25
+ #
26
+ # path.r? # readable?
27
+ # path.w? # writable?
28
+ # path.exist? # exists?
29
+ # path.dir! # ensure parent dirs exist
30
+ # path.rel # relative path with ~ for home
31
+ # path.short # shortest representation
32
+ #
33
+ # ### ANSI - Terminal colors (truecolor, zero deps)
34
+ #
35
+ # ANSI["text", :bold, :success]
36
+ # ANSI["text", "#5AF78E"]
37
+ # ANSI % ["Outer %{inner}", :yellow, inner: ["nested", :blue]]
38
+ #
39
+ # ### CoreExt - Refinements for common operations
40
+ #
41
+ # "".blank? # => true
42
+ # nil.present? # => false
43
+ # [1,2,3].average # => 2.0
44
+ # {a: 1}.deep_merge(b: 2) # => {a: 1, b: 2}
45
+ # "hello world".truncate(8) # => "hello..."
46
+ #
47
+
48
+ module Devex
49
+ module Support
50
+ # Autoload support modules
51
+ autoload :Path, "devex/support/path"
52
+ autoload :ANSI, "devex/support/ansi"
53
+ autoload :CoreExt, "devex/support/core_ext"
54
+
55
+ # Convenience: expose Path at module level
56
+ # Allows: Devex::Support::Path["~/foo"]
57
+ # Or after `include Devex::Support`: Path["~/foo"]
58
+
59
+ class << self
60
+ # Version of the support library
61
+ def version = "0.1.0"
62
+ end
63
+ end
64
+ end
65
+
66
+ # Also expose Path at Devex level for convenience
67
+ # Allows: Devex::Path["~/foo"]
68
+ module Devex
69
+ Path = Support::Path
70
+ ANSI = Support::ANSI
71
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "support/ansi"
4
+
5
+ module Devex
6
+ # Helper methods available in ERB templates.
7
+ # Automatically respects Context.color? for all styling.
8
+ #
9
+ # Usage in templates:
10
+ # <%= c :green, "success" %>
11
+ # <%= c :bold, :white, "header" %>
12
+ # <%= sym :success %> All tests passed
13
+ # <%= hr %>
14
+ # <%= heading "Section" %>
15
+ #
16
+ module TemplateHelpers
17
+ # Color definitions (truecolor RGB) - matches Output::COLORS
18
+ COLORS = {
19
+ success: [0x5A, 0xF7, 0x8E],
20
+ error: [0xFF, 0x6B, 0x6B],
21
+ warning: [0xFF, 0xE6, 0x6D],
22
+ info: [0x6B, 0xC5, 0xFF],
23
+ header: [0xC4, 0xB5, 0xFD],
24
+ muted: [0x88, 0x88, 0x88],
25
+ emphasis: [0xFF, 0xFF, 0xFF]
26
+ }.freeze
27
+
28
+ # Symbols - basic unicode that works everywhere
29
+ # (Not nerdfont glyphs or emoji that render as images)
30
+ SYMBOLS = {
31
+ success: "✓",
32
+ error: "✗",
33
+ warning: "⚠",
34
+ info: "ℹ",
35
+ arrow: "→",
36
+ bullet: "•",
37
+ check: "✓",
38
+ cross: "✗",
39
+ dot: "·"
40
+ }.freeze
41
+
42
+ module_function
43
+
44
+ # Colorize text - the main helper
45
+ # Last argument is the text, preceding arguments are colors/styles
46
+ #
47
+ # Examples:
48
+ # c(:green, "text")
49
+ # c(:bold, :white, "text")
50
+ # c(:success, "text") # uses named color
51
+ # c([0x5A, 0xF7, 0x8E], "text") # RGB array
52
+ #
53
+ def c(*args)
54
+ text = args.pop.to_s
55
+ return text unless Context.color?
56
+ return text if args.empty?
57
+
58
+ # Expand named colors to RGB
59
+ colors = args.map do |color|
60
+ if color.is_a?(Symbol) && COLORS.key?(color)
61
+ COLORS[color]
62
+ else
63
+ color
64
+ end
65
+ end
66
+
67
+ Support::ANSI[text, *colors]
68
+ end
69
+
70
+ # Get symbol - always unicode (basic unicode works everywhere)
71
+ def sym(name) = SYMBOLS.fetch(name, name.to_s)
72
+
73
+ # Colored symbol - combines sym() and c()
74
+ def csym(name, color = nil)
75
+ color ||= name # Default: use symbol name as color name
76
+ s = sym(name)
77
+ c(color, s)
78
+ end
79
+
80
+ # Horizontal rule
81
+ def hr(char: "─", width: 40)
82
+ line = char * width
83
+ Context.color? ? c(:muted, line) : line
84
+ end
85
+
86
+ # Styled heading
87
+ def heading(text, char: "=", width: nil)
88
+ width ||= text.length
89
+ line = char * width
90
+ if Context.color?
91
+ "#{c(:header, text)}\n#{c(:muted, line)}"
92
+ else
93
+ "#{text}\n#{line}"
94
+ end
95
+ end
96
+
97
+ # Muted/secondary text
98
+ def muted(text) = c(:muted, text)
99
+
100
+ # Bold text
101
+ def bold(text) = c(:bold, text)
102
+
103
+ # Create a binding with all helpers and locals available
104
+ def template_binding(locals = {}) = TemplateContext.new(locals).get_binding
105
+ end
106
+
107
+ # Context object for template rendering
108
+ # Includes all helper methods directly so templates can use clean syntax:
109
+ # <%= c :green, "text" %>
110
+ # <%= heading "Title" %>
111
+ #
112
+ class TemplateContext
113
+ include TemplateHelpers
114
+
115
+ def initialize(locals = {})
116
+ @locals = locals
117
+ # Define accessor methods for each local variable
118
+ locals.each do |name, value|
119
+ define_singleton_method(name) { value }
120
+ end
121
+ end
122
+
123
+ def get_binding = binding
124
+
125
+ # Allow accessing locals as methods or via method_missing
126
+ def method_missing(name, *args, &)
127
+ if @locals.key?(name)
128
+ @locals[name]
129
+ else
130
+ super
131
+ end
132
+ end
133
+
134
+ def respond_to_missing?(name, include_private = false) = @locals.key?(name) || super
135
+ end
136
+ end
@@ -0,0 +1,24 @@
1
+ <%= heading "Context Detection" %>
2
+
3
+ <%= c :header, "TTY Detection:" %>
4
+ stdout.tty? = <%= c(tty[:stdout] ? :success : :muted, tty[:stdout]) %>
5
+ stderr.tty? = <%= c(tty[:stderr] ? :success : :muted, tty[:stderr]) %>
6
+ stdin.tty? = <%= c(tty[:stdin] ? :success : :muted, tty[:stdin]) %>
7
+ terminal? = <%= c(tty[:terminal] ? :success : :muted, tty[:terminal]) %>
8
+
9
+ <%= c :header, "Streams:" %>
10
+ streams_merged? = <%= c(streams[:merged] ? :warning : :muted, streams[:merged]) %>
11
+ piped? = <%= c(streams[:piped] ? :info : :muted, streams[:piped]) %>
12
+
13
+ <%= c :header, "Environment:" %>
14
+ ci? = <%= c(environment[:ci] ? :info : :muted, environment[:ci]) %>
15
+ env = <%= c :emphasis, environment[:env] %>
16
+ agent_mode_env? = <%= c(environment[:agent_mode_env] ? :warning : :muted, environment[:agent_mode_env]) %>
17
+
18
+ <%= c :header, "Detection:" %>
19
+ agent_mode? = <%= c(detection[:agent_mode] ? :warning : :success, detection[:agent_mode]) %>
20
+ interactive? = <%= c(detection[:interactive] ? :success : :muted, detection[:interactive]) %>
21
+ color? = <%= c(detection[:color] ? :success : :muted, detection[:color]) %>
22
+
23
+ <%= muted "Call Tree: #{call_tree.inspect}" %>
24
+ <%= muted "Overrides: #{overrides.inspect}" %>