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
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
|