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,437 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Devex
4
+ module Support
5
+ # ANSI terminal color and style support with truecolor (24-bit) capability.
6
+ # Zero dependencies - inspired by the paint gem but self-contained.
7
+ #
8
+ # ## Modes
9
+ #
10
+ # ANSI.mode = 0xFFFFFF # Truecolor (default)
11
+ # ANSI.mode = 256 # 256-color palette
12
+ # ANSI.mode = 16 # 16 ANSI colors
13
+ # ANSI.mode = 0 # Disabled
14
+ #
15
+ # ## Basic Usage
16
+ #
17
+ # ANSI["text", :bold, :success] # Styled text
18
+ # ANSI["text", [0x5A, 0xF7, 0x8E]] # RGB foreground
19
+ # ANSI["text", "#5AF78E"] # Hex foreground
20
+ # ANSI["text", :bold, :success, bg: :error] # With background
21
+ #
22
+ # ## Nested Colors (relative to parent)
23
+ #
24
+ # When you need nested colored spans that reset back to their parent's
25
+ # color rather than to default, use the `%` method with substitutions:
26
+ #
27
+ # ANSI % ["Outer %{inner} text", :yellow, inner: ["nested", :blue]]
28
+ # # => Yellow text, with "nested" in blue, then back to yellow
29
+ #
30
+ # This is essential for building complex colorized output where inner
31
+ # spans shouldn't break the outer context.
32
+ #
33
+ # ## Direct Methods
34
+ #
35
+ # ANSI.bold("text")
36
+ # ANSI.color("text", 0x5A, 0xF7, 0x8E)
37
+ #
38
+ # ## String Refinements
39
+ #
40
+ # using Devex::Support::ANSI::StringMethods
41
+ # "text".ansi(:bold, :success)
42
+ # "text".bold
43
+ #
44
+ module ANSI
45
+ # Reset sequence - clears all formatting
46
+ RESET = "\e[0m"
47
+
48
+ # Semantic colors (truecolor RGB values)
49
+ # These are the primary colors for CLI output
50
+ COLORS = {
51
+ success: [0x5A, 0xF7, 0x8E], # Green
52
+ error: [0xFF, 0x6B, 0x6B], # Red
53
+ warning: [0xFF, 0xE6, 0x6D], # Yellow
54
+ info: [0x6B, 0xC5, 0xFF], # Blue
55
+ header: [0xC4, 0xB5, 0xFD], # Purple
56
+ muted: [0x88, 0x88, 0x88], # Gray
57
+ emphasis: [0xFF, 0xFF, 0xFF] # White
58
+ }.freeze
59
+
60
+ # Basic ANSI colors (for 16-color mode fallback)
61
+ BASIC_COLORS = {
62
+ black: 30, red: 31, green: 32, yellow: 33,
63
+ blue: 34, magenta: 35, cyan: 36, white: 37,
64
+ default: 39,
65
+ # Bright variants
66
+ bright_black: 90, bright_red: 91, bright_green: 92,
67
+ bright_yellow: 93, bright_blue: 94, bright_magenta: 95,
68
+ bright_cyan: 96, bright_white: 97
69
+ }.freeze
70
+
71
+ # Style codes
72
+ STYLES = {
73
+ bold: 1, bright: 1, # bright is alias for bold
74
+ dim: 2, faint: 2,
75
+ italic: 3,
76
+ underline: 4,
77
+ blink: 5,
78
+ reverse: 7, inverse: 7,
79
+ hidden: 8, conceal: 8,
80
+ strike: 9, crossed: 9
81
+ }.freeze
82
+
83
+ # Map semantic colors to basic ANSI for 16-color mode
84
+ SEMANTIC_TO_BASIC = {
85
+ success: :bright_green,
86
+ error: :bright_red,
87
+ warning: :bright_yellow,
88
+ info: :bright_blue,
89
+ header: :bright_magenta,
90
+ muted: :white,
91
+ emphasis: :bright_white
92
+ }.freeze
93
+
94
+ class << self
95
+ # Color mode: 0xFFFFFF (truecolor), 256, 16, or 0 (disabled)
96
+ # nil means auto-detect
97
+ def mode=(value)
98
+ @mode = value
99
+ @cache = {} # Clear cache on mode change
100
+ end
101
+
102
+ def mode
103
+ return @mode if defined?(@mode) && @mode
104
+
105
+ detect_mode
106
+ end
107
+
108
+ # Detect appropriate color mode from environment
109
+ def detect_mode
110
+ return 0 if ENV["NO_COLOR"]
111
+ return 0xFFFFFF if ENV["FORCE_COLOR"]
112
+
113
+ # Check if Context is available for more sophisticated detection
114
+ if defined?(Devex::Context) && Devex::Context.respond_to?(:color?)
115
+ return 0 unless Devex::Context.color?
116
+ else
117
+ return 0 unless $stdout.tty?
118
+ end
119
+
120
+ # Default to truecolor - modern terminals support it
121
+ 0xFFFFFF
122
+ end
123
+
124
+ # Check if colors are enabled
125
+ def enabled? = mode > 0
126
+
127
+ # ─────────────────────────────────────────────────────────────
128
+ # Main API: ANSI["text", :bold, :success]
129
+ # ─────────────────────────────────────────────────────────────
130
+
131
+ # Primary interface - apply styles and colors to text.
132
+ # Text is FIRST argument, followed by styles/colors (matches Paint API).
133
+ # Uses caching for compiled escape sequences.
134
+ #
135
+ # @example
136
+ # ANSI["hello", :bold]
137
+ # ANSI["hello", :success]
138
+ # ANSI["hello", :bold, :success]
139
+ # ANSI["hello", [0x5A, 0xF7, 0x8E]]
140
+ # ANSI["hello", "#5AF78E"]
141
+ # ANSI["hello", :bold, bg: :error]
142
+ #
143
+ def [](*args, bg: nil)
144
+ return "" if args.empty?
145
+
146
+ text = args.shift.to_s
147
+ return text unless enabled?
148
+ return text if args.empty? && bg.nil?
149
+ return text if text.empty? # Don't wrap empty strings
150
+
151
+ # Build cache key from options (include bg in key for caching, pass separately for processing)
152
+ cache_key = bg ? [args, bg].freeze : args.freeze
153
+ prefix = cached_prefix(args, bg, cache_key)
154
+
155
+ return text if prefix.empty?
156
+
157
+ "#{prefix}#{text}#{RESET}"
158
+ end
159
+
160
+ # ─────────────────────────────────────────────────────────────
161
+ # Nested Colors: ANSI % ["text %{key}", :style, key: [...]]
162
+ # ─────────────────────────────────────────────────────────────
163
+
164
+ # Apply colors with nested substitutions that reset to parent context.
165
+ #
166
+ # The key feature: when a nested span ends, it resets back to the
167
+ # parent's colors, not to default. This allows building complex
168
+ # colorized strings without breaking the outer context.
169
+ #
170
+ # @param paint_args [Array] [text, *styles, substitutions_hash]
171
+ # @param clear_color [String] ANSI sequence to reset to (internal use)
172
+ #
173
+ # @example Simple nested color
174
+ # ANSI % ["Hello %{name}!", :yellow, name: ["World", :blue]]
175
+ # # => Yellow "Hello ", blue "World", yellow "!"
176
+ #
177
+ # @example Multiple substitutions
178
+ # ANSI % ["%{status}: %{msg}", :muted,
179
+ # status: ["OK", :success],
180
+ # msg: ["All tests passed", :emphasis]]
181
+ #
182
+ # @example Deeply nested
183
+ # ANSI % ["Outer %{mid} end", :yellow,
184
+ # mid: ["middle %{inner} more", :blue,
185
+ # inner: ["deep", :red]]]
186
+ #
187
+ def %(paint_args, clear_color = RESET)
188
+ args = paint_args.dup
189
+ text = args.shift.to_s
190
+
191
+ # Extract substitution hash if present
192
+ substitutions = args.last.is_a?(Hash) ? args.pop : nil
193
+
194
+ # Get the color sequence for this level
195
+ cache_key = args.freeze
196
+ current_color = cached_prefix(args, nil, cache_key)
197
+
198
+ # Process substitutions recursively
199
+ if substitutions
200
+ substitutions.each do |key, value|
201
+ placeholder = "%{#{key}}"
202
+ replacement = if value.is_a?(Array)
203
+ # Recursive call - nested span resets to current_color, not RESET
204
+ self.%(value, clear_color + current_color)
205
+ else
206
+ value.to_s
207
+ end
208
+ text = text.gsub(placeholder, replacement)
209
+ end
210
+ end
211
+
212
+ return text unless enabled?
213
+
214
+ if current_color.empty?
215
+ text
216
+ else
217
+ "#{current_color}#{text}#{clear_color}"
218
+ end
219
+ end
220
+
221
+ # ─────────────────────────────────────────────────────────────
222
+ # Direct Color Methods
223
+ # ─────────────────────────────────────────────────────────────
224
+
225
+ # Truecolor (24-bit) foreground
226
+ def color(text, r, g, b) = self[text, [r, g, b]]
227
+
228
+ # Truecolor (24-bit) background
229
+ def background(text, r, g, b) = self[text, bg: [r, g, b]]
230
+
231
+ # Hex color foreground: ANSI.hex("text", "#5AF78E")
232
+ def hex(text, hex_color) = self[text, hex_color]
233
+
234
+ # Named semantic color
235
+ def named(text, name) = self[text, name]
236
+
237
+ # ─────────────────────────────────────────────────────────────
238
+ # Style Methods
239
+ # ─────────────────────────────────────────────────────────────
240
+
241
+ def bold(text) = self[text, :bold]
242
+ def dim(text) = self[text, :dim]
243
+ def italic(text) = self[text, :italic]
244
+ def underline(text) = self[text, :underline]
245
+ def blink(text) = self[text, :blink]
246
+ def reverse(text) = self[text, :reverse]
247
+ def hidden(text) = self[text, :hidden]
248
+ def strike(text) = self[text, :strike]
249
+
250
+ # ─────────────────────────────────────────────────────────────
251
+ # Utility Methods
252
+ # ─────────────────────────────────────────────────────────────
253
+
254
+ # Strip ANSI codes from text
255
+ def strip(text) = text.to_s.gsub(/\e\[[0-9;]*m/, "")
256
+
257
+ # Calculate visible length (without ANSI codes)
258
+ def visible_length(text) = strip(text).length
259
+
260
+ # Raw escape sequence without text wrapping.
261
+ # Useful for building custom sequences or streaming output.
262
+ def esc(*args) = cached_prefix(args, nil, args.freeze)
263
+
264
+ # Reset sequence
265
+ def reset = RESET
266
+
267
+ # Clear the escape sequence cache
268
+ def clear_cache! = @cache = {}
269
+
270
+ private
271
+
272
+ # Get or compute cached escape sequence prefix
273
+ # @param fg_args [Array] foreground styles/colors
274
+ # @param bg [Symbol, Array, String, nil] background color
275
+ # @param cache_key [Object] key for caching (includes both fg and bg)
276
+ def cached_prefix(fg_args, bg, cache_key)
277
+ @cache ||= {}
278
+
279
+ @cache[cache_key] ||= begin
280
+ codes = []
281
+
282
+ # Process foreground styles and colors
283
+ fg_args.each do |arg|
284
+ code = resolve_code(arg, foreground: true)
285
+ codes << code if code
286
+ end
287
+
288
+ # Process background
289
+ if bg
290
+ code = resolve_code(bg, foreground: false)
291
+ codes << code if code
292
+ end
293
+
294
+ codes.empty? ? "" : "\e[#{codes.join(';')}m"
295
+ end
296
+ end
297
+
298
+ # Resolve an argument to ANSI code(s)
299
+ def resolve_code(arg, foreground:)
300
+ case arg
301
+ when Symbol then resolve_symbol(arg, foreground)
302
+ when Array
303
+ # RGB array
304
+ rgb_code(*arg, foreground: foreground)
305
+ when String
306
+ # Hex string like "#5AF78E" or "5AF78E"
307
+ resolve_hex(arg, foreground)
308
+ when Integer
309
+ # Direct ANSI code
310
+ arg
311
+ end
312
+ end
313
+
314
+ def resolve_symbol(sym, foreground)
315
+ # Check styles first
316
+ if STYLES.key?(sym)
317
+ STYLES[sym]
318
+ # Then semantic colors
319
+ elsif COLORS.key?(sym)
320
+ rgb = COLORS[sym]
321
+ rgb_code(*rgb, foreground: foreground)
322
+ # Then basic ANSI colors
323
+ elsif BASIC_COLORS.key?(sym)
324
+ code = BASIC_COLORS[sym]
325
+ foreground ? code : code + 10
326
+ end
327
+ end
328
+
329
+ def resolve_hex(hex_str, foreground)
330
+ hex_str = hex_str.delete_prefix("#")
331
+
332
+ # Expand 3-char hex: "FFF" -> "FFFFFF"
333
+ hex_str = hex_str.chars.map { |c| c * 2 }.join if hex_str.length == 3
334
+
335
+ return nil unless hex_str.length == 6
336
+
337
+ r = hex_str[0, 2].to_i(16)
338
+ g = hex_str[2, 2].to_i(16)
339
+ b = hex_str[4, 2].to_i(16)
340
+
341
+ rgb_code(r, g, b, foreground: foreground)
342
+ end
343
+
344
+ def rgb_code(r, g, b, foreground:)
345
+ case mode
346
+ when 0xFFFFFF, (257..)
347
+ # Truecolor
348
+ foreground ? "38;2;#{r};#{g};#{b}" : "48;2;#{r};#{g};#{b}"
349
+ when 256
350
+ # 256-color: convert RGB to nearest color cube index
351
+ index = rgb_to_256(r, g, b)
352
+ foreground ? "38;5;#{index}" : "48;5;#{index}"
353
+ when 16
354
+ # 16-color: find nearest basic color
355
+ basic = rgb_to_basic(r, g, b)
356
+ foreground ? basic : basic + 10
357
+ end
358
+ end
359
+
360
+ # Convert RGB to 256-color palette index
361
+ def rgb_to_256(r, g, b)
362
+ # Check if it's a grayscale
363
+ if r == g && g == b
364
+ return 16 if r < 8
365
+ return 231 if r > 248
366
+
367
+ return ((r - 8) / 10.0).round + 232
368
+ end
369
+
370
+ # Color cube: 6x6x6 starting at index 16
371
+ 16 + (36 * (r / 51.0).round) + (6 * (g / 51.0).round) + (b / 51.0).round
372
+ end
373
+
374
+ # Convert RGB to nearest basic ANSI color code
375
+ def rgb_to_basic(r, g, b)
376
+ # Simple brightness-based mapping
377
+ bright = (r + g + b) > 382
378
+ base = 30
379
+
380
+ # Determine primary color
381
+ base += if r > g && r > b
382
+ 1 # red
383
+ elsif g > r && g > b
384
+ 2 # green
385
+ elsif b > r && b > g
386
+ 4 # blue
387
+ elsif r > b
388
+ 3 # yellow (r+g)
389
+ elsif g > r
390
+ 6 # cyan (g+b)
391
+ elsif r > g
392
+ 5 # magenta (r+b)
393
+ else
394
+ 7 # white/gray
395
+ end
396
+
397
+ bright ? base + 60 : base
398
+ end
399
+ end
400
+
401
+ # String refinements for ANSI colors
402
+ module StringMethods
403
+ refine String do
404
+ # Primary interface: "text".ansi(:bold, :success)
405
+ def ansi(*styles, bg: nil) = ANSI[self, *styles, bg: bg]
406
+
407
+ # RGB colors
408
+ def color(r, g, b) = ANSI.color(self, r, g, b)
409
+
410
+ def background(r, g, b) = ANSI.background(self, r, g, b)
411
+
412
+ # Hex color: "text".hex("#5AF78E")
413
+ def hex(hex_color) = ANSI.hex(self, hex_color)
414
+
415
+ # Named semantic color: "text".named(:success)
416
+ def named(name) = ANSI.named(self, name)
417
+
418
+ # Style shortcuts
419
+ def bold = ANSI.bold(self)
420
+ def dim = ANSI.dim(self)
421
+ def italic = ANSI.italic(self)
422
+ def underline = ANSI.underline(self)
423
+ def blink = ANSI.blink(self)
424
+ def reverse = ANSI.reverse(self)
425
+ def hidden = ANSI.hidden(self)
426
+ def strike = ANSI.strike(self)
427
+
428
+ # Strip ANSI codes
429
+ def strip_ansi = ANSI.strip(self)
430
+
431
+ # Visible length without ANSI codes
432
+ def visible_length = ANSI.visible_length(self)
433
+ end
434
+ end
435
+ end
436
+ end
437
+ end