asgard 0.1.2 → 0.3.0
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 +4 -4
- data/.github/workflows/deploy-github-pages.yml +52 -0
- data/.loki +7 -9
- data/.rubocop.yml +157 -0
- data/CHANGELOG.md +93 -2
- data/CLAUDE.md +19 -9
- data/README.md +110 -58
- data/Rakefile +83 -4
- data/docs/api.md +204 -0
- data/docs/assets/css/custom.css +93 -0
- data/docs/assets/images/asgard.jpg +0 -0
- data/docs/changelog.md +104 -0
- data/docs/dependencies.md +221 -0
- data/docs/environment.md +129 -0
- data/docs/examples.md +140 -0
- data/docs/getting-started.md +179 -0
- data/docs/helpers.md +178 -0
- data/docs/index.md +85 -0
- data/docs/options.md +180 -0
- data/docs/shell.md +208 -0
- data/docs/subcommands.md +181 -0
- data/docs/task-files.md +407 -0
- data/docs/tasks.md +286 -0
- data/docs/variables.md +338 -0
- data/examples/.env +4 -0
- data/examples/.loki +24 -2
- data/examples/concurrent.loki +58 -0
- data/examples/db_subcommands.loki +3 -3
- data/examples/env_usage.loki +27 -0
- data/examples/kitchen_sink.loki +48 -15
- data/examples/server_subcommands.loki +3 -3
- data/examples/subdir/.loki +12 -0
- data/examples/subdir/import_demo.loki +14 -0
- data/examples/subdir/import_up_demo.loki +18 -0
- data/lib/asgard/base.rb +159 -54
- data/lib/asgard/kernel_methods.rb +77 -0
- data/lib/asgard/shell.rb +9 -6
- data/lib/asgard/tasks.rb +0 -5
- data/lib/asgard/version.rb +1 -1
- data/lib/asgard.rb +7 -18
- data/mkdocs.yml +164 -0
- metadata +32 -4
data/docs/task-files.md
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
# Task Files
|
|
2
|
+
|
|
3
|
+
Asgard uses a convention-based file discovery system. A hidden `.loki` file marks the project root. Everything else — loading sibling files, shared task libraries, monorepo-wide tasks — is controlled explicitly from inside your `.loki` file using the `import` and `import_up` Kernel methods.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## The `.loki` Root Marker
|
|
8
|
+
|
|
9
|
+
When you run `asgard`, it searches for a `.loki` file starting in the current working directory and walking upward through parent directories until it finds one or reaches the filesystem root. The first `.loki` file found marks the project root and is the only file Asgard loads automatically.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
myproject/
|
|
13
|
+
.loki ← found regardless of which subdirectory you're in
|
|
14
|
+
src/
|
|
15
|
+
app/
|
|
16
|
+
# asgard still works from here
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The `.loki` file can be completely empty — its presence alone marks the project root. It can also contain task definitions, `import` calls, or any valid Ruby.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Loading Files with `import`
|
|
24
|
+
|
|
25
|
+
`import` is a Kernel method available everywhere in Ruby — at the top level of `.loki` files, inside class bodies, and inside task method bodies. It loads `.loki` files with `require`-like idempotency: a file is loaded at most once per process, no matter how many times `import` is called with the same path.
|
|
26
|
+
|
|
27
|
+
### Single file by absolute path
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
import "/home/shared/gem_tasks.loki"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Single file by relative path
|
|
34
|
+
|
|
35
|
+
Relative paths are resolved relative to the **caller's file location**, like `require_relative`:
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
# .loki — relative to this file's directory
|
|
39
|
+
import "build.loki"
|
|
40
|
+
import "../shared/gem_tasks.loki"
|
|
41
|
+
import "tasks/ci.loki"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### All files in the same directory (glob)
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
import "*.loki" # all *.loki files in the same directory as the calling file
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`*.loki` never matches `.loki` (the dotfile entry point) — Ruby's `Dir.glob` excludes dotfiles from `*` patterns by default.
|
|
51
|
+
|
|
52
|
+
### All files recursively (recursive glob)
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
import "**/*.loki" # every .loki file in this directory and all subdirectories
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Specific named files
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
import "gem_tasks.loki"
|
|
62
|
+
import "ci_tasks.loki"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Combining patterns
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
# .loki
|
|
69
|
+
import "*.loki" # load all siblings
|
|
70
|
+
import "../shared/*.loki" # load a parent-level shared library
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Typical `.loki` entry point
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
# .loki
|
|
77
|
+
import "*.loki" # load all sibling task files
|
|
78
|
+
|
|
79
|
+
class Tasks
|
|
80
|
+
# any top-level task definitions or overrides
|
|
81
|
+
end
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Return value
|
|
85
|
+
|
|
86
|
+
`import` returns `true` if at least one file was newly loaded, `false` if all files were already loaded or no glob pattern matched any file. If a specific (non-glob) file does not exist, `import` raises `LoadError`.
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
import("gem_tasks.loki") ? "loaded now" : "already loaded"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Finding Files with `loki_up`
|
|
95
|
+
|
|
96
|
+
`loki_up(name = ".loki")` searches `Dir.pwd` and each ancestor directory for a file with the given name, returning its absolute path or `nil`. It does **not** load the file — it only finds it.
|
|
97
|
+
|
|
98
|
+
Despite the name, `loki_up` is not limited to `.loki` files — it will locate any file by name. This makes it useful for finding shared config files, `.env` files, or any other resource that lives somewhere up the directory tree:
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
loki_up # finds .loki (the project root marker)
|
|
102
|
+
loki_up("gem_tasks.loki") # finds gem_tasks.loki in CWD or any ancestor
|
|
103
|
+
loki_up(".env") # finds the nearest .env file up the tree
|
|
104
|
+
loki_up("VERSION") # finds a VERSION file in CWD or any ancestor
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Use `loki_up` when you need the path for other purposes, or to check whether a file exists before deciding to load it:
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
if (path = loki_up("gem_tasks.loki"))
|
|
111
|
+
import path
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Pass the located .env to dotenv — works from any subdirectory
|
|
115
|
+
dotenv loki_up(".env") || ".env"
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`loki_up` accepts exact filenames only. Glob patterns are not expanded by `loki_up` — use `import_up` for glob-aware ancestor search.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Loading Files Found up the Tree with `import_up`
|
|
123
|
+
|
|
124
|
+
`import_up(name = ".loki")` combines `loki_up` and `import` into a single call. It finds the file (or files) up the ancestor chain and loads them.
|
|
125
|
+
|
|
126
|
+
### Exact filename
|
|
127
|
+
|
|
128
|
+
```ruby
|
|
129
|
+
import_up "gem_tasks.loki"
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Walks up from `Dir.pwd` until it finds `gem_tasks.loki`, then loads it. Returns `false` if not found anywhere.
|
|
133
|
+
|
|
134
|
+
### Glob pattern
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
import_up "*.loki"
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Walks up from `Dir.pwd` and stops at the **first ancestor directory** that contains any `*.loki` files, loading all of them. It does not aggregate matches from multiple levels — it loads only the nearest match, then stops.
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
~/sandbox/
|
|
144
|
+
gem_tasks.loki ← loaded by import_up("*.loki") from ~/sandbox/myproject/sub/
|
|
145
|
+
ci_tasks.loki ← also loaded — same directory as the first match
|
|
146
|
+
myproject/
|
|
147
|
+
.loki
|
|
148
|
+
sub/
|
|
149
|
+
# Dir.pwd here; import_up("*.loki") finds ~/sandbox/*.loki files
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Return value
|
|
153
|
+
|
|
154
|
+
Returns `true` if any file was newly loaded, `false` if the file was not found or was already loaded.
|
|
155
|
+
|
|
156
|
+
### Conditional load
|
|
157
|
+
|
|
158
|
+
Since `import_up` returns `false` when a file is not found (rather than raising), it composes naturally with `||`:
|
|
159
|
+
|
|
160
|
+
```ruby
|
|
161
|
+
import_up("project_tasks.loki") || import_up("gem_tasks.loki")
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Idempotency
|
|
167
|
+
|
|
168
|
+
Both `import` and `import_up` track loaded files in Ruby's `$LOADED_FEATURES`. A second call with the same path is a no-op and returns `false`. This means:
|
|
169
|
+
|
|
170
|
+
- You can call `import "*.loki"` from both `.loki` and a shared task file without double-loading.
|
|
171
|
+
- `import_up("gem_tasks.loki")` from two different projects in the same process each load their nearest match once.
|
|
172
|
+
- Swapping `require` for `import` in a `.loki` file gives the same once-per-process guarantee.
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Verbose and Debug Feedback
|
|
177
|
+
|
|
178
|
+
`import` and `import_up` emit diagnostic messages to stderr when the `verbose?` or `debug?` flags are active (set via `--verbose` or `--debug` on the CLI, or by setting `$VERBOSE`/`$DEBUG` directly):
|
|
179
|
+
|
|
180
|
+
| Flag | `import` output | `import_up` output |
|
|
181
|
+
|---|---|---|
|
|
182
|
+
| `--verbose` | Prints each file path as it is loaded | Prints `name → /full/path` when found |
|
|
183
|
+
| `--debug` | Same as verbose, plus prints a skip message for already-loaded files | Same as verbose, plus prints `name not found` when the search comes up empty |
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
$ asgard --verbose build
|
|
187
|
+
import: /home/user/myproject/build.loki
|
|
188
|
+
import: /home/user/myproject/test.loki
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Loading Patterns
|
|
194
|
+
|
|
195
|
+
### Single-file project
|
|
196
|
+
|
|
197
|
+
All tasks in `.loki`, nothing else:
|
|
198
|
+
|
|
199
|
+
```ruby
|
|
200
|
+
# .loki
|
|
201
|
+
class Tasks
|
|
202
|
+
@@app ||= "myapp".freeze
|
|
203
|
+
|
|
204
|
+
desc "Compile the project"
|
|
205
|
+
def build = sh "rake build"
|
|
206
|
+
|
|
207
|
+
desc "Run the test suite"
|
|
208
|
+
def test = sh "rake test"
|
|
209
|
+
|
|
210
|
+
desc "Build and push the gem"
|
|
211
|
+
def release = sh "gem push pkg/#{@@app}-*.gem"
|
|
212
|
+
end
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Multi-file project
|
|
216
|
+
|
|
217
|
+
Split tasks across files by concern. Load them all from `.loki` with a glob:
|
|
218
|
+
|
|
219
|
+
```
|
|
220
|
+
myproject/
|
|
221
|
+
.loki ← entry point; imports siblings
|
|
222
|
+
build.loki ← build tasks
|
|
223
|
+
deploy.loki ← deploy tasks
|
|
224
|
+
test.loki ← test tasks
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
```ruby
|
|
228
|
+
# .loki
|
|
229
|
+
import "*.loki"
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
```ruby
|
|
233
|
+
# build.loki
|
|
234
|
+
class Tasks
|
|
235
|
+
desc "Compile the project"
|
|
236
|
+
def build = sh "rake build"
|
|
237
|
+
end
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
```ruby
|
|
241
|
+
# test.loki
|
|
242
|
+
class Tasks
|
|
243
|
+
depends_on :build
|
|
244
|
+
desc "Run the test suite"
|
|
245
|
+
def test = sh "bundle exec rake test"
|
|
246
|
+
end
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
```ruby
|
|
250
|
+
# deploy.loki
|
|
251
|
+
class Tasks
|
|
252
|
+
depends_on :test
|
|
253
|
+
desc "Deploy to production"
|
|
254
|
+
def deploy = sh "cap production deploy"
|
|
255
|
+
end
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Files loaded via glob are sorted alphabetically by `Dir.glob`, so `build.loki` loads before `test.loki`. Tasks defined in earlier files are available to later files via `depends_on`.
|
|
259
|
+
|
|
260
|
+
### Controlled load order
|
|
261
|
+
|
|
262
|
+
When alphabetical order does not match your dependency order, import explicitly:
|
|
263
|
+
|
|
264
|
+
```ruby
|
|
265
|
+
# .loki
|
|
266
|
+
import "infra.loki" # must be first
|
|
267
|
+
import "build.loki" # depends on infra
|
|
268
|
+
import "deploy.loki" # depends on build
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Shared task library in a monorepo
|
|
272
|
+
|
|
273
|
+
Place shared tasks in a parent directory and load them from any sub-project:
|
|
274
|
+
|
|
275
|
+
```
|
|
276
|
+
~/sandbox/
|
|
277
|
+
gem_tasks.loki ← shared: build, install, release tasks for any gem
|
|
278
|
+
myproject/
|
|
279
|
+
.loki ← loads gem_tasks.loki via import_up
|
|
280
|
+
other_project/
|
|
281
|
+
.loki ← also loads gem_tasks.loki via import_up
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
```ruby
|
|
285
|
+
# myproject/.loki
|
|
286
|
+
import_up "gem_tasks.loki" # finds ~/sandbox/gem_tasks.loki
|
|
287
|
+
|
|
288
|
+
class Tasks
|
|
289
|
+
# project-specific overrides here
|
|
290
|
+
end
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Conditional shared library
|
|
294
|
+
|
|
295
|
+
```ruby
|
|
296
|
+
# .loki
|
|
297
|
+
import_up("ci_tasks.loki") || import_up("gem_tasks.loki")
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Loads `ci_tasks.loki` if found up the tree, otherwise falls back to `gem_tasks.loki`.
|
|
301
|
+
|
|
302
|
+
### Subcommand classes across files
|
|
303
|
+
|
|
304
|
+
Define subcommand classes in separate files and register them in `.loki`:
|
|
305
|
+
|
|
306
|
+
```
|
|
307
|
+
myproject/
|
|
308
|
+
.loki
|
|
309
|
+
db.loki
|
|
310
|
+
server.loki
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
```ruby
|
|
314
|
+
# db.loki
|
|
315
|
+
class DBCommands < Tasks
|
|
316
|
+
desc "Run migrations"
|
|
317
|
+
def migrate = sh "rails db:migrate"
|
|
318
|
+
end
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
```ruby
|
|
322
|
+
# server.loki
|
|
323
|
+
class ServerCommands < Tasks
|
|
324
|
+
desc "Start the server"
|
|
325
|
+
def start = sh "rails server"
|
|
326
|
+
end
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
```ruby
|
|
330
|
+
# .loki
|
|
331
|
+
import "*.loki" # db.loki and server.loki load first
|
|
332
|
+
|
|
333
|
+
class Tasks
|
|
334
|
+
desc "db SUBCOMMAND", "Manage the database"; subcommand "db", DBCommands
|
|
335
|
+
desc "server SUBCOMMAND", "Manage the server"; subcommand "server", ServerCommands
|
|
336
|
+
end
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Subcommand classes are available in `.loki` because siblings loaded via `import "*.loki"` execute before `.loki`'s own class body.
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
## Task Name Overloading
|
|
344
|
+
|
|
345
|
+
Because all `*.loki` files reopen the same `class Tasks`, two files can define a method with the same name. Ruby's class reopening semantics apply: the last definition loaded wins, silently replacing the earlier one.
|
|
346
|
+
|
|
347
|
+
Three things are overwritten when a task name is reused:
|
|
348
|
+
|
|
349
|
+
| What | Effect |
|
|
350
|
+
|---|---|
|
|
351
|
+
| `def method_name` | The Ruby method body — the earlier implementation is gone |
|
|
352
|
+
| `desc` metadata | Thor registers the new usage/description string, discarding the old one |
|
|
353
|
+
| `depends_on` stages | `method_added` captures the pending deps for the new definition; the earlier dep chain is replaced |
|
|
354
|
+
|
|
355
|
+
**Accidental overloading** is a silent bug. Keep task names unique across files.
|
|
356
|
+
|
|
357
|
+
!!! warning
|
|
358
|
+
There is no runtime error when a task is overloaded. If a task is not behaving as expected, check whether another `.loki` file defines the same method name and loads after it.
|
|
359
|
+
|
|
360
|
+
**Intentional overloading** lets you extend a task defined in an earlier file using `alias_method`:
|
|
361
|
+
|
|
362
|
+
```ruby
|
|
363
|
+
# build.loki (loaded first)
|
|
364
|
+
class Tasks
|
|
365
|
+
desc "Compile the project"
|
|
366
|
+
def build = sh "rake build"
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# postbuild.loki (loaded after build.loki, alphabetically)
|
|
370
|
+
class Tasks
|
|
371
|
+
no_commands { alias_method :_build_original, :build }
|
|
372
|
+
|
|
373
|
+
desc "Compile the project and copy assets"
|
|
374
|
+
def build
|
|
375
|
+
_build_original
|
|
376
|
+
sh "cp -r dist/ public/"
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
!!! warning "Prefer `depends_on` over intentional overloading"
|
|
382
|
+
Using `alias_method` to bolt post-task behaviour onto an existing task is fragile and load-order dependent. The idiomatic alternative is `depends_on`:
|
|
383
|
+
|
|
384
|
+
```ruby
|
|
385
|
+
class Tasks
|
|
386
|
+
desc "Compile the project"
|
|
387
|
+
def build = sh "rake build"
|
|
388
|
+
|
|
389
|
+
desc "Copy build output to public/"
|
|
390
|
+
def copy_assets = sh "cp -r dist/ public/"
|
|
391
|
+
|
|
392
|
+
depends_on :build, :copy_assets
|
|
393
|
+
desc "Compile and copy assets"
|
|
394
|
+
def build_all; end
|
|
395
|
+
end
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## Summary of Loading Rules
|
|
401
|
+
|
|
402
|
+
| Method | Finds? | Loads? | Glob? | Ancestor search? |
|
|
403
|
+
|---|---|---|---|---|
|
|
404
|
+
| `loki_up(name)` | Yes | No | No | Yes |
|
|
405
|
+
| `import(path)` | No | Yes | Yes | No |
|
|
406
|
+
| `import_up(name)` | Yes | Yes | Yes | Yes |
|
|
407
|
+
| Asgard's `run!` | Yes | `.loki` only | No | Yes |
|
data/docs/tasks.md
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
# Defining Tasks
|
|
2
|
+
|
|
3
|
+
Every task is a public method inside `class Tasks`. Asgard pre-defines `Tasks` as a subclass of `Asgard::Base` (which is itself a Thor subclass), so your `.loki` files just reopen the class and add methods. The full Thor DSL is available everywhere.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Basic Task
|
|
8
|
+
|
|
9
|
+
A task with no parameters and no options:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
class Tasks
|
|
13
|
+
desc "Say hello"
|
|
14
|
+
def hello = puts "Hello, World!"
|
|
15
|
+
end
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`desc` accepts either one or two strings. With one argument, the description is shown in `asgard help` and the usage string defaults to the method name. Pass two arguments when the usage string needs to document parameters — `desc "greet NAME", "Greet NAME by name"`.
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
asgard hello
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Positional Parameter with Default
|
|
27
|
+
|
|
28
|
+
Positional parameters are declared directly in the method signature. Document them in the `desc` usage string (uppercase by convention):
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
class Tasks
|
|
32
|
+
desc "greet NAME", "Greet NAME; omit NAME to greet the world"
|
|
33
|
+
def greet(name = "World")
|
|
34
|
+
puts "Hello, #{name}!"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
asgard greet # Hello, World!
|
|
41
|
+
asgard greet Alice # Hello, Alice!
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Named Options
|
|
47
|
+
|
|
48
|
+
Use `method_option` (alias: `option`) for named flags. Access them inside the method via `options[:name]`.
|
|
49
|
+
|
|
50
|
+
### All Five Option Types
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
class Tasks
|
|
54
|
+
desc "Compile the project"
|
|
55
|
+
option :output, aliases: "-o", type: :string, default: "dist/", desc: "Output directory"
|
|
56
|
+
option :verbose, aliases: "-v", type: :boolean, default: false, desc: "Enable verbose output"
|
|
57
|
+
option :jobs, aliases: "-j", type: :numeric, default: 1, desc: "Number of parallel jobs"
|
|
58
|
+
option :tags, type: :array, desc: "Build tags to apply"
|
|
59
|
+
option :defines, type: :hash, desc: "Preprocessor defines (KEY:VALUE)"
|
|
60
|
+
def compile
|
|
61
|
+
puts "Compiling → #{options[:output]} with #{options[:jobs]} job(s)"
|
|
62
|
+
puts "Tags: #{options[:tags].join(', ')}" if options[:tags]
|
|
63
|
+
puts "Defines: #{options[:defines]}" if options[:defines]
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Option Types Reference
|
|
69
|
+
|
|
70
|
+
| Type | CLI Example | Ruby Value |
|
|
71
|
+
|---|---|---|
|
|
72
|
+
| `:string` | `--output dist/` | `"dist/"` |
|
|
73
|
+
| `:boolean` | `--verbose` / `--no-verbose` | `true` / `false` |
|
|
74
|
+
| `:numeric` | `--jobs 4` | `4` |
|
|
75
|
+
| `:array` | `--tags foo bar baz` | `["foo", "bar", "baz"]` |
|
|
76
|
+
| `:hash` | `--defines KEY:val FOO:bar` | `{"KEY"=>"val", "FOO"=>"bar"}` |
|
|
77
|
+
|
|
78
|
+
### Common Option Keys
|
|
79
|
+
|
|
80
|
+
| Key | Description |
|
|
81
|
+
|---|---|
|
|
82
|
+
| `aliases` | Short-form flag, e.g. `"-o"` |
|
|
83
|
+
| `type` | `:string`, `:boolean`, `:numeric`, `:array`, or `:hash` |
|
|
84
|
+
| `default` | Value used when the flag is omitted |
|
|
85
|
+
| `required` | If `true`, Thor raises an error when the flag is missing |
|
|
86
|
+
| `desc` | One-line description shown in help |
|
|
87
|
+
| `enum` | Array of allowed values; Thor validates automatically |
|
|
88
|
+
| `banner` | Placeholder shown in help for the value slot, e.g. `"SECONDS"` |
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Required Option
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
class Tasks
|
|
96
|
+
desc "deploy ENV", "Deploy to ENV"
|
|
97
|
+
option :strategy,
|
|
98
|
+
type: :string,
|
|
99
|
+
required: true,
|
|
100
|
+
enum: %w[blue-green rolling canary],
|
|
101
|
+
desc: "Deployment strategy"
|
|
102
|
+
def deploy(env = "staging")
|
|
103
|
+
sh "cap #{env} deploy --strategy #{options[:strategy]}"
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
asgard deploy # Error: required option '--strategy' is missing
|
|
110
|
+
asgard deploy --strategy rolling
|
|
111
|
+
asgard deploy production --strategy blue-green
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Enum Validation
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
class Tasks
|
|
120
|
+
desc "Build the project"
|
|
121
|
+
option :env,
|
|
122
|
+
type: :string,
|
|
123
|
+
default: "development",
|
|
124
|
+
enum: %w[development staging production],
|
|
125
|
+
desc: "Target environment"
|
|
126
|
+
def build
|
|
127
|
+
sh "rake build ENV=#{options[:env]}"
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Thor validates the value against the enum and shows a helpful error if it doesn't match.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Banner
|
|
137
|
+
|
|
138
|
+
`banner` replaces the default `VALUE` placeholder in help output with a more descriptive name:
|
|
139
|
+
|
|
140
|
+
```ruby
|
|
141
|
+
class Tasks
|
|
142
|
+
desc "Wait for a service to become available"
|
|
143
|
+
option :timeout, type: :numeric, default: 30, banner: "SECONDS", desc: "Give up after SECONDS"
|
|
144
|
+
def wait
|
|
145
|
+
sh "wait-for-it --timeout #{options[:timeout]}"
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Help output shows: `[--timeout=SECONDS]` instead of `[--timeout=VALUE]`.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Extended Description
|
|
155
|
+
|
|
156
|
+
`long_desc` provides detailed help shown by `asgard help <task>`. Use `\x5` at the start of a line to force a line break within the wrapped text (a Thor convention):
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
class Tasks
|
|
160
|
+
long_desc <<~DESC
|
|
161
|
+
Generates a project report covering test coverage, lint results,
|
|
162
|
+
and a dependency audit.
|
|
163
|
+
|
|
164
|
+
Pass --format to control output style. Use --since to scope the
|
|
165
|
+
report to changes after a given date.
|
|
166
|
+
|
|
167
|
+
Examples:\x5
|
|
168
|
+
asgard report --format html --since 2024-01-01\x5
|
|
169
|
+
asgard report --format json --output report.json\x5
|
|
170
|
+
asgard report --format text
|
|
171
|
+
DESC
|
|
172
|
+
desc "Generate a project report"
|
|
173
|
+
option :format, type: :string, default: "text", enum: %w[text html json], desc: "Output format"
|
|
174
|
+
option :since, type: :string, banner: "DATE", desc: "Limit to changes after DATE"
|
|
175
|
+
def report
|
|
176
|
+
sh "generate-report --format #{options[:format]}"
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
!!! tip
|
|
182
|
+
`desc` and `depends_on` are independent of each other — either can come first, but both must appear before the `def`.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Default Task
|
|
187
|
+
|
|
188
|
+
`default_task` declares which command runs when `asgard` is invoked with no arguments:
|
|
189
|
+
|
|
190
|
+
```ruby
|
|
191
|
+
class Tasks
|
|
192
|
+
default_task :greet
|
|
193
|
+
|
|
194
|
+
desc "Say hello (runs by default)"
|
|
195
|
+
def greet
|
|
196
|
+
puts "Hello from Asgard!"
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
asgard # same as: asgard greet
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Without `default_task`, running `asgard` with no arguments displays the help message.
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Command Aliases
|
|
210
|
+
|
|
211
|
+
`map` creates short aliases for existing tasks:
|
|
212
|
+
|
|
213
|
+
```ruby
|
|
214
|
+
class Tasks
|
|
215
|
+
map "-v" => "version"
|
|
216
|
+
map "--v" => "version"
|
|
217
|
+
map "t" => "test"
|
|
218
|
+
map "b" => "build"
|
|
219
|
+
|
|
220
|
+
desc "Print the version"
|
|
221
|
+
def version = puts Asgard::VERSION
|
|
222
|
+
|
|
223
|
+
desc "Run tests"
|
|
224
|
+
def test = sh "bundle exec rake test"
|
|
225
|
+
|
|
226
|
+
desc "Build the gem"
|
|
227
|
+
def build = sh "bundle exec rake build"
|
|
228
|
+
end
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
asgard t # same as: asgard test
|
|
233
|
+
asgard b # same as: asgard build
|
|
234
|
+
asgard -v # same as: asgard version (note: --version is the built-in flag)
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## Formal Argument Declaration
|
|
240
|
+
|
|
241
|
+
`argument` provides rich positional-parameter metadata including type checking, enums, and help text.
|
|
242
|
+
|
|
243
|
+
!!! warning "Class-level scope"
|
|
244
|
+
`argument` is a **class-level declaration** that applies to **every task in the class**, not just the one that follows it. It is best suited for single-command CLIs or when every task in the file genuinely shares the same positional input. In multi-task files, prefer method signature parameters instead.
|
|
245
|
+
|
|
246
|
+
```ruby
|
|
247
|
+
class Tasks
|
|
248
|
+
argument :name,
|
|
249
|
+
type: :string,
|
|
250
|
+
default: "World",
|
|
251
|
+
desc: "Name to greet"
|
|
252
|
+
|
|
253
|
+
desc "hello NAME", "Say hello to NAME"
|
|
254
|
+
def hello = puts "Hello, #{name}!"
|
|
255
|
+
end
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
For most multi-task `.loki` files, the simpler positional default pattern is safer:
|
|
259
|
+
|
|
260
|
+
```ruby
|
|
261
|
+
def hello(name = "World") = puts "Hello, #{name}!"
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## No Commands Block
|
|
267
|
+
|
|
268
|
+
`no_commands` marks a block of methods as public helpers that are excluded from the CLI and `--help` output. They are callable from any task in the same class:
|
|
269
|
+
|
|
270
|
+
```ruby
|
|
271
|
+
class Tasks
|
|
272
|
+
desc "Compile the project"
|
|
273
|
+
def build
|
|
274
|
+
puts "Revision: #{current_sha}"
|
|
275
|
+
sh "rake build"
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
no_commands do
|
|
279
|
+
def current_sha
|
|
280
|
+
`git rev-parse --short HEAD`.strip
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
See [Helper Methods](helpers.md) for the full guide on helpers, `private`, and cross-file sharing.
|