asgard 0.2.0 → 0.3.1
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/.loki +13 -26
- data/.rubocop.yml +157 -0
- data/CHANGELOG.md +89 -11
- data/CLAUDE.md +19 -9
- data/README.md +161 -54
- data/docs/api.md +91 -15
- data/docs/changelog.md +12 -0
- data/docs/dependencies.md +25 -25
- data/docs/environment.md +30 -14
- data/docs/examples.md +3 -3
- data/docs/getting-started.md +5 -6
- data/docs/helpers.md +117 -9
- data/docs/index.md +6 -6
- data/docs/options.md +42 -8
- data/docs/shell.md +11 -11
- data/docs/subcommands.md +9 -9
- data/docs/task-files.md +266 -113
- data/docs/tasks.md +17 -15
- data/docs/variables.md +267 -51
- data/examples/.env +4 -0
- data/examples/.loki +24 -2
- data/examples/concurrent.loki +5 -5
- 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 +33 -24
- data/examples/subdir/.loki +12 -0
- data/examples/subdir/import_demo.loki +14 -0
- data/examples/subdir/import_up_demo.loki +18 -0
- data/gem_tasks.loki +35 -0
- data/lib/asgard/base.rb +158 -80
- data/lib/asgard/kernel_methods.rb +79 -0
- data/lib/asgard/shell.rb +8 -7
- data/lib/asgard/tasks.rb +12 -17
- data/lib/asgard/version.rb +1 -1
- data/lib/asgard.rb +6 -18
- data/quality.loki +75 -0
- metadata +15 -5
- data/Rakefile +0 -22
data/README.md
CHANGED
|
@@ -17,12 +17,13 @@
|
|
|
17
17
|
- <strong>Task Dependencies</strong> — sequential, parallel, and mixed dependency graphs via <code>depends_on</code><br>
|
|
18
18
|
- <strong>Concurrent Execution</strong> — parallel task groups run in native Ruby threads<br>
|
|
19
19
|
- <strong>Subcommands</strong> — group related tasks under a named namespace<br>
|
|
20
|
-
- <strong>Variables</strong> —
|
|
20
|
+
- <strong>Variables</strong> — shared configuration via Ruby class variables (<code>@@name</code>), visible across all tasks and subcommands<br>
|
|
21
|
+
- <strong>`helper` DSL</strong> — define a method once, available in both class-level DSL calls (<code>header</code>) and inside task instance methods<br>
|
|
21
22
|
- <strong>Shell Helpers</strong> — <code>sh</code> for any shell command or heredoc; <code>shebang</code> for polyglot scripts<br>
|
|
22
23
|
- <strong>Dotenv Support</strong> — load <code>.env</code> files into the environment with <code>dotenv</code><br>
|
|
23
24
|
- <strong>Auto-Discovery</strong> — <code>.loki</code> root marker searched from CWD upward through parent directories<br>
|
|
24
|
-
- <strong>Multi-File Tasks</strong> — split tasks across <code>*.loki</code> files, loaded
|
|
25
|
-
- <strong>Built-in Flags</strong> — <code>--
|
|
25
|
+
- <strong>Multi-File Tasks</strong> — split tasks across <code>*.loki</code> files, loaded via <code>import</code> from your <code>.loki</code><br>
|
|
26
|
+
- <strong>Built-in Flags</strong> — <code>--debug</code>, <code>--verbose</code>, and <code>--version</code> built-in class options; header/footer DSL for static help text<br>
|
|
26
27
|
</td>
|
|
27
28
|
</tr>
|
|
28
29
|
</table>
|
|
@@ -51,8 +52,8 @@ Every `.loki` file defines tasks as methods inside `class Tasks`. The `Tasks` cl
|
|
|
51
52
|
|
|
52
53
|
```ruby
|
|
53
54
|
class Tasks
|
|
54
|
-
desc "
|
|
55
|
-
def hello =
|
|
55
|
+
desc "Say hello"
|
|
56
|
+
def hello = puts "Hello, World!"
|
|
56
57
|
end
|
|
57
58
|
```
|
|
58
59
|
|
|
@@ -67,7 +68,7 @@ Declare positional parameters directly in the method signature. Document them in
|
|
|
67
68
|
```ruby
|
|
68
69
|
class Tasks
|
|
69
70
|
desc "hello NAME", "Say hello to NAME"
|
|
70
|
-
def hello(name = "World") =
|
|
71
|
+
def hello(name = "World") = puts "Hello, #{name}!"
|
|
71
72
|
end
|
|
72
73
|
```
|
|
73
74
|
|
|
@@ -88,7 +89,7 @@ class Tasks
|
|
|
88
89
|
desc: "Name to greet"
|
|
89
90
|
|
|
90
91
|
desc "hello NAME", "Say hello to NAME"
|
|
91
|
-
def hello =
|
|
92
|
+
def hello = puts "Hello, #{name}!"
|
|
92
93
|
end
|
|
93
94
|
```
|
|
94
95
|
|
|
@@ -103,7 +104,7 @@ class Tasks
|
|
|
103
104
|
method_option :count, aliases: "-n", type: :numeric, default: 1, desc: "Repeat N times"
|
|
104
105
|
def hello(name = "World")
|
|
105
106
|
message = options[:shout] ? "HELLO, #{name.upcase}!" : "Hello, #{name}!"
|
|
106
|
-
options[:count].times {
|
|
107
|
+
options[:count].times { puts message }
|
|
107
108
|
end
|
|
108
109
|
end
|
|
109
110
|
```
|
|
@@ -128,7 +129,7 @@ class Tasks
|
|
|
128
129
|
method_option :count, aliases: "-n", type: :numeric, default: 1, desc: "Repeat N times"
|
|
129
130
|
def hello(name = "World")
|
|
130
131
|
message = options[:shout] ? "HELLO, #{name.upcase}!" : "Hello, #{name}!"
|
|
131
|
-
options[:count].times {
|
|
132
|
+
options[:count].times { puts message }
|
|
132
133
|
end
|
|
133
134
|
end
|
|
134
135
|
```
|
|
@@ -139,7 +140,7 @@ end
|
|
|
139
140
|
|
|
140
141
|
`depends_on` declares what must run before a task. Each dependency runs at most once per `asgard` invocation regardless of how many tasks declare it. Circular dependencies are caught at startup.
|
|
141
142
|
|
|
142
|
-
`desc` and `depends_on` are independent — either can come first, both must appear before `def`.
|
|
143
|
+
`desc` and `depends_on` are independent — either can come first, both must appear before `def`.
|
|
143
144
|
|
|
144
145
|
### Sequential dependencies
|
|
145
146
|
|
|
@@ -147,15 +148,15 @@ Bare symbols run one after another in the order declared:
|
|
|
147
148
|
|
|
148
149
|
```ruby
|
|
149
150
|
class Tasks
|
|
150
|
-
desc "
|
|
151
|
+
desc "Compile the project"
|
|
151
152
|
def build = sh "rake build"
|
|
152
153
|
|
|
153
154
|
depends_on :build
|
|
154
|
-
desc "
|
|
155
|
+
desc "Run the test suite"
|
|
155
156
|
def test = sh "rake test"
|
|
156
157
|
|
|
157
158
|
depends_on :test
|
|
158
|
-
desc "
|
|
159
|
+
desc "Publish the gem"
|
|
159
160
|
def release = sh "bundle exec rake release"
|
|
160
161
|
end
|
|
161
162
|
```
|
|
@@ -170,15 +171,15 @@ Wrap symbols in an array to declare they can run concurrently. Asgard waits for
|
|
|
170
171
|
|
|
171
172
|
```ruby
|
|
172
173
|
class Tasks
|
|
173
|
-
desc "
|
|
174
|
+
desc "Check code style"
|
|
174
175
|
def lint = sh "bundle exec rubocop"
|
|
175
176
|
|
|
176
|
-
desc "
|
|
177
|
+
desc "Run type checks"
|
|
177
178
|
def typecheck = sh "bundle exec srb tc"
|
|
178
179
|
|
|
179
180
|
# lint and typecheck run in parallel, test waits for both
|
|
180
181
|
depends_on [:lint, :typecheck]
|
|
181
|
-
desc "
|
|
182
|
+
desc "Run the test suite"
|
|
182
183
|
def test = sh "bundle exec rake test"
|
|
183
184
|
end
|
|
184
185
|
```
|
|
@@ -193,16 +194,16 @@ Mix bare symbols (sequential) and arrays (parallel) in a single `depends_on` cal
|
|
|
193
194
|
|
|
194
195
|
```ruby
|
|
195
196
|
class Tasks
|
|
196
|
-
desc "
|
|
197
|
-
desc "
|
|
198
|
-
desc "
|
|
199
|
-
desc "
|
|
200
|
-
desc "
|
|
197
|
+
desc "Install dependencies"; def setup = sh "bundle install"
|
|
198
|
+
desc "Check code style"; def lint = sh "bundle exec rubocop"
|
|
199
|
+
desc "Compile assets"; def build = sh "rake assets:precompile"
|
|
200
|
+
desc "Run tests"; def test = sh "bundle exec rake test"
|
|
201
|
+
desc "Post to Slack"; def notify = sh "curl $SLACK_WEBHOOK -d '{\"text\":\"done\"}'"
|
|
201
202
|
|
|
202
203
|
# setup first, then lint+build in parallel, then test, then notify
|
|
203
204
|
depends_on :setup, [:lint, :build], :test, :notify
|
|
204
|
-
desc "
|
|
205
|
-
def ci =
|
|
205
|
+
desc "Full CI pipeline"
|
|
206
|
+
def ci = puts "CI complete"
|
|
206
207
|
end
|
|
207
208
|
```
|
|
208
209
|
|
|
@@ -224,18 +225,24 @@ asgard ci executes:
|
|
|
224
225
|
|
|
225
226
|
## Variables
|
|
226
227
|
|
|
227
|
-
|
|
228
|
+
Shared configuration values are declared as Ruby class variables (`@@name`) at the top of the class body. Use `||=` so the first declaration wins when multiple `.loki` files reopen `Tasks`, and `.freeze` to prevent mutation:
|
|
228
229
|
|
|
229
230
|
```ruby
|
|
230
231
|
class Tasks
|
|
231
|
-
|
|
232
|
-
|
|
232
|
+
@@app ||= "myapp".freeze
|
|
233
|
+
@@max_jobs ||= 4
|
|
233
234
|
|
|
234
|
-
desc "
|
|
235
|
-
def tag = sh "git tag #{app}-#{version}"
|
|
235
|
+
desc "Create a release tag"
|
|
236
|
+
def tag = sh "git tag #{@@app}-#{version}"
|
|
237
|
+
|
|
238
|
+
private
|
|
239
|
+
|
|
240
|
+
def version = `git describe --tags`.strip
|
|
236
241
|
end
|
|
237
242
|
```
|
|
238
243
|
|
|
244
|
+
Class variables are visible in all task instance methods and in subcommand subclasses — unlike class instance variables (`@name`), which are not accessible inside instance methods.
|
|
245
|
+
|
|
239
246
|
---
|
|
240
247
|
|
|
241
248
|
## Helper methods
|
|
@@ -244,13 +251,13 @@ Private methods are callable from any task in the same class but are never regis
|
|
|
244
251
|
|
|
245
252
|
```ruby
|
|
246
253
|
class Tasks
|
|
247
|
-
desc "
|
|
254
|
+
desc "Compile and package"
|
|
248
255
|
def build
|
|
249
256
|
compile("src")
|
|
250
257
|
package(version)
|
|
251
258
|
end
|
|
252
259
|
|
|
253
|
-
desc "
|
|
260
|
+
desc "Build and publish"
|
|
254
261
|
def release
|
|
255
262
|
build
|
|
256
263
|
sh "gem push pkg/myapp-#{version}.gem"
|
|
@@ -268,6 +275,40 @@ class Tasks
|
|
|
268
275
|
end
|
|
269
276
|
```
|
|
270
277
|
|
|
278
|
+
### The `helper` DSL method
|
|
279
|
+
|
|
280
|
+
Some values need to be available in both class context (e.g. inside `header`) and inside task instance methods. `helper` defines the method once in both contexts:
|
|
281
|
+
|
|
282
|
+
```ruby
|
|
283
|
+
class Tasks
|
|
284
|
+
@@project ||= "myapp".freeze
|
|
285
|
+
|
|
286
|
+
helper(:version) {
|
|
287
|
+
File.read("lib/myapp/version.rb").match(/VERSION\s*=\s*"([^"]+)"/)[1].freeze
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
header "#{@@project} v#{version}" # class context
|
|
291
|
+
|
|
292
|
+
desc "Show the current version"
|
|
293
|
+
def show_version
|
|
294
|
+
puts version # instance context
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Without `helper`, achieving this requires two separate definitions:
|
|
300
|
+
|
|
301
|
+
```ruby
|
|
302
|
+
def self.version = File.read(...).match(...)[1].freeze
|
|
303
|
+
no_commands { private def version = self.class.version }
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
`helper` accepts positional arguments, keyword arguments, and blocks — any signature valid in a Ruby method definition:
|
|
307
|
+
|
|
308
|
+
```ruby
|
|
309
|
+
helper(:tag) { |name, ver, prefix: "v"| "#{prefix}#{name}-#{ver}" }
|
|
310
|
+
```
|
|
311
|
+
|
|
271
312
|
Helpers can also be shared across multiple `.loki` files by extracting them into a plain Ruby file and loading it explicitly:
|
|
272
313
|
|
|
273
314
|
```ruby
|
|
@@ -286,13 +327,60 @@ require_relative "shared/helpers"
|
|
|
286
327
|
class Tasks
|
|
287
328
|
include BuildHelpers
|
|
288
329
|
|
|
289
|
-
desc "
|
|
330
|
+
desc "Compile the project"
|
|
290
331
|
def build = compile("src")
|
|
291
332
|
end
|
|
292
333
|
```
|
|
293
334
|
|
|
294
335
|
---
|
|
295
336
|
|
|
337
|
+
## Help header and footer
|
|
338
|
+
|
|
339
|
+
Add static text above and below the command list in `asgard help` output:
|
|
340
|
+
|
|
341
|
+
```ruby
|
|
342
|
+
class Tasks
|
|
343
|
+
header "my-project — build & release tasks"
|
|
344
|
+
footer "See https://example.com/docs for details"
|
|
345
|
+
end
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
Multiple calls accumulate. `header` appends each line (top to bottom); `footer` prepends each line (bottom to top), so content from a later-loaded `.loki` file sits closer to the commands:
|
|
349
|
+
|
|
350
|
+
```ruby
|
|
351
|
+
# .loki
|
|
352
|
+
class Tasks
|
|
353
|
+
header "my-project"
|
|
354
|
+
footer "Maintainer: you@example.com"
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
import "*.loki"
|
|
358
|
+
|
|
359
|
+
# deploy.loki
|
|
360
|
+
class Tasks
|
|
361
|
+
header " deploy targets: staging, production"
|
|
362
|
+
footer "See runbook at wiki/deploy"
|
|
363
|
+
end
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
```
|
|
367
|
+
my-project
|
|
368
|
+
deploy targets: staging, production
|
|
369
|
+
|
|
370
|
+
Commands:
|
|
371
|
+
...
|
|
372
|
+
|
|
373
|
+
Options:
|
|
374
|
+
...
|
|
375
|
+
|
|
376
|
+
See runbook at wiki/deploy
|
|
377
|
+
Maintainer: you@example.com
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
Header and footer text is only shown for the general `asgard help` page, not for `asgard help <command>`.
|
|
381
|
+
|
|
382
|
+
---
|
|
383
|
+
|
|
296
384
|
## Options shared across all tasks
|
|
297
385
|
|
|
298
386
|
`class_option` defines an option available to every task in the class:
|
|
@@ -320,7 +408,7 @@ end
|
|
|
320
408
|
|
|
321
409
|
```ruby
|
|
322
410
|
class Tasks
|
|
323
|
-
desc "
|
|
411
|
+
desc "Bootstrap the development environment"
|
|
324
412
|
def setup
|
|
325
413
|
sh <<~SHELL
|
|
326
414
|
brew install redis postgresql
|
|
@@ -330,7 +418,7 @@ class Tasks
|
|
|
330
418
|
SHELL
|
|
331
419
|
end
|
|
332
420
|
|
|
333
|
-
desc "
|
|
421
|
+
desc "Run Python data analysis"
|
|
334
422
|
def analyze
|
|
335
423
|
shebang :python3, <<~PYTHON
|
|
336
424
|
import json
|
|
@@ -339,7 +427,7 @@ class Tasks
|
|
|
339
427
|
PYTHON
|
|
340
428
|
end
|
|
341
429
|
|
|
342
|
-
desc "
|
|
430
|
+
desc "Build frontend assets with esbuild"
|
|
343
431
|
def bundle_assets
|
|
344
432
|
shebang :node, <<~JS
|
|
345
433
|
const esbuild = require("esbuild")
|
|
@@ -365,18 +453,31 @@ PY
|
|
|
365
453
|
|
|
366
454
|
## Environment variables
|
|
367
455
|
|
|
368
|
-
`dotenv` loads a `.env` file into the environment before tasks run:
|
|
456
|
+
`dotenv` loads a `.env` file into the environment before tasks run. Use the `env` Kernel method to read environment variables inside task bodies — it accepts a symbol or string and upcases the key automatically:
|
|
369
457
|
|
|
370
458
|
```ruby
|
|
371
459
|
class Tasks
|
|
372
460
|
dotenv # loads .env
|
|
373
461
|
dotenv ".env.local" # or a specific file
|
|
374
462
|
|
|
375
|
-
desc "
|
|
376
|
-
def
|
|
463
|
+
desc "Start the server"
|
|
464
|
+
def start
|
|
465
|
+
sh "puma -p #{env(:port, '3000')} -e #{env(:rack_env, 'development')}"
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
desc "Deploy the app"
|
|
469
|
+
def deploy
|
|
470
|
+
sh "cap #{env(:deploy_target)} deploy" # raises KeyError if DEPLOY_TARGET is unset
|
|
471
|
+
end
|
|
377
472
|
end
|
|
378
473
|
```
|
|
379
474
|
|
|
475
|
+
| Call | Behaviour |
|
|
476
|
+
|------|-----------|
|
|
477
|
+
| `env(:port, "3000")` | Returns `"3000"` when `PORT` is unset |
|
|
478
|
+
| `env(:api_key)` | Raises `KeyError` when `API_KEY` is missing |
|
|
479
|
+
| `env("DATABASE_URL")` | String name works too — always upcased |
|
|
480
|
+
|
|
380
481
|
---
|
|
381
482
|
|
|
382
483
|
## Command aliases
|
|
@@ -389,7 +490,7 @@ class Tasks
|
|
|
389
490
|
map "--v" => "version"
|
|
390
491
|
map "t" => "test"
|
|
391
492
|
|
|
392
|
-
desc "
|
|
493
|
+
desc "Print the version"
|
|
393
494
|
def version = puts Asgard::VERSION
|
|
394
495
|
end
|
|
395
496
|
```
|
|
@@ -402,10 +503,10 @@ Group related tasks under a common name using Thor's `subcommand` method. Define
|
|
|
402
503
|
|
|
403
504
|
```ruby
|
|
404
505
|
class DeployCommands < Tasks
|
|
405
|
-
desc "
|
|
506
|
+
desc "Deploy to staging"
|
|
406
507
|
def staging = sh "cap staging deploy"
|
|
407
508
|
|
|
408
|
-
desc "
|
|
509
|
+
desc "Deploy to production"
|
|
409
510
|
def production = sh "cap production deploy"
|
|
410
511
|
end
|
|
411
512
|
|
|
@@ -421,20 +522,20 @@ asgard deploy staging
|
|
|
421
522
|
asgard deploy production
|
|
422
523
|
```
|
|
423
524
|
|
|
424
|
-
Subcommand tasks have all the same access to
|
|
525
|
+
Subcommand tasks have all the same access to `sh`, `shebang`, `depends_on`, and the built-in `--debug`/`--verbose` class options as normal tasks. `@@` class variables declared on `Tasks` are also visible in subcommand subclasses.
|
|
425
526
|
|
|
426
527
|
`depends_on` only works within a subcommand group exactly as it does at the top level:
|
|
427
528
|
|
|
428
529
|
```ruby
|
|
429
530
|
class DBCommands < Tasks
|
|
430
|
-
desc "
|
|
531
|
+
desc "Run pending migrations"
|
|
431
532
|
def migrate = sh "rails db:migrate"
|
|
432
533
|
|
|
433
|
-
desc "
|
|
534
|
+
desc "Load seed data"
|
|
434
535
|
def seed = sh "rails db:seed"
|
|
435
536
|
|
|
436
537
|
depends_on :migrate, :seed
|
|
437
|
-
desc "
|
|
538
|
+
desc "Migrate then seed"
|
|
438
539
|
def reset = puts "Done."
|
|
439
540
|
end
|
|
440
541
|
|
|
@@ -470,7 +571,7 @@ Common `method_option` keys: `aliases`, `type`, `default`, `required`, `desc`, `
|
|
|
470
571
|
|
|
471
572
|
## Task files
|
|
472
573
|
|
|
473
|
-
Asgard searches the current directory and its ancestors for a `.loki` file. That file marks the project root. `*.loki` files
|
|
574
|
+
Asgard searches the current directory and its ancestors for a `.loki` file. That file marks the project root. Additional `*.loki` files are loaded only when your `.loki` file explicitly calls `import`.
|
|
474
575
|
|
|
475
576
|
### Single file
|
|
476
577
|
|
|
@@ -485,16 +586,23 @@ Split tasks across files — each reopens `class Tasks`:
|
|
|
485
586
|
|
|
486
587
|
```
|
|
487
588
|
myproject/
|
|
488
|
-
.loki ← entry point
|
|
589
|
+
.loki ← entry point; calls import to load task files
|
|
489
590
|
build.loki
|
|
490
591
|
deploy.loki
|
|
491
592
|
test.loki
|
|
492
593
|
```
|
|
493
594
|
|
|
595
|
+
The `.loki` entry file must call `import` to load the other task files. It also marks the project root for auto-discovery:
|
|
596
|
+
|
|
597
|
+
```ruby
|
|
598
|
+
# .loki
|
|
599
|
+
import "*.loki"
|
|
600
|
+
```
|
|
601
|
+
|
|
494
602
|
```ruby
|
|
495
603
|
# build.loki
|
|
496
604
|
class Tasks
|
|
497
|
-
desc "
|
|
605
|
+
desc "Compile the project"
|
|
498
606
|
def build = sh "rake build"
|
|
499
607
|
end
|
|
500
608
|
```
|
|
@@ -503,7 +611,7 @@ end
|
|
|
503
611
|
# test.loki
|
|
504
612
|
class Tasks
|
|
505
613
|
depends_on :build
|
|
506
|
-
desc "
|
|
614
|
+
desc "Run the test suite"
|
|
507
615
|
def test = sh "bundle exec rake test"
|
|
508
616
|
end
|
|
509
617
|
```
|
|
@@ -512,21 +620,21 @@ end
|
|
|
512
620
|
# deploy.loki
|
|
513
621
|
class Tasks
|
|
514
622
|
depends_on :test
|
|
515
|
-
desc "
|
|
623
|
+
desc "Deploy to production"
|
|
516
624
|
def deploy = sh "cap production deploy"
|
|
517
625
|
end
|
|
518
626
|
```
|
|
519
627
|
|
|
520
|
-
|
|
628
|
+
In a single-file project, `.loki` can be completely empty — its presence alone marks the project root. In a multi-file project, add at least an `import` call.
|
|
521
629
|
|
|
522
630
|
### Explicit loading
|
|
523
631
|
|
|
524
|
-
Load any Ruby or `.loki` file manually from `.loki
|
|
632
|
+
Load any Ruby or `.loki` file manually from `.loki`. Use `require_relative` for plain Ruby; use `import` for `.loki` files (it enforces the extension and is idempotent):
|
|
525
633
|
|
|
526
634
|
```ruby
|
|
527
635
|
# .loki
|
|
528
636
|
require_relative "shared/helpers"
|
|
529
|
-
|
|
637
|
+
import "ci.loki"
|
|
530
638
|
|
|
531
639
|
class Tasks
|
|
532
640
|
# additional tasks
|
|
@@ -540,8 +648,7 @@ end
|
|
|
540
648
|
| Method | Description |
|
|
541
649
|
|---|---|
|
|
542
650
|
| `Asgard.run!(argv)` | Entry point — finds `.loki`, loads task files, starts CLI |
|
|
543
|
-
| `Asgard.find_task_file` | Returns
|
|
544
|
-
| `Asgard.load_loki(dir)` | Loads all `*.loki` files in dir alphabetically — called by `run!` only when `--auto-load` is passed |
|
|
651
|
+
| `Asgard.find_task_file` | Returns a `Pathname` to `.loki` searching from CWD upward, or `nil` |
|
|
545
652
|
|
|
546
653
|
`run!` handles its own errors — a missing `.loki`, a circular dependency, or a `depends_on` that names a task that doesn't exist all produce a clean one-line message and exit 1.
|
|
547
654
|
|
data/docs/api.md
CHANGED
|
@@ -11,8 +11,7 @@ These class methods are defined on the `Asgard` module itself.
|
|
|
11
11
|
| Method | Signature | Description |
|
|
12
12
|
|---|---|---|
|
|
13
13
|
| `run!` | `Asgard.run!(argv)` | Main entry point. Finds `.loki`, loads all task files, validates the dependency graph, and dispatches via Thor. Handles its own errors: missing `.loki` and circular dependencies both produce a clean one-line message and `exit 1`. |
|
|
14
|
-
| `find_task_file` | `Asgard.find_task_file →
|
|
15
|
-
| `load_loki` | `Asgard.load_loki(dir)` | Loads all `*.loki` files in `dir` alphabetically, excluding `.loki` itself. Called by `run!` only when `--auto-load` is present in `argv`. |
|
|
14
|
+
| `find_task_file` | `Asgard.find_task_file → Pathname, nil` | Searches `Dir.pwd` and each ancestor directory for a `.loki` file. Returns a `Pathname` of the first match, or `nil` if none is found. |
|
|
16
15
|
|
|
17
16
|
### `run!` Details
|
|
18
17
|
|
|
@@ -30,6 +29,82 @@ After loading task files, it calls `Tasks.validate_deps!` (circular dependency c
|
|
|
30
29
|
|
|
31
30
|
---
|
|
32
31
|
|
|
32
|
+
## Kernel Methods
|
|
33
|
+
|
|
34
|
+
These methods are defined as `module_function` on `Kernel` and are therefore available everywhere in Ruby — at the top level of `.loki` files, inside class bodies, and inside task method bodies. No `require` or `include` is needed; they are loaded when `asgard` starts.
|
|
35
|
+
|
|
36
|
+
| Method | Signature | Returns | Description |
|
|
37
|
+
|---|---|---|---|
|
|
38
|
+
| `loki_up` | `loki_up(name = ".loki") → Pathname, nil` | `Pathname` or `nil` | Searches `Dir.pwd` and each ancestor directory for a file named `name`. Returns a `Pathname` for the first match, or `nil` if not found. Exact filenames only — does not expand globs. |
|
|
39
|
+
| `import` | `import(path) → true, false` | `true` if any file newly loaded | Loads one `.loki` file or a glob of `.loki` files. Relative paths resolve relative to the caller's file (like `require_relative`). Idempotent via `$LOADED_FEATURES`. Raises `ArgumentError` if `path` does not end with `.loki`. Raises `LoadError` if a non-glob path does not exist. |
|
|
40
|
+
| `import_up` | `import_up(name = ".loki") → true, false` | `true` if any file newly loaded | Combines `loki_up` and `import`. Walks ancestors to find the file or glob match, then loads it. Returns `false` if nothing is found. |
|
|
41
|
+
| `debug?` | `debug? → true, false` | `$DEBUG` | Returns the current value of `$DEBUG`. Set to `true` by `--debug` on the CLI or directly via `$DEBUG = true`. |
|
|
42
|
+
| `verbose?` | `verbose? → true, false` | `$VERBOSE` | Returns the current value of `$VERBOSE`. Set to `true` by `--verbose` on the CLI or directly via `$VERBOSE = true`. |
|
|
43
|
+
| `env` | `env(name, default = nil) → String, nil` | `ENV` value or default | Fetches an environment variable by symbol or string name. The name is upcased automatically. Raises `KeyError` when the variable is missing and no default is given. |
|
|
44
|
+
|
|
45
|
+
### `loki_up` Details
|
|
46
|
+
|
|
47
|
+
Despite the name, `loki_up` is not limited to `.loki` files — it locates any file by walking up the directory tree:
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
loki_up # find .loki (the project root marker)
|
|
51
|
+
loki_up("gem_tasks.loki") # find gem_tasks.loki in CWD or any ancestor
|
|
52
|
+
loki_up(".env") # find the nearest .env file up the tree
|
|
53
|
+
loki_up("VERSION") # find a VERSION file in CWD or any ancestor
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Returns a `Pathname` or `nil`. Does not load the file. `Pathname` is accepted by `import`, `dotenv`, `load`, and standard Ruby file methods — no `.to_s` conversion needed in common usage.
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
if (path = loki_up("gem_tasks.loki"))
|
|
60
|
+
import path # Pathname accepted directly
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Pass the located .env to dotenv — works from any subdirectory
|
|
64
|
+
dotenv loki_up(".env") || ".env"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### `import` Details
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
import "build.loki" # relative — resolved from the calling file's directory
|
|
71
|
+
import "/home/shared/gem_tasks.loki" # absolute
|
|
72
|
+
import "*.loki" # all *.loki in the same directory as the caller
|
|
73
|
+
import "../shared/*.loki" # all *.loki one level up
|
|
74
|
+
import "**/*.loki" # all *.loki recursively
|
|
75
|
+
import Pathname.new("tasks.loki") # Pathname accepted
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Extension requirement:** the path (or glob pattern) must end with `.loki`. Passing any other extension raises `ArgumentError`.
|
|
79
|
+
|
|
80
|
+
**Glob behaviour:** `Dir.glob` is used for pattern expansion. `*.loki` does not match `.loki` (the dotfile) — Ruby's glob excludes dotfiles from `*` by default. Files are loaded in the order `Dir.glob` returns them (sorted on Ruby ≥ 2.7).
|
|
81
|
+
|
|
82
|
+
**Idempotency:** each resolved absolute path is checked against `$LOADED_FEATURES` before loading. A file already in `$LOADED_FEATURES` is silently skipped and contributes `false` to the return value.
|
|
83
|
+
|
|
84
|
+
**Return value:** `true` if at least one file was newly loaded; `false` if all matched files were already loaded or no glob pattern produced any matches.
|
|
85
|
+
|
|
86
|
+
**Verbose/debug output** (to stderr):
|
|
87
|
+
- `verbose?` true — prints each file path as it is loaded
|
|
88
|
+
- `debug?` true — also prints a skip message for each already-loaded file
|
|
89
|
+
|
|
90
|
+
### `import_up` Details
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
import_up # find and load .loki
|
|
94
|
+
import_up "gem_tasks.loki" # find and load gem_tasks.loki up the tree
|
|
95
|
+
import_up "*.loki" # find the nearest ancestor with *.loki files and load them all
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**Exact name:** delegates to `loki_up` to find the file, then calls `import` with the absolute path. Returns `false` without raising if the file is not found.
|
|
99
|
+
|
|
100
|
+
**Glob name:** walks ancestor directories manually using `Dir.glob`. Stops at the **first** ancestor that has any matches and loads all of them — it does not continue walking after finding a match. Returns `false` if no ancestor contains matching files.
|
|
101
|
+
|
|
102
|
+
**Verbose/debug output** (to stderr):
|
|
103
|
+
- `verbose?` true — prints `name → /full/path` when a file or directory is found
|
|
104
|
+
- `debug?` true — also prints `name not found` when the search comes up empty
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
33
108
|
## `Asgard::Base` DSL Class Methods
|
|
34
109
|
|
|
35
110
|
`Asgard::Base` is a `Thor` subclass that provides the task DSL. It is the superclass of `Tasks`. All DSL methods are class methods (called in the class body).
|
|
@@ -37,9 +112,10 @@ After loading task files, it calls `Tasks.validate_deps!` (circular dependency c
|
|
|
37
112
|
| Method | Signature | Description |
|
|
38
113
|
|---|---|---|
|
|
39
114
|
| `depends_on` | `depends_on(*tasks)` | Declare prerequisites for the next `def`. Bare symbols run sequentially; arrays within the splat run as a parallel group. |
|
|
40
|
-
| `var` | `var(name, value = nil, &block)` | Declare a named variable. If `value` responds to `call` (lambda/proc) or a block is given, the value is computed lazily on first access. Accessible in task bodies as a method. |
|
|
41
|
-
| `import` | `import(mod)` | Include a module into the current class (thin alias for `include`). |
|
|
42
115
|
| `dotenv` | `dotenv(path = ".env")` | Load the specified `.env` file into `ENV` using the dotenv gem. Silently skipped if the file does not exist. Called at class-load time. |
|
|
116
|
+
| `header` | `header(text)` | Append a line of text shown above the commands list in `asgard help`. Each call adds another line. No-op for per-command help. |
|
|
117
|
+
| `footer` | `footer(text)` | Prepend a line of text shown below the options block in `asgard help`. Each call inserts above the previous lines. No-op for per-command help. |
|
|
118
|
+
| `no_negate` | `no_negate(*names)` | Suppress `[--no-name]` / `[--skip-name]` help entries for one or more boolean class options. Call after the `class_option` declaration. |
|
|
43
119
|
| `sh` | `sh(script, silent: false)` | Instance method. Run a shell command or multiline heredoc. Single-line → `system(script)`; multiline → `system("bash", "-c", script)`. Exits with the command's status on failure. |
|
|
44
120
|
| `shebang` | `shebang(interpreter, script, silent: false)` | Instance method. Write `script` to a tempfile and execute it with `interpreter`. See the [Shell Helpers](shell.md) page for the full interpreter table. |
|
|
45
121
|
| `validate_deps!` | `Tasks.validate_deps!` | Build and topologically sort the full dependency graph using Dagwood. Raises `Asgard::CircularDependencyError` on cycles. Called by `run!` at startup. |
|
|
@@ -64,10 +140,9 @@ depends_on :setup, [:lint, :build], :test # setup, then lint+build concurrently
|
|
|
64
140
|
|---|---|---|
|
|
65
141
|
| `class_option :debug` | class option | `--debug` flag. Sets `$DEBUG = true` before any task runs. Boolean, default `false`. |
|
|
66
142
|
| `class_option :verbose` | class option | `--verbose` flag. Sets `$VERBOSE = true` before any task runs. Boolean, default `false`. |
|
|
67
|
-
| `
|
|
68
|
-
| `debug?` |
|
|
69
|
-
| `verbose?` |
|
|
70
|
-
| `--auto-load` | CLI flag (consumed by `run!`) | Triggers loading of all `*.loki` files before the main `.loki` and the requested task. Consumed by `run!` before Thor dispatch. |
|
|
143
|
+
| `class_option :version` | class option | `--version` flag. Handled by `Asgard.run!` before the `.loki` file is loaded — prints `Asgard::VERSION` and exits. `no_negate :version` suppresses the `[--no-version]` / `[--skip-version]` help entries. |
|
|
144
|
+
| `debug?` | Kernel module function | Returns `$DEBUG`. Available everywhere via `Kernel`. |
|
|
145
|
+
| `verbose?` | Kernel module function | Returns `$VERBOSE`. Available everywhere via `Kernel`. |
|
|
71
146
|
|
|
72
147
|
---
|
|
73
148
|
|
|
@@ -78,9 +153,10 @@ These are implementation details exposed for extensibility. Prefer the DSL metho
|
|
|
78
153
|
| Method | Description |
|
|
79
154
|
|---|---|
|
|
80
155
|
| `_deps` | Hash mapping task name symbols to their stage arrays. Set by `depends_on` + `method_added`. |
|
|
81
|
-
| `
|
|
82
|
-
| `
|
|
83
|
-
| `
|
|
156
|
+
| `_done` | `Set` of task name symbols that have completed in the current invocation. |
|
|
157
|
+
| `_running` | `Set` of task name symbols currently executing (started but not yet finished). |
|
|
158
|
+
| `_cond` | Hash of `ConditionVariable` objects keyed by task name; threads wait here when a dep is in-flight. |
|
|
159
|
+
| `_ran_mutex` | `Mutex` protecting `_done`, `_running`, and `_cond` for thread-safe access. |
|
|
84
160
|
| `_build_dep_graph(stages)` | Translates the stage array (from `_deps`) into a Dagwood-compatible hash. |
|
|
85
161
|
|
|
86
162
|
---
|
|
@@ -90,10 +166,10 @@ These are implementation details exposed for extensibility. Prefer the DSL metho
|
|
|
90
166
|
`Asgard::Base` overrides Thor's `invoke_command` to implement dependency resolution and deduplication:
|
|
91
167
|
|
|
92
168
|
1. Sets `$DEBUG` / `$VERBOSE` from `options` if the corresponding flags are present.
|
|
93
|
-
2.
|
|
94
|
-
3.
|
|
95
|
-
4.
|
|
96
|
-
5.
|
|
169
|
+
2. Tries to acquire a run token (`acquire_run_token`): if the task is already in `_done`, returns immediately (skip); if it is in `_running`, waits on the `_cond` ConditionVariable until it finishes, then returns (skip); otherwise adds the task to `_running` and continues.
|
|
170
|
+
3. Resolves dependency stages from `_deps`, builds the Dagwood graph, and executes groups (parallel groups in threads, sequential groups one at a time).
|
|
171
|
+
4. Calls `command.run(self, *args)` to execute the task itself.
|
|
172
|
+
5. In an `ensure` block, adds the task to `_done` and broadcasts on its `_cond` to wake any waiting threads.
|
|
97
173
|
|
|
98
174
|
---
|
|
99
175
|
|
data/docs/changelog.md
CHANGED
|
@@ -8,6 +8,18 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). Asg
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **`helper` DSL method** — defines a method available in both class context (e.g. inside `header`) and instance context (inside task methods) with a single declaration. Eliminates the manual `def self.name` + `no_commands { private def name = self.class.name }` boilerplate. Supports positional arguments, keyword arguments, and block arguments. See [Helper Methods](helpers.md).
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- **`quality` task** — all three gates (`test`, `rubocop`, `flog_check`) now run in parallel. Each gate captures its own pass/fail result; output is suppressed on pass and filtered to failures only on fail. A summary table is printed after all gates complete.
|
|
18
|
+
|
|
19
|
+
### Removed
|
|
20
|
+
|
|
21
|
+
- **`var` DSL method** — replaced by native Ruby class variables. Use `@@name ||= "value".freeze` in the class body. Class variables are visible in all task instance methods and in subcommand subclasses, making them the correct tool for shared configuration in a Thor-based task runner. See [Variables](variables.md).
|
|
22
|
+
|
|
11
23
|
## [0.2.0] — 2026-05-29
|
|
12
24
|
|
|
13
25
|
### Changed
|