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,560 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ module Devex
6
+ module Support
7
+ # Core extensions as refinements.
8
+ #
9
+ # Usage:
10
+ # using Devex::Support::CoreExt
11
+ #
12
+ # Or load globally (for CLI tools):
13
+ # require "devex/support/global"
14
+ #
15
+ module CoreExt
16
+ # ─────────────────────────────────────────────────────────────
17
+ # Implementation Modules
18
+ # These define the actual methods and can be included in both
19
+ # refinements and monkey-patched classes.
20
+ # ─────────────────────────────────────────────────────────────
21
+
22
+ module ObjectMethods
23
+ def blank? = respond_to?(:empty?) ? empty? : !self
24
+
25
+ def present? = !blank?
26
+
27
+ def presence() self if present? end
28
+
29
+ def numeric?
30
+ true if Float(self)
31
+ rescue StandardError
32
+ false
33
+ end
34
+
35
+ def in?(collection) = collection.include?(self)
36
+ end
37
+
38
+ module NilMethods
39
+ def blank? = true
40
+ def present? = false
41
+ def presence = nil
42
+ end
43
+
44
+ module FalseMethods
45
+ def blank? = true
46
+ def present? = false
47
+ def presence = nil
48
+ end
49
+
50
+ module TrueMethods
51
+ def blank? = false
52
+ def present? = true
53
+ def presence = self
54
+ end
55
+
56
+ module NumericMethods
57
+ def blank? = false
58
+ def present? = true
59
+ def presence = self
60
+ def numeric? = true
61
+ end
62
+
63
+ module ArrayBlankMethods
64
+ def blank? = empty?
65
+ def present? = !blank?
66
+
67
+ def presence() self if present? end
68
+ end
69
+
70
+ module HashBlankMethods
71
+ def blank? = empty?
72
+ def present? = !blank?
73
+
74
+ def presence() self if present? end
75
+ end
76
+
77
+ module StringMethods
78
+ def blank? = empty? || !match?(/[^[:space:]]/)
79
+
80
+ # Override present? to use String's blank? (import_methods copies bytecode,
81
+ # so ObjectMethods#present? would call Object's blank?)
82
+ def present? = !blank?
83
+
84
+ def presence() self if present? end
85
+
86
+ def to_p = Devex::Support::Path.new(self)
87
+
88
+ def wrap(indent = :first, width = 90)
89
+ ind = case indent
90
+ when :first then self[/^[[:space:]]*/] || ""
91
+ when ::String then indent
92
+ when ::Integer then " " * indent.abs
93
+ else ""
94
+ end
95
+
96
+ ind_size = (ind.count("\t") * 8) + ind.length - ind.count("\t")
97
+ effective_width = [width - ind_size, 1].max
98
+
99
+ paragraphs = strip.split(/\n[ \t]*\n/m)
100
+ paragraphs.map do |p|
101
+ p.gsub(/[[:space:]]+/, " ")
102
+ .strip
103
+ .scan(/.{1,#{effective_width}}(?: |$)/)
104
+ .map { |row| ind + row.strip }
105
+ .join("\n")
106
+ end.join("\n\n")
107
+ end
108
+
109
+ def sentences = gsub(/\s+/, " ").scan(/[^.!?]+[.!?]+(?:\s+|$)|[^.!?]+$/).map(&:strip).reject(&:empty?)
110
+
111
+ def to_sh
112
+ return "''" if empty?
113
+
114
+ gsub(%r{([^A-Za-z0-9_\-.,:/@\n])}, '\\\\\\\\\\1').gsub("\n", "'\n'")
115
+ end
116
+
117
+ def squish = gsub(/[[:space:]]+/, " ").strip
118
+
119
+ def fnv32 = bytes.reduce(0x811c9dc5) { |h, b| ((h ^ b) * 0x01000193) % (1 << 32) }
120
+
121
+ def fnv64 = bytes.reduce(0xcbf29ce484222325) { |h, b| ((h ^ b) * 0x100000001b3) % (1 << 64) }
122
+
123
+ def base64url
124
+ require "base64"
125
+ Base64.urlsafe_encode64(self, padding: false)
126
+ end
127
+
128
+ def truncate(length, omission: "...")
129
+ return self if self.length <= length
130
+
131
+ stop = length - omission.length
132
+ stop = 0 if stop < 0
133
+ self[0, stop] + omission
134
+ end
135
+
136
+ def truncate_words(count, omission: "...")
137
+ words = split
138
+ return self if words.length <= count
139
+
140
+ words.first(count).join(" ") + omission
141
+ end
142
+
143
+ def indent(amount, indent_char = " ")
144
+ prefix = indent_char * amount
145
+ gsub(/^/, prefix)
146
+ end
147
+
148
+ def remove(pattern) = gsub(pattern, "")
149
+
150
+ # ─────────────────────────────────────────────────────────────
151
+ # Case Transforms
152
+ # ─────────────────────────────────────────────────────────────
153
+
154
+ # ALL UPPER CASE
155
+ def up_case = upcase
156
+
157
+ # all lower case
158
+ def down_case = downcase
159
+
160
+ # snake_case
161
+ # Converts CamelCase, kebab-case, spaces to snake_case
162
+ def snake_case = gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').gsub(/([a-z\d])([A-Z])/, '\1_\2').gsub(/[\s-]+/, "_").downcase
163
+
164
+ # SCREAM_CASE (screaming snake case / constant case)
165
+ def scream_case = snake_case.upcase
166
+
167
+ # kebab-case
168
+ def kebab_case = snake_case.tr("_", "-")
169
+
170
+ # PascalCase (first letter uppercase)
171
+ def pascal_case = snake_case.split("_").map(&:capitalize).join
172
+
173
+ # camelCase (first letter lowercase)
174
+ def camel_case
175
+ result = pascal_case
176
+ return result if result.empty?
177
+
178
+ result[0] = result[0].downcase
179
+ result
180
+ end
181
+
182
+ # Title Case With Proper Rules
183
+ # - Always capitalize first/last word
184
+ # - Lowercase: articles, coord conjunctions, short prepositions
185
+ # - Capitalize after hyphens (unless minor word)
186
+ def title_case
187
+ # Words that should be lowercase (unless first/last)
188
+ # Articles, coordinating conjunctions, short prepositions
189
+ # Note: verbs (is), pronouns (it), subordinating conj (if) should be capitalized
190
+ minor_words = Set.new(%w[
191
+ a an the
192
+ for and nor but or yet so
193
+ at by in to of on up as
194
+ ])
195
+
196
+ # Split keeping delimiters (spaces and hyphens)
197
+ tokens = split(/(\s+|-+)/)
198
+ return "" if tokens.empty?
199
+
200
+ # Find actual words (not delimiters)
201
+ word_indices = tokens.each_index.select { |i| !tokens[i].match?(/^[\s-]+$/) }
202
+ return self if word_indices.empty?
203
+
204
+ first_word_idx = word_indices.first
205
+ last_word_idx = word_indices.last
206
+
207
+ tokens.each_with_index.map do |token, idx|
208
+ if token.match?(/^[\s-]+$/)
209
+ # Delimiter - keep as-is
210
+ token
211
+ elsif idx == first_word_idx || idx == last_word_idx
212
+ # First or last word - always capitalize
213
+ token.capitalize
214
+ elsif minor_words.include?(token.downcase)
215
+ # Minor word - lowercase
216
+ token.downcase
217
+ else
218
+ # Regular word - capitalize
219
+ token.capitalize
220
+ end
221
+ end.join
222
+ end
223
+
224
+ # Aliases without underscores
225
+ # Note: upcase and downcase are Ruby native - don't override
226
+ def snakecase = snake_case
227
+ def screamcase = scream_case
228
+ def kebabcase = kebab_case
229
+ def pascalcase = pascal_case
230
+ def camelcase = camel_case
231
+ def titlecase = title_case
232
+
233
+ # Additional common aliases
234
+ def upper = up_case
235
+ def uppercase = up_case
236
+ def upper_case = up_case
237
+ def caps = up_case
238
+
239
+ def lower = down_case
240
+ def lowercase = down_case
241
+ def lower_case = down_case
242
+
243
+ def var_case = snake_case
244
+ def varcase = snake_case
245
+
246
+ def const_case = scream_case
247
+ def constcase = scream_case
248
+
249
+ def mod_case = pascal_case
250
+ def modcase = pascal_case
251
+ end
252
+
253
+ module EnumerableMethods
254
+ def average
255
+ return 0.0 if respond_to?(:empty?) && empty?
256
+
257
+ arr = to_a
258
+ return 0.0 if arr.empty?
259
+
260
+ arr.sum.to_f / arr.size
261
+ end
262
+
263
+ def mean = average
264
+
265
+ def median
266
+ arr = to_a.sort
267
+ return nil if arr.empty?
268
+
269
+ mid = arr.size / 2
270
+ arr.size.odd? ? arr[mid] : (arr[mid - 1] + arr[mid]) / 2.0
271
+ end
272
+
273
+ def sample_variance
274
+ arr = to_a
275
+ return 0.0 if arr.size < 2
276
+
277
+ avg = arr.sum.to_f / arr.size
278
+ arr.sum { |x| (x - avg) ** 2 } / (arr.size - 1).to_f
279
+ end
280
+
281
+ def variance = sample_variance
282
+
283
+ def standard_deviation = Math.sqrt(sample_variance)
284
+
285
+ def stddev = standard_deviation
286
+
287
+ def percentile(p)
288
+ arr = to_a.sort
289
+ return nil if arr.empty?
290
+
291
+ k = (p / 100.0) * (arr.size - 1)
292
+ f = k.floor
293
+ c = k.ceil
294
+ return arr[f] if f == c
295
+
296
+ (arr[f] * (c - k)) + (arr[c] * (k - f))
297
+ end
298
+
299
+ def q20 = percentile(20)
300
+ def q80 = percentile(80)
301
+
302
+ def robust_average
303
+ arr = to_a
304
+ return nil if arr.empty?
305
+
306
+ (q20.to_f + median.to_f + q80.to_f) / 3.0
307
+ end
308
+
309
+ def amap(method, *args, &block) = map { |item| item.send(method, *args, &block) }
310
+
311
+ def summarize_runs
312
+ arr = to_a
313
+ return [] if arr.empty?
314
+
315
+ arr.chunk_while { |a, b| a == b }.map { |run| [run.size, run.first] }
316
+ end
317
+
318
+ def many?
319
+ count = 0
320
+ if block_given?
321
+ each do |e|
322
+ count += 1 if yield(e)
323
+ return true if count > 1
324
+ end
325
+ else
326
+ each do
327
+ count += 1
328
+ return true if count > 1
329
+ end
330
+ end
331
+ false
332
+ end
333
+
334
+ def index_by = each_with_object({}) { |e, h| h[yield(e)] = e }
335
+
336
+ def index_with(default = nil)
337
+ if block_given?
338
+ each_with_object({}) { |e, h| h[e] = yield(e) }
339
+ else
340
+ each_with_object({}) { |e, h| h[e] = default }
341
+ end
342
+ end
343
+
344
+ def excluding(*elements) = reject { |e| elements.include?(e) }
345
+
346
+ def without(*elements) = excluding(*elements)
347
+
348
+ def including(*elements) = to_a + elements
349
+
350
+ def pluck(*keys)
351
+ if keys.one?
352
+ key = keys.first
353
+ map { |e| e.respond_to?(key) ? e.send(key) : e[key] }
354
+ else
355
+ map { |e| keys.map { |k| e.respond_to?(k) ? e.send(k) : e[k] } }
356
+ end
357
+ end
358
+ end
359
+
360
+ module ArrayMethods
361
+ def second = self[1]
362
+ def third = self[2]
363
+ def fourth = self[3]
364
+ def fifth = self[4]
365
+ def second_to_last = self[-2]
366
+ def third_to_last = self[-3]
367
+
368
+ def to_sentence(connector: ", ", last_connector: ", and ")
369
+ case size
370
+ when 0 then ""
371
+ when 1 then first.to_s
372
+ when 2 then "#{first}#{last_connector.sub(/^, /, ' ')}#{second}"
373
+ else
374
+ "#{self[0..-2].join(connector)}#{last_connector}#{last}"
375
+ end
376
+ end
377
+
378
+ def in_groups_of(n, fill_with = nil)
379
+ arr = dup
380
+ if fill_with && (remainder = arr.size % n) > 0
381
+ arr.concat(Array.new(n - remainder, fill_with))
382
+ end
383
+ arr.each_slice(n).to_a
384
+ end
385
+
386
+ def in_groups(n, fill_with = nil)
387
+ division = size.div(n)
388
+ modulo = size % n
389
+ groups = []
390
+ start = 0
391
+
392
+ n.times do |i|
393
+ length = division + (modulo > 0 && modulo > i ? 1 : 0)
394
+ groups << slice(start, length)
395
+ groups.last << fill_with if fill_with && groups.last.size < division + 1
396
+ start += length
397
+ end
398
+
399
+ groups
400
+ end
401
+
402
+ def extract_options! = last.is_a?(Hash) ? pop : {}
403
+
404
+ def deep_dup
405
+ map do |e|
406
+ e.respond_to?(:deep_dup) ? e.deep_dup : e.dup
407
+ rescue StandardError
408
+ e
409
+ end
410
+ end
411
+ end
412
+
413
+ module HashMethods
414
+ def deep_dup
415
+ each_with_object({}) do |(k, v), h|
416
+ h[if k.respond_to?(:deep_dup)
417
+ k.deep_dup
418
+ else
419
+ begin
420
+ k.dup
421
+ rescue StandardError
422
+ k
423
+ end
424
+ end] =
425
+ if v.respond_to?(:deep_dup)
426
+ v.deep_dup
427
+ else
428
+ begin
429
+ v.dup
430
+ rescue StandardError
431
+ v
432
+ end
433
+ end
434
+ end
435
+ end
436
+
437
+ def deep_merge(other, &) = dup.deep_merge!(other, &)
438
+
439
+ def deep_merge!(other, &block)
440
+ other.each do |k, v|
441
+ self[k] = if self[k].is_a?(Hash) && v.is_a?(Hash)
442
+ self[k].deep_merge(v, &block)
443
+ elsif block_given?
444
+ yield(k, self[k], v)
445
+ else
446
+ v
447
+ end
448
+ end
449
+ self
450
+ end
451
+
452
+ def deep_stringify_keys = transform_keys_recursive(&:to_s)
453
+
454
+ def deep_symbolize_keys = transform_keys_recursive { |k| k.respond_to?(:to_sym) ? k.to_sym : k }
455
+
456
+ def assert_valid_keys(*valid_keys)
457
+ valid_keys = valid_keys.flatten
458
+ each_key do |k|
459
+ unless valid_keys.include?(k)
460
+ raise ArgumentError, "Unknown key: #{k.inspect}. Valid keys are: #{valid_keys.map(&:inspect).join(', ')}"
461
+ end
462
+ end
463
+ self
464
+ end
465
+
466
+ def stable_compact
467
+ compact
468
+ .transform_values do |v|
469
+ case v
470
+ when Hash then v.stable_compact
471
+ when Array then v.map { |e| e.respond_to?(:stable_compact) ? e.stable_compact : e }
472
+ else v
473
+ end
474
+ end
475
+ .sort_by { |k, _| k.to_s }
476
+ .to_h
477
+ end
478
+
479
+ def to_sig
480
+ require "digest"
481
+ Digest::SHA1.hexdigest(stable_compact.inspect)
482
+ end
483
+
484
+ def transform_keys_recursive(&block)
485
+ each_with_object({}) do |(k, v), h|
486
+ new_key = yield(k)
487
+ h[new_key] = case v
488
+ when Hash then v.transform_keys_recursive(&block)
489
+ when Array then v.map { |e| e.is_a?(Hash) ? e.transform_keys_recursive(&block) : e }
490
+ else v
491
+ end
492
+ end
493
+ end
494
+ end
495
+
496
+ # Ordinal suffixes (defined outside refinement to avoid warning)
497
+ ORDINALS = { 1 => "st", 2 => "nd", 3 => "rd" }.freeze
498
+
499
+ module IntegerMethods
500
+ def ordinal
501
+ abs_mod_100 = abs % 100
502
+ if (11..13).cover?(abs_mod_100)
503
+ "th"
504
+ else
505
+ CoreExt::ORDINALS.fetch(abs % 10, "th")
506
+ end
507
+ end
508
+
509
+ def ordinalize = "#{self}#{ordinal}"
510
+ end
511
+
512
+ # ─────────────────────────────────────────────────────────────
513
+ # Refinements
514
+ # Use import_methods (Ruby 3.1+) instead of include (removed in 3.2)
515
+ # ─────────────────────────────────────────────────────────────
516
+
517
+ refine Object do
518
+ import_methods ObjectMethods
519
+ end
520
+
521
+ refine NilClass do
522
+ import_methods NilMethods
523
+ end
524
+
525
+ refine FalseClass do
526
+ import_methods FalseMethods
527
+ end
528
+
529
+ refine TrueClass do
530
+ import_methods TrueMethods
531
+ end
532
+
533
+ refine Numeric do
534
+ import_methods NumericMethods
535
+ end
536
+
537
+ refine Array do
538
+ import_methods ArrayBlankMethods
539
+ import_methods ArrayMethods
540
+ end
541
+
542
+ refine Hash do
543
+ import_methods HashBlankMethods
544
+ import_methods HashMethods
545
+ end
546
+
547
+ refine String do
548
+ import_methods StringMethods
549
+ end
550
+
551
+ refine Enumerable do
552
+ import_methods EnumerableMethods
553
+ end
554
+
555
+ refine Integer do
556
+ import_methods IntegerMethods
557
+ end
558
+ end
559
+ end
560
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Global loading of Devex support extensions.
4
+ #
5
+ # This file monkey-patches core classes with the extensions from CoreExt.
6
+ # Use this in CLI tools where you want the extensions available everywhere.
7
+ #
8
+ # For library code, prefer refinements:
9
+ # using Devex::Support::CoreExt
10
+ #
11
+ # For CLI tools:
12
+ # require "devex/support/global"
13
+ #
14
+
15
+ require_relative "../support"
16
+ require_relative "core_ext"
17
+
18
+ module Devex
19
+ module Support
20
+ module Global
21
+ class << self
22
+ def load!
23
+ return if @loaded
24
+
25
+ # Include the shared implementation modules into core classes
26
+ Object.include CoreExt::ObjectMethods
27
+ NilClass.include CoreExt::NilMethods
28
+ FalseClass.include CoreExt::FalseMethods
29
+ TrueClass.include CoreExt::TrueMethods
30
+ Numeric.include CoreExt::NumericMethods
31
+
32
+ Array.include CoreExt::ArrayBlankMethods
33
+ Array.include CoreExt::ArrayMethods
34
+
35
+ Hash.include CoreExt::HashBlankMethods
36
+ Hash.include CoreExt::HashMethods
37
+
38
+ String.include CoreExt::StringMethods
39
+
40
+ # Enumerable goes on the module, affects all including classes
41
+ Enumerable.module_eval { include CoreExt::EnumerableMethods }
42
+
43
+ Integer.include CoreExt::IntegerMethods
44
+
45
+ # Also add ANSI string methods
46
+ add_ansi_string_methods!
47
+
48
+ @loaded = true
49
+ end
50
+
51
+ private
52
+
53
+ def add_ansi_string_methods!
54
+ String.class_eval do
55
+ def ansi(*styles, bg: nil) = Devex::Support::ANSI[self, *styles, bg: bg]
56
+
57
+ def strip_ansi = Devex::Support::ANSI.strip(self)
58
+
59
+ def visible_length = Devex::Support::ANSI.visible_length(self)
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ # Auto-load when required
68
+ Devex::Support::Global.load!