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
|
@@ -0,0 +1,1000 @@
|
|
|
1
|
+
# Developing Tools for Devex
|
|
2
|
+
|
|
3
|
+
This guide covers how to create tools (commands) for devex, including all available interfaces, best practices, and patterns.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
Create a file in `tools/` (or `lib/devex/builtins/` for built-ins):
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
# tools/hello.rb
|
|
11
|
+
desc "Say hello"
|
|
12
|
+
|
|
13
|
+
def run
|
|
14
|
+
$stdout.print "Hello, world!\n"
|
|
15
|
+
end
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Run with: `dx hello`
|
|
19
|
+
|
|
20
|
+
### Using Project Libraries
|
|
21
|
+
|
|
22
|
+
Devex automatically adds your project's `lib/` directory to the load path. This means tools can `require` project code directly:
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
# tools/deploy.rb
|
|
26
|
+
require "myproject/config" # loads lib/myproject/config.rb
|
|
27
|
+
require "myproject/deploy" # loads lib/myproject/deploy.rb
|
|
28
|
+
|
|
29
|
+
desc "Deploy the application"
|
|
30
|
+
|
|
31
|
+
def run
|
|
32
|
+
config = MyProject::Config.load
|
|
33
|
+
MyProject::Deploy.run(config)
|
|
34
|
+
end
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
No `require_relative` needed - just use standard `require` with your library's namespace.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Tool Definition DSL
|
|
42
|
+
|
|
43
|
+
### Basic Metadata
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
desc "Short description (shown in help listing)"
|
|
47
|
+
|
|
48
|
+
long_desc <<~DESC
|
|
49
|
+
Longer description shown when running `dx help <tool>`.
|
|
50
|
+
Can span multiple lines and include examples.
|
|
51
|
+
DESC
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Flags (Options)
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
flag :dry_run, "-n", "--dry-run", desc: "Show what would happen"
|
|
58
|
+
flag :count, "-c COUNT", "--count=COUNT", desc: "Number of times"
|
|
59
|
+
flag :output, "-o FILE", "--output=FILE", desc: "Output file"
|
|
60
|
+
flag :format, "--format=FMT", desc: "Output format", default: "text"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Access in `run`: `dry_run`, `count`, `output`, `format` (as methods) or `options[:dry_run]`
|
|
64
|
+
|
|
65
|
+
**Flag options:**
|
|
66
|
+
- `desc:` - Description shown in help
|
|
67
|
+
- `default:` - Default value for the flag. Boolean flags without values default to `false`. Flags with arguments default to `nil` unless `default:` is specified.
|
|
68
|
+
|
|
69
|
+
**Reserved flags:** The following flags are reserved for global use and cannot be used by tools:
|
|
70
|
+
- `-v`, `--verbose` - Use `verbose?` to check global verbose level
|
|
71
|
+
- `-f`, `--format` - Use `global_options[:format]`
|
|
72
|
+
- `-q`, `--quiet` - Use `global_options[:quiet]`
|
|
73
|
+
- `--no-color`, `--color` - Color is handled automatically
|
|
74
|
+
|
|
75
|
+
Tools that define conflicting flags will fail with an error when invoked.
|
|
76
|
+
|
|
77
|
+
### Positional Arguments
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
required_arg :filename, desc: "File to process"
|
|
81
|
+
optional_arg :output, desc: "Output file (default: stdout)"
|
|
82
|
+
remaining_args :files, desc: "Additional files"
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Access in `run`: `filename`, `output`, `files` (as methods)
|
|
86
|
+
|
|
87
|
+
### Nested Tools (Subcommands)
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
# tools/db.rb
|
|
91
|
+
desc "Database operations"
|
|
92
|
+
|
|
93
|
+
tool "migrate" do
|
|
94
|
+
desc "Run migrations"
|
|
95
|
+
def run
|
|
96
|
+
# ...
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
tool "seed" do
|
|
101
|
+
desc "Seed the database"
|
|
102
|
+
flag :env, "-e ENV", desc: "Environment"
|
|
103
|
+
def run
|
|
104
|
+
# ...
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Access as: `dx db migrate`, `dx db seed --env=test`
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## External Command Execution
|
|
114
|
+
|
|
115
|
+
The `Devex::Exec` module provides methods for running external commands with automatic environment handling.
|
|
116
|
+
|
|
117
|
+
### Quick Reference
|
|
118
|
+
|
|
119
|
+
| Method | Purpose | stdout | Returns |
|
|
120
|
+
|--------|---------|--------|---------|
|
|
121
|
+
| `cmd(*args)` | Run command, wait | streams | `Result` |
|
|
122
|
+
| `cmd?(*args)` | Test if command succeeds | silent | `bool` |
|
|
123
|
+
| `capture(*args)` | Run and capture output | captured | `Result` |
|
|
124
|
+
| `spawn(*args)` | Run in background | configurable | `Controller` |
|
|
125
|
+
| `exec!(*args)` | Replace this process | N/A | never returns |
|
|
126
|
+
| `shell(str)` | Run via shell | streams | `Result` |
|
|
127
|
+
| `shell?(str)` | Test shell command | silent | `bool` |
|
|
128
|
+
| `ruby(*args)` | Run Ruby subprocess | streams | `Result` |
|
|
129
|
+
| `tool(name, *args)` | Run another dx tool | streams | `Result` |
|
|
130
|
+
|
|
131
|
+
**Note:** `cmd` and `cmd?` are aliases for `run` and `run?`. Use `cmd`/`cmd?` inside tools to avoid shadowing the `def run` entry point.
|
|
132
|
+
|
|
133
|
+
### Basic Usage
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
# In your tool's run method:
|
|
137
|
+
include Devex::Exec
|
|
138
|
+
|
|
139
|
+
def run
|
|
140
|
+
# Use `cmd` instead of `run` to avoid collision with `def run`
|
|
141
|
+
cmd "bundle", "install"
|
|
142
|
+
|
|
143
|
+
# Check if command succeeded
|
|
144
|
+
result = cmd "make", "test"
|
|
145
|
+
if result.failed?
|
|
146
|
+
Output.error "Tests failed"
|
|
147
|
+
exit result.exit_code
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Exit immediately on failure
|
|
151
|
+
cmd("bundle", "install").exit_on_failure!
|
|
152
|
+
|
|
153
|
+
# Chain commands (short-circuit on failure)
|
|
154
|
+
cmd("lint").then { cmd("test") }.then { cmd("build") }.exit_on_failure!
|
|
155
|
+
end
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
**Note:** Use `cmd` instead of `run` when including `Devex::Exec` in tools. The `def run` entry point shadows `Devex::Exec.run`, so `cmd` is provided as an alias to avoid this collision.
|
|
159
|
+
|
|
160
|
+
### `cmd` / `run` - Run Command
|
|
161
|
+
|
|
162
|
+
The workhorse method. Runs a command, streams output, waits for completion.
|
|
163
|
+
Use `cmd` inside tools (alias for `run`) to avoid shadowing `def run`.
|
|
164
|
+
|
|
165
|
+
```ruby
|
|
166
|
+
cmd "bundle", "install"
|
|
167
|
+
|
|
168
|
+
# With options
|
|
169
|
+
cmd "make", "test", env: { CI: "1" }, chdir: "subproject/"
|
|
170
|
+
|
|
171
|
+
# With timeout (seconds)
|
|
172
|
+
cmd "slow_task", timeout: 30
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**Behavior:**
|
|
176
|
+
- Streams stdout/stderr to terminal
|
|
177
|
+
- Applies environment stack (cleans bundler pollution)
|
|
178
|
+
- Returns `Result` object
|
|
179
|
+
- Never raises on non-zero exit
|
|
180
|
+
|
|
181
|
+
### `cmd?` / `run?` - Test Command Success
|
|
182
|
+
|
|
183
|
+
Silent execution, returns boolean. Perfect for conditionals.
|
|
184
|
+
Use `cmd?` inside tools (alias for `run?`).
|
|
185
|
+
|
|
186
|
+
```ruby
|
|
187
|
+
if cmd? "which", "rubocop"
|
|
188
|
+
cmd "rubocop", "--autocorrect"
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
unless cmd? "git", "diff", "--quiet"
|
|
192
|
+
Output.warn "Uncommitted changes"
|
|
193
|
+
end
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### `capture` - Capture Output
|
|
197
|
+
|
|
198
|
+
When you need the output as a string.
|
|
199
|
+
|
|
200
|
+
```ruby
|
|
201
|
+
result = capture "git", "rev-parse", "HEAD"
|
|
202
|
+
commit = result.stdout.strip
|
|
203
|
+
|
|
204
|
+
result = capture "git", "status", "--porcelain"
|
|
205
|
+
if result.success? && result.stdout.empty?
|
|
206
|
+
Output.success "Working directory clean"
|
|
207
|
+
end
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### `spawn` - Background Execution
|
|
211
|
+
|
|
212
|
+
Start a process without waiting. Returns immediately with a Controller.
|
|
213
|
+
|
|
214
|
+
```ruby
|
|
215
|
+
# Start server in background
|
|
216
|
+
server = spawn "rails", "server", "-p", "3000"
|
|
217
|
+
|
|
218
|
+
# Do other work...
|
|
219
|
+
run "curl", "http://localhost:3000/health"
|
|
220
|
+
|
|
221
|
+
# Clean up
|
|
222
|
+
server.kill(:TERM)
|
|
223
|
+
result = server.result # Wait for exit
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### `exec!` - Replace Process
|
|
227
|
+
|
|
228
|
+
Replaces the current process. Use sparingly.
|
|
229
|
+
|
|
230
|
+
```ruby
|
|
231
|
+
exec! "vim", filename
|
|
232
|
+
# This line never executes
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### `shell` / `shell?` - Shell Execution
|
|
236
|
+
|
|
237
|
+
When you need shell features (pipes, globs, variable expansion).
|
|
238
|
+
|
|
239
|
+
```ruby
|
|
240
|
+
# Pipes and variables
|
|
241
|
+
shell "grep TODO **/*.rb | wc -l"
|
|
242
|
+
shell "echo $HOME"
|
|
243
|
+
|
|
244
|
+
# Test with shell
|
|
245
|
+
if shell? "command -v docker"
|
|
246
|
+
shell "docker compose up -d"
|
|
247
|
+
end
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
**Security note:** Never interpolate untrusted input into shell commands.
|
|
251
|
+
|
|
252
|
+
### `ruby` - Ruby Subprocess
|
|
253
|
+
|
|
254
|
+
Run Ruby with clean environment.
|
|
255
|
+
|
|
256
|
+
```ruby
|
|
257
|
+
ruby "-e", "puts RUBY_VERSION"
|
|
258
|
+
ruby "script.rb", "--verbose"
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### `tool` - Run Another dx Tool
|
|
262
|
+
|
|
263
|
+
Invoke another devex tool programmatically.
|
|
264
|
+
|
|
265
|
+
```ruby
|
|
266
|
+
tool "lint", "--fix"
|
|
267
|
+
|
|
268
|
+
if tool?("test")
|
|
269
|
+
tool "deploy"
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Capture tool output
|
|
273
|
+
result = tool "version", capture: true
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Propagates call tree so child tool knows it was invoked from parent.
|
|
277
|
+
|
|
278
|
+
### The Result Object
|
|
279
|
+
|
|
280
|
+
All commands (except `run?`/`shell?`/`exec!`) return a `Result`:
|
|
281
|
+
|
|
282
|
+
```ruby
|
|
283
|
+
result = run "make", "test"
|
|
284
|
+
|
|
285
|
+
# Status
|
|
286
|
+
result.success? # exit_code == 0
|
|
287
|
+
result.failed? # exit_code != 0 or didn't start
|
|
288
|
+
result.signaled? # killed by signal
|
|
289
|
+
result.timed_out? # killed due to timeout
|
|
290
|
+
|
|
291
|
+
# Info
|
|
292
|
+
result.command # ["make", "test"]
|
|
293
|
+
result.exit_code # 0-255 or nil if signaled
|
|
294
|
+
result.pid # Process ID
|
|
295
|
+
result.duration # Seconds elapsed
|
|
296
|
+
|
|
297
|
+
# Output (if captured)
|
|
298
|
+
result.stdout # String or nil
|
|
299
|
+
result.stderr # String or nil
|
|
300
|
+
result.stdout_lines # Array of lines
|
|
301
|
+
|
|
302
|
+
# Monad operations
|
|
303
|
+
result.exit_on_failure! # Exit process if failed
|
|
304
|
+
result.then { run("next") } # Chain if successful
|
|
305
|
+
result.map { |out| out.strip } # Transform stdout
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### The Controller Object
|
|
309
|
+
|
|
310
|
+
`spawn` returns a `Controller` for managing background processes:
|
|
311
|
+
|
|
312
|
+
```ruby
|
|
313
|
+
ctrl = spawn "server"
|
|
314
|
+
|
|
315
|
+
ctrl.pid # Process ID
|
|
316
|
+
ctrl.executing? # Still running?
|
|
317
|
+
ctrl.elapsed # Seconds since start
|
|
318
|
+
|
|
319
|
+
ctrl.kill(:TERM) # Send signal
|
|
320
|
+
ctrl.terminate # TERM + wait
|
|
321
|
+
|
|
322
|
+
ctrl.result # Wait and get Result
|
|
323
|
+
ctrl.result(timeout: 30) # With timeout
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### Common Options
|
|
327
|
+
|
|
328
|
+
```ruby
|
|
329
|
+
run "command",
|
|
330
|
+
env: { KEY: "value" }, # Additional environment variables
|
|
331
|
+
chdir: "subdir/", # Working directory
|
|
332
|
+
timeout: 30, # Seconds before killing
|
|
333
|
+
raw: true, # Skip all environment wrappers
|
|
334
|
+
bundle: false, # Skip bundle exec wrapping
|
|
335
|
+
mise: false, # Skip mise exec wrapping
|
|
336
|
+
dotenv: true, # Enable dotenv wrapper (explicit opt-in)
|
|
337
|
+
clean_env: true # Clean bundler pollution (default)
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
### Environment Wrapper Chain
|
|
341
|
+
|
|
342
|
+
When running commands, devex automatically applies a wrapper chain:
|
|
343
|
+
|
|
344
|
+
```
|
|
345
|
+
[dotenv] [mise exec --] [bundle exec] your-command
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
| Wrapper | When Applied | Default |
|
|
349
|
+
|---------|--------------|---------|
|
|
350
|
+
| `dotenv` | Explicit opt-in only | OFF |
|
|
351
|
+
| `mise exec --` | Auto if `.mise.toml` or `.tool-versions` exists | AUTO |
|
|
352
|
+
| `bundle exec` | Auto if `Gemfile` exists and command looks like a gem | AUTO |
|
|
353
|
+
|
|
354
|
+
**Examples:**
|
|
355
|
+
|
|
356
|
+
```ruby
|
|
357
|
+
# Just runs: echo hello
|
|
358
|
+
run "echo", "hello"
|
|
359
|
+
|
|
360
|
+
# Auto-detected Gemfile, runs: bundle exec rspec
|
|
361
|
+
run "rspec"
|
|
362
|
+
|
|
363
|
+
# Auto-detected .mise.toml, runs: mise exec -- bundle exec rspec
|
|
364
|
+
run "rspec"
|
|
365
|
+
|
|
366
|
+
# Explicit dotenv, runs: dotenv mise exec -- bundle exec rspec
|
|
367
|
+
run "rspec", dotenv: true
|
|
368
|
+
|
|
369
|
+
# Skip mise wrapping: bundle exec rspec
|
|
370
|
+
run "rspec", mise: false
|
|
371
|
+
|
|
372
|
+
# Skip all wrappers: rspec
|
|
373
|
+
run "rspec", raw: true
|
|
374
|
+
|
|
375
|
+
# Force mise even if not detected: mise exec -- echo hello
|
|
376
|
+
run "echo", "hello", mise: true
|
|
377
|
+
|
|
378
|
+
# Force bundle exec even for non-gem commands: bundle exec custom-script
|
|
379
|
+
run "custom-script", bundle: true
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
**Gem commands that trigger `bundle exec`:**
|
|
383
|
+
`rake`, `rspec`, `rubocop`, `standardrb`, `steep`, `rbs`, `rails`, `sidekiq`, `puma`, `unicorn`, `thin`, `bundler`, `bundle`, `erb`, `rdoc`, `ri`, `yard`
|
|
384
|
+
|
|
385
|
+
**Note:** The `dotenv` option requires the `dotenv` CLI to be installed (`gem install dotenv`). It loads `.env` files before running the command.
|
|
386
|
+
|
|
387
|
+
---
|
|
388
|
+
|
|
389
|
+
## Directory Context
|
|
390
|
+
|
|
391
|
+
Devex provides a rich directory context system for tools that need to work with project paths.
|
|
392
|
+
|
|
393
|
+
### Core Directories (`Devex::Dirs`)
|
|
394
|
+
|
|
395
|
+
```ruby
|
|
396
|
+
# Where dx was invoked from
|
|
397
|
+
Devex::Dirs.invoked_dir # => Path
|
|
398
|
+
|
|
399
|
+
# The destination directory (usually same as invoked_dir)
|
|
400
|
+
Devex::Dirs.dest_dir # => Path
|
|
401
|
+
|
|
402
|
+
# Project root (found by walking up looking for markers)
|
|
403
|
+
Devex::Dirs.project_dir # => Path
|
|
404
|
+
|
|
405
|
+
# Where devex gem itself lives
|
|
406
|
+
Devex::Dirs.dx_src_dir # => Path
|
|
407
|
+
|
|
408
|
+
# Is this inside a project?
|
|
409
|
+
Devex::Dirs.in_project? # => true/false
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
Project markers searched (in order): `.dx.yml`, `.dx/`, `.git`, `Gemfile`, `Rakefile`
|
|
413
|
+
|
|
414
|
+
### Project Paths (`Devex::ProjectPaths`)
|
|
415
|
+
|
|
416
|
+
Lazy path resolution with fail-fast feedback:
|
|
417
|
+
|
|
418
|
+
```ruby
|
|
419
|
+
prj = Devex::ProjectPaths.new(root: Devex::Dirs.project_dir)
|
|
420
|
+
|
|
421
|
+
# Standard paths (raises if not found)
|
|
422
|
+
prj.root # => /path/to/project
|
|
423
|
+
prj.lib # => /path/to/project/lib
|
|
424
|
+
prj.src # => /path/to/project/src
|
|
425
|
+
prj.bin # => /path/to/project/bin
|
|
426
|
+
prj.exe # => /path/to/project/exe
|
|
427
|
+
|
|
428
|
+
# Paths with alternatives (tries each in order)
|
|
429
|
+
prj.test # => finds test/, spec/, or tests/
|
|
430
|
+
prj.docs # => finds docs/ or doc/
|
|
431
|
+
|
|
432
|
+
# Glob from root
|
|
433
|
+
prj["*.rb"] # => Array of Path objects
|
|
434
|
+
prj["lib/**/*.rb"] # => Array of Path objects
|
|
435
|
+
|
|
436
|
+
# Config detection (simple vs organized mode)
|
|
437
|
+
prj.config # => .dx.yml or .dx/config.yml
|
|
438
|
+
prj.tools # => tools/ or .dx/tools/
|
|
439
|
+
|
|
440
|
+
# Version file detection
|
|
441
|
+
prj.version # => VERSION, version.rb, or similar
|
|
442
|
+
|
|
443
|
+
# Check mode
|
|
444
|
+
prj.organized_mode? # => true if .dx/ directory exists
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
### Working Directory Context
|
|
448
|
+
|
|
449
|
+
Immutable working directory for command execution:
|
|
450
|
+
|
|
451
|
+
```ruby
|
|
452
|
+
include Devex::WorkingDirMixin
|
|
453
|
+
|
|
454
|
+
def run
|
|
455
|
+
# Current working directory
|
|
456
|
+
working_dir # => Path to current context
|
|
457
|
+
|
|
458
|
+
# Execute block in different directory
|
|
459
|
+
within "packages/core" do
|
|
460
|
+
working_dir # => /project/packages/core
|
|
461
|
+
run "npm", "test" # Runs from packages/core
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
working_dir # => /project (unchanged)
|
|
465
|
+
|
|
466
|
+
# Nest as deep as needed
|
|
467
|
+
within "apps" do
|
|
468
|
+
within "web" do
|
|
469
|
+
run "yarn", "build"
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
# Use with project paths
|
|
474
|
+
within prj.test do
|
|
475
|
+
run "rspec"
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
The `within` block:
|
|
481
|
+
- Takes relative or absolute paths
|
|
482
|
+
- Restores directory on block exit (even if exception)
|
|
483
|
+
- Thread-safe via mutex
|
|
484
|
+
- Passes directory to spawned commands via `chdir:`
|
|
485
|
+
|
|
486
|
+
### The Path Class
|
|
487
|
+
|
|
488
|
+
All directory methods return `Devex::Support::Path` objects:
|
|
489
|
+
|
|
490
|
+
```ruby
|
|
491
|
+
path = Devex::Support::Path["/some/path"]
|
|
492
|
+
path = Devex::Support::Path.pwd
|
|
493
|
+
|
|
494
|
+
# Navigation (returns new Path, immutable)
|
|
495
|
+
path / "subdir" # => Path to /some/path/subdir
|
|
496
|
+
path.parent # => Path to /some
|
|
497
|
+
path.join("a", "b") # => Path to /some/path/a/b
|
|
498
|
+
|
|
499
|
+
# Queries
|
|
500
|
+
path.exist?
|
|
501
|
+
path.file?
|
|
502
|
+
path.directory?
|
|
503
|
+
path.readable?
|
|
504
|
+
path.writable?
|
|
505
|
+
path.executable?
|
|
506
|
+
path.absolute?
|
|
507
|
+
path.relative?
|
|
508
|
+
path.empty? # Empty file or empty directory
|
|
509
|
+
|
|
510
|
+
# File operations
|
|
511
|
+
path.read # => String contents
|
|
512
|
+
path.write("content")
|
|
513
|
+
path.append("more")
|
|
514
|
+
path.touch
|
|
515
|
+
path.mkdir
|
|
516
|
+
path.mkdir_p
|
|
517
|
+
path.rm
|
|
518
|
+
path.rm_rf
|
|
519
|
+
path.cp(dest)
|
|
520
|
+
path.mv(dest)
|
|
521
|
+
|
|
522
|
+
# Metadata
|
|
523
|
+
path.basename # => "path"
|
|
524
|
+
path.extname # => ".rb"
|
|
525
|
+
path.dirname # => Path to parent
|
|
526
|
+
path.expand # => Expanded Path
|
|
527
|
+
path.realpath # => Resolved symlinks
|
|
528
|
+
|
|
529
|
+
# Enumeration
|
|
530
|
+
path.children # => Array of Paths
|
|
531
|
+
path.glob("**/*.rb") # => Array of Paths
|
|
532
|
+
path.find { |p| ... } # Recursive find
|
|
533
|
+
|
|
534
|
+
# Conversion
|
|
535
|
+
path.to_s # => "/some/path"
|
|
536
|
+
path.to_str # => "/some/path" (implicit)
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
---
|
|
540
|
+
|
|
541
|
+
## Runtime Context
|
|
542
|
+
|
|
543
|
+
### Detecting Environment
|
|
544
|
+
|
|
545
|
+
```ruby
|
|
546
|
+
def run
|
|
547
|
+
# What environment are we in?
|
|
548
|
+
Devex::Context.env # => "development", "test", "staging", "production"
|
|
549
|
+
Devex::Context.development? # => true/false
|
|
550
|
+
Devex::Context.production? # => true/false
|
|
551
|
+
Devex::Context.safe_env? # => true for dev/test, false for staging/prod
|
|
552
|
+
end
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
Set via `DX_ENV`, `DEVEX_ENV`, `RAILS_ENV`, or `RACK_ENV`.
|
|
556
|
+
|
|
557
|
+
### Detecting Agent Mode
|
|
558
|
+
|
|
559
|
+
When invoked by an AI agent (Claude, etc.), output should be structured and machine-readable:
|
|
560
|
+
|
|
561
|
+
```ruby
|
|
562
|
+
def run
|
|
563
|
+
if Devex::Context.agent_mode?
|
|
564
|
+
# Output JSON, avoid colors, no interactive prompts
|
|
565
|
+
else
|
|
566
|
+
# Rich terminal output okay
|
|
567
|
+
end
|
|
568
|
+
end
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
Agent mode is detected when:
|
|
572
|
+
- `DX_AGENT_MODE=1` environment variable is set
|
|
573
|
+
- stdout/stderr are merged (`2>&1` redirection)
|
|
574
|
+
- Not a TTY and not CI
|
|
575
|
+
|
|
576
|
+
### Detecting Interactive Mode
|
|
577
|
+
|
|
578
|
+
```ruby
|
|
579
|
+
def run
|
|
580
|
+
if Devex::Context.interactive?
|
|
581
|
+
# Can prompt user, show progress bars, etc.
|
|
582
|
+
else
|
|
583
|
+
# Non-interactive: fail or use defaults, no prompts
|
|
584
|
+
end
|
|
585
|
+
end
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
### Detecting CI
|
|
589
|
+
|
|
590
|
+
```ruby
|
|
591
|
+
def run
|
|
592
|
+
if Devex::Context.ci?
|
|
593
|
+
# Running in GitHub Actions, GitLab CI, etc.
|
|
594
|
+
end
|
|
595
|
+
end
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
### Call Tree (Task Invocation Chain)
|
|
599
|
+
|
|
600
|
+
Tools can know if they were invoked from another tool:
|
|
601
|
+
|
|
602
|
+
```ruby
|
|
603
|
+
def run
|
|
604
|
+
Devex::Context.invoked_from_task? # => true if called by another tool
|
|
605
|
+
Devex::Context.invoking_task # => "pre-commit" (immediate parent)
|
|
606
|
+
Devex::Context.root_task # => "pre-commit" (first in chain)
|
|
607
|
+
Devex::Context.call_tree # => ["pre-commit", "lint", "rubocop"]
|
|
608
|
+
end
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
Use case: A `lint` tool might skip certain checks when invoked from `pre-commit` vs directly.
|
|
612
|
+
|
|
613
|
+
### Terminal Detection
|
|
614
|
+
|
|
615
|
+
```ruby
|
|
616
|
+
Devex::Context.terminal? # All three streams are TTYs
|
|
617
|
+
Devex::Context.stdout_tty? # stdout specifically
|
|
618
|
+
Devex::Context.piped? # Data being piped in or out
|
|
619
|
+
Devex::Context.color? # Should we use colors?
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
---
|
|
623
|
+
|
|
624
|
+
## Global Options
|
|
625
|
+
|
|
626
|
+
Tools have access to global flags set by the user:
|
|
627
|
+
|
|
628
|
+
```ruby
|
|
629
|
+
def run
|
|
630
|
+
# Access global options
|
|
631
|
+
global_options[:format] # --format value
|
|
632
|
+
global_options[:verbose] # -v count (0, 1, 2, ...)
|
|
633
|
+
global_options[:quiet] # -q was set
|
|
634
|
+
|
|
635
|
+
# Convenience methods
|
|
636
|
+
verbose? # true if -v was passed
|
|
637
|
+
verbose # verbosity level (0, 1, 2, ...)
|
|
638
|
+
quiet? # true if -q was passed
|
|
639
|
+
|
|
640
|
+
# Effective output format (considers global + tool flags + context)
|
|
641
|
+
output_format # => :text, :json, or :yaml
|
|
642
|
+
end
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
---
|
|
646
|
+
|
|
647
|
+
## Output Patterns
|
|
648
|
+
|
|
649
|
+
### Rule: Never Stack `puts` Calls
|
|
650
|
+
|
|
651
|
+
Bad:
|
|
652
|
+
```ruby
|
|
653
|
+
puts "Header"
|
|
654
|
+
puts "Line 1"
|
|
655
|
+
puts "Line 2"
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
Good:
|
|
659
|
+
```ruby
|
|
660
|
+
$stdout.print Devex.render_template("my_template", data)
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
### Structured Output (JSON/YAML)
|
|
664
|
+
|
|
665
|
+
```ruby
|
|
666
|
+
def run
|
|
667
|
+
data = { status: "ok", count: 42 }
|
|
668
|
+
|
|
669
|
+
case output_format
|
|
670
|
+
when :json, :yaml
|
|
671
|
+
Devex::Output.data(data, format: output_format)
|
|
672
|
+
else
|
|
673
|
+
$stdout.print Devex.render_template("my_template", data)
|
|
674
|
+
end
|
|
675
|
+
end
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
### Using Templates
|
|
679
|
+
|
|
680
|
+
Templates live in `lib/devex/templates/*.erb`:
|
|
681
|
+
|
|
682
|
+
```ruby
|
|
683
|
+
# In your tool:
|
|
684
|
+
$stdout.print Devex.render_template("status", {
|
|
685
|
+
name: "myproject",
|
|
686
|
+
version: "1.0.0",
|
|
687
|
+
healthy: true
|
|
688
|
+
})
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
```erb
|
|
692
|
+
<%# lib/devex/templates/status.erb %>
|
|
693
|
+
<%= heading "Status" %>
|
|
694
|
+
Project: <%= c :emphasis, name %>
|
|
695
|
+
Version: <%= version %>
|
|
696
|
+
Health: <%= healthy ? csym(:success) : csym(:error) %> <%= healthy ? "OK" : "FAILING" %>
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
### Template Helpers
|
|
700
|
+
|
|
701
|
+
Available in all templates:
|
|
702
|
+
|
|
703
|
+
| Helper | Description | Example |
|
|
704
|
+
|--------|-------------|---------|
|
|
705
|
+
| `c(color, text)` | Colorize text | `<%= c :success, "done" %>` |
|
|
706
|
+
| `c(style, color, text)` | Multiple styles | `<%= c :bold, :white, "HEADER" %>` |
|
|
707
|
+
| `sym(name)` | Unicode symbol | `<%= sym :success %>` → ✓ |
|
|
708
|
+
| `csym(name)` | Colored symbol | `<%= csym :error %>` → red ✗ |
|
|
709
|
+
| `heading(text)` | Section heading | `<%= heading "Results" %>` |
|
|
710
|
+
| `muted(text)` | Gray/secondary | `<%= muted "optional info" %>` |
|
|
711
|
+
| `bold(text)` | Bold text | `<%= bold "important" %>` |
|
|
712
|
+
| `hr` | Horizontal rule | `<%= hr %>` |
|
|
713
|
+
|
|
714
|
+
**Colors:** `:success`, `:error`, `:warning`, `:info`, `:header`, `:muted`, `:emphasis`
|
|
715
|
+
|
|
716
|
+
**Symbols:** `:success` (✓), `:error` (✗), `:warning` (⚠), `:info` (ℹ), `:arrow` (→), `:bullet` (•), `:dot` (·)
|
|
717
|
+
|
|
718
|
+
Colors automatically respect `--no-color`. Symbols are always unicode (basic unicode works everywhere).
|
|
719
|
+
|
|
720
|
+
### Streaming Multiple Documents
|
|
721
|
+
|
|
722
|
+
For composed tools outputting multiple results:
|
|
723
|
+
|
|
724
|
+
```ruby
|
|
725
|
+
# YAML stream with proper separators
|
|
726
|
+
Devex::Output.yaml_stream([result1, result2, result3])
|
|
727
|
+
# Outputs: doc1, ---, doc2, ---, doc3, ...
|
|
728
|
+
|
|
729
|
+
# JSON Lines (one object per line)
|
|
730
|
+
Devex::Output.jsonl_stream([result1, result2, result3])
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
---
|
|
734
|
+
|
|
735
|
+
## Error Handling
|
|
736
|
+
|
|
737
|
+
### User Errors
|
|
738
|
+
|
|
739
|
+
```ruby
|
|
740
|
+
def run
|
|
741
|
+
unless File.exist?(filename)
|
|
742
|
+
Devex::Output.error("File not found: #{filename}")
|
|
743
|
+
exit(1)
|
|
744
|
+
end
|
|
745
|
+
end
|
|
746
|
+
```
|
|
747
|
+
|
|
748
|
+
### Structured Errors (Agent Mode)
|
|
749
|
+
|
|
750
|
+
The `Output.error` method automatically adapts to context.
|
|
751
|
+
|
|
752
|
+
### Exit Codes
|
|
753
|
+
|
|
754
|
+
- `0` - Success
|
|
755
|
+
- `1` - General error
|
|
756
|
+
- `2` - Usage/argument error
|
|
757
|
+
|
|
758
|
+
### Command Execution Errors
|
|
759
|
+
|
|
760
|
+
Commands return `Result` objects instead of raising exceptions:
|
|
761
|
+
|
|
762
|
+
```ruby
|
|
763
|
+
result = run "might_fail"
|
|
764
|
+
|
|
765
|
+
if result.failed?
|
|
766
|
+
if result.exception
|
|
767
|
+
# Command failed to start (not found, permission denied)
|
|
768
|
+
Output.error "Command failed to start: #{result.exception.message}"
|
|
769
|
+
else
|
|
770
|
+
# Command ran but returned non-zero
|
|
771
|
+
Output.error "Command failed with exit code #{result.exit_code}"
|
|
772
|
+
end
|
|
773
|
+
exit 1
|
|
774
|
+
end
|
|
775
|
+
```
|
|
776
|
+
|
|
777
|
+
---
|
|
778
|
+
|
|
779
|
+
## Support Library
|
|
780
|
+
|
|
781
|
+
### Core Extensions (Refinements)
|
|
782
|
+
|
|
783
|
+
Enable Ruby refinements for cleaner code:
|
|
784
|
+
|
|
785
|
+
```ruby
|
|
786
|
+
using Devex::Support::CoreExt
|
|
787
|
+
|
|
788
|
+
# String
|
|
789
|
+
"hello".present? # => true
|
|
790
|
+
"".blank? # => true
|
|
791
|
+
"HELLO".underscore # => "hello"
|
|
792
|
+
"hello".titleize # => "Hello"
|
|
793
|
+
|
|
794
|
+
# Array/Hash
|
|
795
|
+
[].blank? # => true
|
|
796
|
+
{ a: 1 }.present? # => true
|
|
797
|
+
|
|
798
|
+
# Enumerable
|
|
799
|
+
[1, 2, 3].average # => 2.0
|
|
800
|
+
[1, 2, 3].sum_by { |x| x * 2 } # => 12
|
|
801
|
+
|
|
802
|
+
# Numeric
|
|
803
|
+
5.clamp(1, 3) # => 3
|
|
804
|
+
5.positive? # => true
|
|
805
|
+
```
|
|
806
|
+
|
|
807
|
+
Or load globally (for tools that prefer it):
|
|
808
|
+
|
|
809
|
+
```ruby
|
|
810
|
+
Devex::Support::Global.load!
|
|
811
|
+
```
|
|
812
|
+
|
|
813
|
+
### ANSI Colors
|
|
814
|
+
|
|
815
|
+
Direct access to terminal colors:
|
|
816
|
+
|
|
817
|
+
```ruby
|
|
818
|
+
Devex::Support::ANSI["Hello", :green]
|
|
819
|
+
Devex::Support::ANSI["Error", :red, :bold]
|
|
820
|
+
Devex::Support::ANSI["Text", :white, bg: :blue]
|
|
821
|
+
|
|
822
|
+
# Check if colors enabled
|
|
823
|
+
Devex::Support::ANSI.enabled?
|
|
824
|
+
Devex::Support::ANSI.disable!
|
|
825
|
+
Devex::Support::ANSI.enable!
|
|
826
|
+
```
|
|
827
|
+
|
|
828
|
+
---
|
|
829
|
+
|
|
830
|
+
## Accessing CLI State
|
|
831
|
+
|
|
832
|
+
```ruby
|
|
833
|
+
def run
|
|
834
|
+
cli.project_root # Path to project root (where .dx.yml or .git is)
|
|
835
|
+
cli.executable_name # "dx"
|
|
836
|
+
end
|
|
837
|
+
```
|
|
838
|
+
|
|
839
|
+
---
|
|
840
|
+
|
|
841
|
+
## Invoking Other Tools
|
|
842
|
+
|
|
843
|
+
```ruby
|
|
844
|
+
def run
|
|
845
|
+
# Via the tool() method (recommended - tracks call tree)
|
|
846
|
+
tool "test"
|
|
847
|
+
tool "lint", "--fix"
|
|
848
|
+
|
|
849
|
+
# Legacy method
|
|
850
|
+
run_tool("test")
|
|
851
|
+
run_tool("lint", "--fix")
|
|
852
|
+
end
|
|
853
|
+
```
|
|
854
|
+
|
|
855
|
+
---
|
|
856
|
+
|
|
857
|
+
## Overriding Built-ins
|
|
858
|
+
|
|
859
|
+
Project tasks override built-ins of the same name:
|
|
860
|
+
|
|
861
|
+
```ruby
|
|
862
|
+
# tools/version.rb - overrides built-in version command
|
|
863
|
+
desc "Custom version display"
|
|
864
|
+
|
|
865
|
+
def run
|
|
866
|
+
# Your custom implementation
|
|
867
|
+
|
|
868
|
+
# Optionally call the built-in:
|
|
869
|
+
builtin.run if builtin
|
|
870
|
+
end
|
|
871
|
+
```
|
|
872
|
+
|
|
873
|
+
---
|
|
874
|
+
|
|
875
|
+
## Testing Considerations
|
|
876
|
+
|
|
877
|
+
### Debug Flags
|
|
878
|
+
|
|
879
|
+
For reproducing issues, users can force context detection:
|
|
880
|
+
|
|
881
|
+
```bash
|
|
882
|
+
dx --dx-agent-mode version # Force agent mode
|
|
883
|
+
dx --dx-no-agent-mode version # Force non-agent mode
|
|
884
|
+
dx --dx-env=production version # Force environment
|
|
885
|
+
dx --dx-force-color version # Force colors on
|
|
886
|
+
dx --dx-no-color version # Force colors off
|
|
887
|
+
```
|
|
888
|
+
|
|
889
|
+
### Programmatic Overrides
|
|
890
|
+
|
|
891
|
+
In tests, use `Context.with_overrides`:
|
|
892
|
+
|
|
893
|
+
```ruby
|
|
894
|
+
Devex::Context.with_overrides(agent_mode: true, color: false) do
|
|
895
|
+
# Test code here
|
|
896
|
+
end
|
|
897
|
+
```
|
|
898
|
+
|
|
899
|
+
---
|
|
900
|
+
|
|
901
|
+
## Complete Example
|
|
902
|
+
|
|
903
|
+
```ruby
|
|
904
|
+
# tools/check.rb
|
|
905
|
+
desc "Run project health checks"
|
|
906
|
+
|
|
907
|
+
long_desc <<~DESC
|
|
908
|
+
Runs various health checks on the project and reports status.
|
|
909
|
+
Use --fix to automatically fix issues where possible.
|
|
910
|
+
DESC
|
|
911
|
+
|
|
912
|
+
flag :fix, "--fix", desc: "Automatically fix issues"
|
|
913
|
+
flag :strict, "--strict", desc: "Fail on warnings"
|
|
914
|
+
|
|
915
|
+
include Devex::Exec
|
|
916
|
+
include Devex::WorkingDirMixin
|
|
917
|
+
|
|
918
|
+
def run
|
|
919
|
+
results = {
|
|
920
|
+
checks: [],
|
|
921
|
+
passed: 0,
|
|
922
|
+
failed: 0,
|
|
923
|
+
warnings: 0
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
# Run tests
|
|
927
|
+
within prj.test do
|
|
928
|
+
result = capture "rspec", "--format", "json"
|
|
929
|
+
if result.success?
|
|
930
|
+
results[:passed] += 1
|
|
931
|
+
results[:checks] << { name: "tests", status: "passed" }
|
|
932
|
+
else
|
|
933
|
+
results[:failed] += 1
|
|
934
|
+
results[:checks] << { name: "tests", status: "failed" }
|
|
935
|
+
end
|
|
936
|
+
end
|
|
937
|
+
|
|
938
|
+
# Run linter (use cmd/cmd? inside def run to avoid shadowing)
|
|
939
|
+
if cmd? "which", "rubocop"
|
|
940
|
+
result = cmd "rubocop", *(fix ? ["--autocorrect"] : [])
|
|
941
|
+
status = result.success? ? "passed" : "failed"
|
|
942
|
+
results[:checks] << { name: "lint", status: status }
|
|
943
|
+
result.success? ? results[:passed] += 1 : results[:failed] += 1
|
|
944
|
+
end
|
|
945
|
+
|
|
946
|
+
# Output based on format
|
|
947
|
+
case output_format
|
|
948
|
+
when :json, :yaml
|
|
949
|
+
Devex::Output.data(results, format: output_format)
|
|
950
|
+
else
|
|
951
|
+
$stdout.print Devex.render_template("check_results", results)
|
|
952
|
+
end
|
|
953
|
+
|
|
954
|
+
# Exit code
|
|
955
|
+
exit(1) if results[:failed] > 0
|
|
956
|
+
exit(1) if strict && results[:warnings] > 0
|
|
957
|
+
end
|
|
958
|
+
```
|
|
959
|
+
|
|
960
|
+
```erb
|
|
961
|
+
<%# lib/devex/templates/check_results.erb %>
|
|
962
|
+
<%= heading "Health Check Results" %>
|
|
963
|
+
|
|
964
|
+
<% checks.each do |check| -%>
|
|
965
|
+
<%= csym(check[:status] == "passed" ? :success : :error) %> <%= check[:name] %>
|
|
966
|
+
<% end -%>
|
|
967
|
+
|
|
968
|
+
<%= muted "#{passed} passed, #{failed} failed, #{warnings} warnings" %>
|
|
969
|
+
```
|
|
970
|
+
|
|
971
|
+
---
|
|
972
|
+
|
|
973
|
+
## Summary of Available Interfaces
|
|
974
|
+
|
|
975
|
+
| Interface | Purpose |
|
|
976
|
+
|-----------|---------|
|
|
977
|
+
| **Context** | |
|
|
978
|
+
| `Devex::Context.*` | Runtime detection (agent, CI, env, call tree) |
|
|
979
|
+
| `Devex::Dirs.*` | Core directories (invoked, project, dest) |
|
|
980
|
+
| `Devex::ProjectPaths` | Lazy project path resolution |
|
|
981
|
+
| `Devex::WorkingDirMixin` | Working directory context |
|
|
982
|
+
| **Execution** | |
|
|
983
|
+
| `Devex::Exec` | Command execution (run, capture, spawn, etc.) |
|
|
984
|
+
| `Devex::Exec::Result` | Command result with monad operations |
|
|
985
|
+
| `Devex::Exec::Controller` | Background process management |
|
|
986
|
+
| **Output** | |
|
|
987
|
+
| `Devex::Output.*` | Styled output, structured data |
|
|
988
|
+
| `Devex.render_template(name, locals)` | Render ERB template |
|
|
989
|
+
| **Support** | |
|
|
990
|
+
| `Devex::Support::Path` | Immutable path operations |
|
|
991
|
+
| `Devex::Support::ANSI` | Terminal colors |
|
|
992
|
+
| `Devex::Support::CoreExt` | Ruby refinements |
|
|
993
|
+
| **Tool Runtime** | |
|
|
994
|
+
| `output_format` | Effective format (:text, :json, :yaml) |
|
|
995
|
+
| `verbose?`, `quiet?` | Global verbosity flags |
|
|
996
|
+
| `cli.project_root` | Project root path |
|
|
997
|
+
| `tool(name, *args)` | Invoke another tool |
|
|
998
|
+
| `builtin` | Access overridden built-in |
|
|
999
|
+
| `options` | Tool-specific flag/arg values |
|
|
1000
|
+
| `global_options` | Global flag values |
|