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
data/lib/devex/cli.rb ADDED
@@ -0,0 +1,340 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Devex
4
+ # Main CLI class - entry point for CLI commands.
5
+ #
6
+ # Can be used with a Configuration for custom CLI applications,
7
+ # or with defaults for backward-compatible dx usage.
8
+ #
9
+ # @example Core usage
10
+ # config = Devex::Core::Configuration.new(
11
+ # executable_name: "mycli",
12
+ # flag_prefix: "mycli"
13
+ # )
14
+ # cli = Devex::CLI.new(config: config)
15
+ # cli.load_tools("/path/to/tools")
16
+ # exit cli.run(ARGV)
17
+ #
18
+ class CLI
19
+ HELP_FLAGS = %w[-h -? --help].freeze
20
+ HELP_WORD = "help"
21
+
22
+ # Universal flags available to all commands
23
+ UNIVERSAL_FLAGS = {
24
+ format: ["-f", "--format=FORMAT"],
25
+ verbose: ["-v", "--verbose"],
26
+ quiet: ["-q", "--quiet"],
27
+ no_color: ["--no-color"],
28
+ color: ["--color=MODE"]
29
+ }.freeze
30
+
31
+ attr_reader :root_tool, :executable_name, :project_root, :global_options, :config
32
+
33
+ # @param executable_name [String] name of the executable (for help text)
34
+ # @param config [Core::Configuration, nil] full configuration (overrides executable_name)
35
+ def initialize(executable_name: "dx", config: nil)
36
+ @config = config
37
+ @executable_name = config&.executable_name || executable_name
38
+ @flag_prefix = config&.flag_prefix || "dx"
39
+ @root_tool = Tool.new(nil)
40
+ @builtin_root = Tool.new(nil)
41
+ @mixins = {}
42
+ @project_root = nil
43
+ @global_options = {
44
+ format: nil,
45
+ verbose: 0,
46
+ quiet: false
47
+ }
48
+ end
49
+
50
+ # Main entry point
51
+ def run(argv = ARGV)
52
+ argv = argv.dup
53
+
54
+ # Configure Context with our configuration (for env var prefix, etc.)
55
+ Context.configure(@config) if @config
56
+
57
+ # Extract and apply global flags first (before help or tool resolution)
58
+ argv, show_version = extract_global_flags(argv)
59
+
60
+ # --{prefix}-version shows gem version and exits
61
+ if show_version
62
+ output_gem_version
63
+ return 0
64
+ end
65
+
66
+ # Transform help anywhere in args to --help at the right place
67
+ argv, show_help = extract_help(argv)
68
+
69
+ # Find the tool to execute
70
+ tool, remaining_argv = resolve_tool(argv)
71
+
72
+ if show_help
73
+ show_help(tool)
74
+ return 0
75
+ end
76
+
77
+ if tool.nil?
78
+ show_help(@root_tool)
79
+ return 1
80
+ end
81
+
82
+ begin
83
+ # Track task invocation in context
84
+ task_name = tool.full_name.empty? ? "root" : tool.full_name
85
+ Context.with_task(task_name) do
86
+ tool.execute(remaining_argv, self)
87
+ end
88
+ 0
89
+ rescue Error => e
90
+ Output.error(e.message)
91
+ 1
92
+ rescue OptionParser::ParseError => e
93
+ Output.error(e.message)
94
+ 1
95
+ end
96
+ end
97
+
98
+ # Load built-in tools from a directory
99
+ # @param dir [String, nil] directory path (uses gem builtins if nil)
100
+ def load_builtins(dir = nil)
101
+ builtin_dir = dir || File.join(__dir__, "builtins")
102
+ Loader.load_directory(builtin_dir, @builtin_root, @mixins)
103
+ end
104
+
105
+ # Load tools from a directory
106
+ # @param dir [String] directory path
107
+ def load_tools(dir)
108
+ Loader.load_directory(dir, @root_tool, @mixins) if dir && File.directory?(dir)
109
+ end
110
+
111
+ # Load project-specific tools
112
+ def load_project_tools(project_root)
113
+ @project_root = project_root
114
+
115
+ # Add project lib/ to load path so tools can `require` without `require_relative`
116
+ add_project_lib_to_load_path(project_root)
117
+
118
+ tools_dir = Devex.tools_dir(project_root)
119
+ Loader.load_directory(tools_dir, @root_tool, @mixins) if tools_dir
120
+ end
121
+
122
+ # Add project's lib/ directory to $LOAD_PATH if it exists.
123
+ def add_project_lib_to_load_path(project_root)
124
+ return unless project_root
125
+
126
+ lib_dir = File.join(project_root, "lib")
127
+ return unless File.directory?(lib_dir)
128
+ return if $LOAD_PATH.include?(lib_dir)
129
+
130
+ $LOAD_PATH.unshift(lib_dir)
131
+ end
132
+
133
+ # Merge builtin tools (project tools take precedence)
134
+ def merge_builtins
135
+ @builtin_root.subtools.each do |name, builtin_tool|
136
+ if @root_tool.subtools[name]
137
+ # Project overrides builtin - store reference for super-like behavior
138
+ @root_tool.subtools[name].builtin = builtin_tool
139
+ else
140
+ # No override - use builtin directly
141
+ @root_tool.add_subtool(builtin_tool)
142
+ end
143
+ end
144
+ end
145
+
146
+ # Display help for a tool
147
+ def show_help(tool)
148
+ help_text = tool.help_text(@executable_name)
149
+
150
+ # Append global options section if showing root help
151
+ help_text += global_options_help if tool == @root_tool
152
+
153
+ puts help_text
154
+ end
155
+
156
+ private
157
+
158
+ # Flag prefix for framework flags (--{prefix}-version, etc.)
159
+ def flag_prefix
160
+ @flag_prefix
161
+ end
162
+
163
+ # Extract global flags from argv, apply Context overrides
164
+ # Returns [remaining_argv, show_version]
165
+ def extract_global_flags(argv)
166
+ remaining = []
167
+ show_version = false
168
+ prefix = flag_prefix
169
+
170
+ # Build dynamic flag patterns
171
+ version_flag = "--#{prefix}-version"
172
+ from_dir_flag = "--#{prefix}-from-dir"
173
+ force_color_flag = "--#{prefix}-force-color"
174
+ no_color_flag = "--#{prefix}-no-color"
175
+ agent_mode_flag = "--#{prefix}-agent-mode"
176
+ no_agent_flag = "--#{prefix}-no-agent-mode"
177
+ interactive_flag = "--#{prefix}-interactive"
178
+ no_interactive = "--#{prefix}-no-interactive"
179
+ env_flag_pattern = /^--#{Regexp.escape(prefix)}-env=(.+)$/
180
+ ci_flag = "--#{prefix}-ci"
181
+ no_ci_flag = "--#{prefix}-no-ci"
182
+ terminal_flag = "--#{prefix}-terminal"
183
+ no_terminal_flag = "--#{prefix}-no-terminal"
184
+
185
+ i = 0
186
+ while i < argv.length
187
+ arg = argv[i]
188
+ consumed = false
189
+
190
+ # Check universal flags
191
+ case arg
192
+ when version_flag
193
+ show_version = true
194
+ consumed = true
195
+ when "-f", "--format"
196
+ @global_options[:format] = argv[i + 1]
197
+ i += 1
198
+ consumed = true
199
+ when /^--format=(.+)$/
200
+ @global_options[:format] = ::Regexp.last_match(1)
201
+ consumed = true
202
+ when "-v", "--verbose"
203
+ @global_options[:verbose] += 1
204
+ consumed = true
205
+ when "--no-verbose"
206
+ @global_options[:verbose] = 0
207
+ consumed = true
208
+ when "-q", "--quiet"
209
+ @global_options[:quiet] = true
210
+ consumed = true
211
+ when "--no-quiet"
212
+ @global_options[:quiet] = false
213
+ consumed = true
214
+ when "--no-color"
215
+ Context.set_override(:color, false)
216
+ consumed = true
217
+ when "--color=always"
218
+ Context.set_override(:color, true)
219
+ consumed = true
220
+ when "--color=never"
221
+ Context.set_override(:color, false)
222
+ consumed = true
223
+ when "--color=auto"
224
+ Context.clear_override(:color)
225
+ consumed = true
226
+ when /^--color=(.+)$/
227
+ consumed = true
228
+ end
229
+
230
+ # Check hidden debug flags (not in help)
231
+ unless consumed
232
+ case arg
233
+ when force_color_flag
234
+ Context.set_override(:color, true)
235
+ consumed = true
236
+ when no_color_flag
237
+ Context.set_override(:color, false)
238
+ consumed = true
239
+ when agent_mode_flag
240
+ Context.set_override(:agent_mode, true)
241
+ consumed = true
242
+ when no_agent_flag
243
+ Context.set_override(:agent_mode, false)
244
+ consumed = true
245
+ when interactive_flag
246
+ Context.set_override(:interactive, true)
247
+ consumed = true
248
+ when no_interactive
249
+ Context.set_override(:interactive, false)
250
+ consumed = true
251
+ when env_flag_pattern
252
+ Context.set_override(:env, ::Regexp.last_match(1))
253
+ consumed = true
254
+ when ci_flag
255
+ Context.set_override(:ci, true)
256
+ consumed = true
257
+ when no_ci_flag
258
+ Context.set_override(:ci, false)
259
+ consumed = true
260
+ when terminal_flag
261
+ Context.set_override(:terminal, true)
262
+ consumed = true
263
+ when no_terminal_flag
264
+ Context.set_override(:terminal, false)
265
+ consumed = true
266
+ end
267
+ end
268
+
269
+ remaining << arg unless consumed
270
+ i += 1
271
+ end
272
+
273
+ [remaining, show_version]
274
+ end
275
+
276
+ # Extract help indicators from argv, return [cleaned_argv, show_help]
277
+ def extract_help(argv)
278
+ show_help = false
279
+
280
+ HELP_FLAGS.each do |flag|
281
+ if argv.include?(flag)
282
+ argv.delete(flag)
283
+ show_help = true
284
+ end
285
+ end
286
+
287
+ if argv.include?(HELP_WORD)
288
+ argv.delete(HELP_WORD)
289
+ show_help = true
290
+ end
291
+
292
+ [argv, show_help]
293
+ end
294
+
295
+ # Resolve a tool from argv
296
+ def resolve_tool(argv)
297
+ tool = @root_tool
298
+ remaining = argv.dup
299
+
300
+ while remaining.any?
301
+ candidate = remaining.first
302
+ break if candidate.start_with?("-")
303
+
304
+ subtool = tool.subtool(candidate)
305
+ break unless subtool
306
+
307
+ tool = subtool
308
+ remaining.shift
309
+ end
310
+
311
+ [tool, remaining]
312
+ end
313
+
314
+ # Generate help text for global options
315
+ def global_options_help
316
+ prefix = flag_prefix
317
+ <<~HELP
318
+
319
+ Global Options:
320
+ -f, --format=FORMAT Output format (text, json, yaml)
321
+ -v, --verbose Increase verbosity (can be repeated)
322
+ -q, --quiet Suppress non-error output
323
+ --no-color Disable colored output
324
+ --color=MODE Color mode: auto, always, never
325
+ --#{prefix}-version#{' ' * (14 - prefix.length)}Show #{@executable_name} version
326
+ --#{prefix}-from-dir=PATH Operate on project at PATH
327
+ HELP
328
+ end
329
+
330
+ # Output gem version (not project version)
331
+ def output_gem_version
332
+ if @global_options[:format] == "json"
333
+ require "json"
334
+ puts JSON.generate({ name: @executable_name, version: VERSION })
335
+ else
336
+ puts "#{@executable_name} #{VERSION}"
337
+ end
338
+ end
339
+ end
340
+ end