asgard 0.1.0 → 0.1.2
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 +4 -2
- data/CHANGELOG.md +29 -2
- data/CLAUDE.md +117 -0
- data/README.md +415 -136
- data/bin/asgard +1 -20
- data/examples/.loki +2 -0
- data/examples/db_subcommands.loki +74 -0
- data/examples/kitchen_sink.loki +164 -0
- data/examples/server_subcommands.loki +56 -0
- data/lib/asgard/base.rb +10 -8
- data/lib/asgard/tasks.rb +28 -0
- data/lib/asgard/version.rb +1 -1
- data/lib/asgard.rb +26 -13
- metadata +14 -22
data/README.md
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# Asgard
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
A Ruby task runner built on [Thor](https://github.com/rails/thor) for argument handling and [Dagwood](https://github.com/rewindio/dagwood) for dependency ordering.
|
|
4
4
|
|
|
5
5
|
The name comes from Norse mythology: **Thor** is the CLI framework, **Asgard** is the realm where tasks live, and the task file is named **loki** — because Loki holds all the tricks.
|
|
6
6
|
|
|
7
|
+
> **Asgard is a wrapper around [Thor](https://github.com/rails/thor).** Anything Thor can do — subcommands, typed options, argument validation, shell completion — is available inside a `.loki` file. Familiarity with Thor's DSL will make you immediately productive with Asgard.
|
|
8
|
+
|
|
7
9
|
## Installation
|
|
8
10
|
|
|
9
11
|
```bash
|
|
@@ -16,241 +18,518 @@ Or add to your Gemfile:
|
|
|
16
18
|
bundle add asgard
|
|
17
19
|
```
|
|
18
20
|
|
|
19
|
-
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Tasks
|
|
24
|
+
|
|
25
|
+
Every `.loki` file defines tasks as methods inside `class Tasks`. The `Tasks` class is pre-defined by the gem — just reopen it and add methods.
|
|
20
26
|
|
|
21
|
-
|
|
27
|
+
### A task with no parameters
|
|
22
28
|
|
|
23
29
|
```ruby
|
|
24
|
-
|
|
30
|
+
class Tasks
|
|
31
|
+
desc "hello", "Say hello"
|
|
32
|
+
def hello = sh 'echo "Hello, World!"'
|
|
33
|
+
end
|
|
34
|
+
```
|
|
25
35
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
sh <<~SHELL
|
|
30
|
-
brew install redis
|
|
31
|
-
npm install
|
|
32
|
-
bundle install
|
|
33
|
-
SHELL
|
|
34
|
-
end
|
|
36
|
+
```bash
|
|
37
|
+
asgard hello
|
|
38
|
+
```
|
|
35
39
|
|
|
36
|
-
|
|
37
|
-
desc "test", "Run the test suite"
|
|
38
|
-
def test
|
|
39
|
-
sh "bundle exec rake test"
|
|
40
|
-
end
|
|
40
|
+
### A task with a positional parameter
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
42
|
+
Declare positional parameters directly in the method signature. Document them in the `desc` usage string:
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
class Tasks
|
|
46
|
+
desc "hello NAME", "Say hello to NAME"
|
|
47
|
+
def hello(name = "World") = sh "echo 'Hello, #{name}!'"
|
|
48
|
+
end
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
asgard hello
|
|
53
|
+
asgard hello Alice
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### A task with a formal argument declaration
|
|
57
|
+
|
|
58
|
+
Use `argument` for richer metadata — type checking, enums, and help text:
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
class Tasks
|
|
62
|
+
argument :name,
|
|
63
|
+
type: :string,
|
|
64
|
+
default: "World",
|
|
65
|
+
desc: "Name to greet"
|
|
66
|
+
|
|
67
|
+
desc "hello NAME", "Say hello to NAME"
|
|
68
|
+
def hello = sh "echo 'Hello, #{name}!'"
|
|
51
69
|
end
|
|
52
70
|
```
|
|
53
71
|
|
|
54
|
-
|
|
72
|
+
### A task with named options
|
|
73
|
+
|
|
74
|
+
Use `method_option` (alias: `option`) for named flags. Access them inside the method via `options[:name]`:
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
class Tasks
|
|
78
|
+
desc "hello NAME", "Say hello to NAME"
|
|
79
|
+
method_option :shout, aliases: "-s", type: :boolean, desc: "Uppercase the output"
|
|
80
|
+
method_option :count, aliases: "-n", type: :numeric, default: 1, desc: "Repeat N times"
|
|
81
|
+
def hello(name = "World")
|
|
82
|
+
message = options[:shout] ? "HELLO, #{name.upcase}!" : "Hello, #{name}!"
|
|
83
|
+
options[:count].times { sh "echo '#{message}'" }
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
```
|
|
55
87
|
|
|
56
88
|
```bash
|
|
57
|
-
asgard
|
|
58
|
-
asgard release # runs deps, then test, then release
|
|
59
|
-
asgard help # list all available tasks
|
|
89
|
+
asgard hello Alice --shout --count 3
|
|
60
90
|
```
|
|
61
91
|
|
|
62
|
-
|
|
92
|
+
### A task with an extended description
|
|
93
|
+
|
|
94
|
+
`long_desc` provides detailed help shown by `asgard help <task>`:
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
class Tasks
|
|
98
|
+
long_desc <<~DESC
|
|
99
|
+
Says hello to NAME.
|
|
100
|
+
Repeats the greeting COUNT times.
|
|
101
|
+
Use --shout to uppercase the output.
|
|
102
|
+
DESC
|
|
103
|
+
desc "hello NAME", "Say hello to NAME"
|
|
104
|
+
method_option :shout, aliases: "-s", type: :boolean, desc: "Uppercase the output"
|
|
105
|
+
method_option :count, aliases: "-n", type: :numeric, default: 1, desc: "Repeat N times"
|
|
106
|
+
def hello(name = "World")
|
|
107
|
+
message = options[:shout] ? "HELLO, #{name.upcase}!" : "Hello, #{name}!"
|
|
108
|
+
options[:count].times { sh "echo '#{message}'" }
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Dependencies
|
|
116
|
+
|
|
117
|
+
`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.
|
|
63
118
|
|
|
64
|
-
|
|
119
|
+
`desc` and `depends_on` are independent — either can come first, both must appear before `def`.
|
|
65
120
|
|
|
66
|
-
|
|
67
|
-
2. **`*.loki`** — all matching files loaded alphabetically when no `.loki` exists.
|
|
121
|
+
### Sequential dependencies
|
|
68
122
|
|
|
69
|
-
|
|
123
|
+
Bare symbols run one after another in the order declared:
|
|
70
124
|
|
|
125
|
+
```ruby
|
|
126
|
+
class Tasks
|
|
127
|
+
desc "build", "Compile the project"
|
|
128
|
+
def build = sh "rake build"
|
|
129
|
+
|
|
130
|
+
depends_on :build
|
|
131
|
+
desc "test", "Run the test suite"
|
|
132
|
+
def test = sh "rake test"
|
|
133
|
+
|
|
134
|
+
depends_on :test
|
|
135
|
+
desc "release", "Publish the gem"
|
|
136
|
+
def release = sh "bundle exec rake release"
|
|
137
|
+
end
|
|
71
138
|
```
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
asgard release # build → test → release
|
|
75
142
|
```
|
|
76
143
|
|
|
77
|
-
|
|
144
|
+
### Parallel dependencies
|
|
145
|
+
|
|
146
|
+
Wrap symbols in an array to declare they can run concurrently. Asgard waits for all tasks in a parallel group to finish before moving to the next stage:
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
class Tasks
|
|
150
|
+
desc "lint", "Check code style"
|
|
151
|
+
def lint = sh "bundle exec rubocop"
|
|
152
|
+
|
|
153
|
+
desc "typecheck", "Run type checks"
|
|
154
|
+
def typecheck = sh "bundle exec srb tc"
|
|
78
155
|
|
|
156
|
+
# lint and typecheck run in parallel, test waits for both
|
|
157
|
+
depends_on [:lint, :typecheck]
|
|
158
|
+
desc "test", "Run the test suite"
|
|
159
|
+
def test = sh "bundle exec rake test"
|
|
160
|
+
end
|
|
79
161
|
```
|
|
80
|
-
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
asgard test # lint ∥ typecheck → test
|
|
81
165
|
```
|
|
82
166
|
|
|
83
|
-
|
|
167
|
+
### Mixed sequential and parallel
|
|
84
168
|
|
|
85
|
-
|
|
169
|
+
Mix bare symbols (sequential) and arrays (parallel) in a single `depends_on` call. Execution proceeds stage by stage — each stage must complete before the next begins:
|
|
86
170
|
|
|
87
171
|
```ruby
|
|
88
|
-
|
|
89
|
-
desc "
|
|
90
|
-
def
|
|
91
|
-
|
|
172
|
+
class Tasks
|
|
173
|
+
desc "setup", "Install dependencies"; def setup = sh "bundle install"
|
|
174
|
+
desc "lint", "Check code style"; def lint = sh "bundle exec rubocop"
|
|
175
|
+
desc "build", "Compile assets"; def build = sh "rake assets:precompile"
|
|
176
|
+
desc "test", "Run tests"; def test = sh "bundle exec rake test"
|
|
177
|
+
desc "notify", "Post to Slack"; def notify = sh "curl $SLACK_WEBHOOK -d '{\"text\":\"done\"}'"
|
|
178
|
+
|
|
179
|
+
# setup first, then lint+build in parallel, then test, then notify
|
|
180
|
+
depends_on :setup, [:lint, :build], :test, :notify
|
|
181
|
+
desc "ci", "Full CI pipeline"
|
|
182
|
+
def ci = sh "echo 'CI complete'"
|
|
92
183
|
end
|
|
93
184
|
```
|
|
94
185
|
|
|
95
|
-
|
|
186
|
+
```
|
|
187
|
+
asgard ci executes:
|
|
188
|
+
|
|
189
|
+
setup
|
|
190
|
+
↓
|
|
191
|
+
lint ∥ build (concurrent)
|
|
192
|
+
↓
|
|
193
|
+
test
|
|
194
|
+
↓
|
|
195
|
+
notify
|
|
196
|
+
↓
|
|
197
|
+
ci
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## Variables
|
|
96
203
|
|
|
97
|
-
|
|
204
|
+
`var` declares a named value available to all tasks as a method. Pass a lambda for lazy evaluation — it is called once on first use:
|
|
98
205
|
|
|
99
206
|
```ruby
|
|
100
|
-
|
|
101
|
-
var :
|
|
207
|
+
class Tasks
|
|
208
|
+
var :app, "myapp"
|
|
209
|
+
var :version, -> { `git describe --tags`.strip }
|
|
102
210
|
|
|
103
|
-
desc "tag", "Create a
|
|
104
|
-
def tag
|
|
105
|
-
sh "git tag #{version}"
|
|
211
|
+
desc "tag", "Create a release tag"
|
|
212
|
+
def tag = sh "git tag #{app}-#{version}"
|
|
106
213
|
end
|
|
107
214
|
```
|
|
108
215
|
|
|
109
|
-
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Helper methods
|
|
219
|
+
|
|
220
|
+
Private methods are callable from any task in the same class but are never registered as commands — they won't appear in `--help` output and can't be invoked from the CLI.
|
|
110
221
|
|
|
111
222
|
```ruby
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
223
|
+
class Tasks
|
|
224
|
+
desc "build", "Compile and package"
|
|
225
|
+
def build
|
|
226
|
+
compile("src")
|
|
227
|
+
package(version)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
desc "release", "Build and publish"
|
|
231
|
+
def release
|
|
232
|
+
build
|
|
233
|
+
sh "gem push pkg/myapp-#{version}.gem"
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
private
|
|
237
|
+
|
|
238
|
+
def compile(dir)
|
|
239
|
+
sh "gcc -O2 -o bin/myapp #{dir}/*.c"
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def package(ver)
|
|
243
|
+
sh "tar czf pkg/myapp-#{ver}.tar.gz bin/"
|
|
244
|
+
end
|
|
120
245
|
end
|
|
121
246
|
```
|
|
122
247
|
|
|
123
|
-
|
|
248
|
+
Helpers can also be shared across multiple `.loki` files by extracting them into a plain Ruby file and loading it explicitly:
|
|
124
249
|
|
|
125
250
|
```ruby
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
251
|
+
# shared/helpers.rb
|
|
252
|
+
module BuildHelpers
|
|
253
|
+
private
|
|
254
|
+
|
|
255
|
+
def compile(dir)
|
|
256
|
+
sh "gcc -O2 -o bin/myapp #{dir}/*.c"
|
|
257
|
+
end
|
|
133
258
|
end
|
|
134
259
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
260
|
+
# .loki
|
|
261
|
+
require_relative "shared/helpers"
|
|
262
|
+
|
|
263
|
+
class Tasks
|
|
264
|
+
include BuildHelpers
|
|
265
|
+
|
|
266
|
+
desc "build", "Compile the project"
|
|
267
|
+
def build = compile("src")
|
|
141
268
|
end
|
|
142
269
|
```
|
|
143
270
|
|
|
144
|
-
|
|
271
|
+
---
|
|
145
272
|
|
|
146
|
-
|
|
273
|
+
## Options shared across all tasks
|
|
147
274
|
|
|
148
|
-
|
|
275
|
+
`class_option` defines an option available to every task in the class:
|
|
149
276
|
|
|
150
277
|
```ruby
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
278
|
+
class Tasks
|
|
279
|
+
class_option :dry_run, aliases: "-n", type: :boolean, desc: "Print commands without running"
|
|
280
|
+
|
|
281
|
+
desc "deploy ENV", "Deploy to the given environment"
|
|
282
|
+
def deploy(env = "staging")
|
|
283
|
+
if options[:dry_run]
|
|
284
|
+
puts "Would deploy to #{env}"
|
|
285
|
+
else
|
|
286
|
+
sh "cap #{env} deploy"
|
|
287
|
+
end
|
|
156
288
|
end
|
|
157
289
|
end
|
|
290
|
+
```
|
|
158
291
|
|
|
159
|
-
|
|
160
|
-
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## Shell helpers
|
|
161
295
|
|
|
162
|
-
|
|
163
|
-
|
|
296
|
+
`sh` runs any shell command or multiline heredoc. `shebang` writes a script body to a tempfile and executes it with the given interpreter. Both exit with the command's status code on failure.
|
|
297
|
+
|
|
298
|
+
```ruby
|
|
299
|
+
class Tasks
|
|
300
|
+
desc "setup", "Bootstrap the development environment"
|
|
301
|
+
def setup
|
|
302
|
+
sh <<~SHELL
|
|
303
|
+
brew install redis postgresql
|
|
304
|
+
brew services start redis
|
|
305
|
+
bundle install
|
|
306
|
+
rails db:setup
|
|
307
|
+
SHELL
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
desc "analyze", "Run Python data analysis"
|
|
311
|
+
def analyze
|
|
312
|
+
shebang :python3, <<~PYTHON
|
|
313
|
+
import json
|
|
314
|
+
data = json.load(open("results.json"))
|
|
315
|
+
print(f"Total: {sum(data.values())}")
|
|
316
|
+
PYTHON
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
desc "bundle_assets", "Build frontend assets with esbuild"
|
|
320
|
+
def bundle_assets
|
|
321
|
+
shebang :node, <<~JS
|
|
322
|
+
const esbuild = require("esbuild")
|
|
323
|
+
esbuild.buildSync({ entryPoints: ["src/app.js"], bundle: true, outfile: "dist/app.js" })
|
|
324
|
+
JS
|
|
325
|
+
end
|
|
164
326
|
end
|
|
165
327
|
```
|
|
166
328
|
|
|
167
|
-
|
|
329
|
+
Supported interpreters: `:python3`, `:python`, `:node`, `:ruby`, `:perl`, `:bash`, `:sh`. Any other symbol is passed directly to `system` with a `.tmp` extension.
|
|
330
|
+
|
|
331
|
+
Pass `silent: true` to suppress the command echo:
|
|
168
332
|
|
|
169
333
|
```ruby
|
|
170
|
-
|
|
171
|
-
# invoked as: asgard deploy production
|
|
334
|
+
def build = sh "rake build", silent: true
|
|
172
335
|
```
|
|
173
336
|
|
|
174
|
-
|
|
337
|
+
---
|
|
338
|
+
|
|
339
|
+
## Environment variables
|
|
340
|
+
|
|
341
|
+
`dotenv` loads a `.env` file into the environment before tasks run:
|
|
175
342
|
|
|
176
343
|
```ruby
|
|
177
|
-
class Tasks
|
|
178
|
-
dotenv
|
|
179
|
-
dotenv ".env.local"
|
|
344
|
+
class Tasks
|
|
345
|
+
dotenv # loads .env
|
|
346
|
+
dotenv ".env.local" # or a specific file
|
|
180
347
|
|
|
181
|
-
desc "check", "Print the app name"
|
|
182
|
-
def check
|
|
183
|
-
sh "echo $APP_NAME"
|
|
184
|
-
end
|
|
348
|
+
desc "check", "Print the app name from .env"
|
|
349
|
+
def check = sh "echo $APP_NAME"
|
|
185
350
|
end
|
|
186
351
|
```
|
|
187
352
|
|
|
188
|
-
|
|
353
|
+
---
|
|
354
|
+
|
|
355
|
+
## Command aliases
|
|
189
356
|
|
|
190
|
-
|
|
357
|
+
`map` creates alternative names for a task:
|
|
191
358
|
|
|
192
359
|
```ruby
|
|
193
|
-
|
|
194
|
-
|
|
360
|
+
class Tasks
|
|
361
|
+
map "-v" => "version"
|
|
362
|
+
map "--v" => "version"
|
|
363
|
+
map "t" => "test"
|
|
364
|
+
|
|
365
|
+
desc "version", "Print the version"
|
|
366
|
+
def version = puts Asgard::VERSION
|
|
195
367
|
end
|
|
196
368
|
```
|
|
197
369
|
|
|
198
|
-
|
|
370
|
+
---
|
|
199
371
|
|
|
200
|
-
|
|
201
|
-
|---|---|
|
|
202
|
-
| `sh(script, silent: false)` | Run a shell command or multiline script |
|
|
203
|
-
| `shebang(interpreter, script, silent: false)` | Write script to a tempfile and execute it |
|
|
372
|
+
## Subcommands
|
|
204
373
|
|
|
205
|
-
|
|
374
|
+
Group related tasks under a common name using Thor's `subcommand` method. Define a subcommand class that inherits from `Tasks`, then register it with a name and description.
|
|
206
375
|
|
|
207
|
-
|
|
376
|
+
```ruby
|
|
377
|
+
class DeployCommands < Tasks
|
|
378
|
+
desc "staging", "Deploy to staging"
|
|
379
|
+
def staging = sh "cap staging deploy"
|
|
380
|
+
|
|
381
|
+
desc "production", "Deploy to production"
|
|
382
|
+
def production = sh "cap production deploy"
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
class Tasks
|
|
386
|
+
desc "deploy SUBCOMMAND", "Deploy the application"
|
|
387
|
+
subcommand "deploy", DeployCommands
|
|
388
|
+
end
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
```bash
|
|
392
|
+
asgard deploy # shows deploy subcommand help
|
|
393
|
+
asgard deploy staging
|
|
394
|
+
asgard deploy production
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
Subcommand tasks have all the same access to helper methods like `sh`, `shebang`, `depends_on`, `var`, and the built-in `--debug`/`--verbose` class options as normal tasks.
|
|
398
|
+
|
|
399
|
+
`depends_on` only works within a subcommand group exactly as it does at the top level:
|
|
208
400
|
|
|
209
401
|
```ruby
|
|
210
|
-
|
|
402
|
+
class DBCommands < Tasks
|
|
403
|
+
desc "migrate", "Run pending migrations"
|
|
404
|
+
def migrate = sh "rails db:migrate"
|
|
211
405
|
|
|
212
|
-
|
|
213
|
-
|
|
406
|
+
desc "seed", "Load seed data"
|
|
407
|
+
def seed = sh "rails db:seed"
|
|
214
408
|
|
|
215
|
-
|
|
216
|
-
|
|
409
|
+
depends_on :migrate, :seed
|
|
410
|
+
desc "reset", "Migrate then seed"
|
|
411
|
+
def reset = puts "Done."
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
class Tasks
|
|
415
|
+
desc "db SUBCOMMAND", "Manage the database"
|
|
416
|
+
subcommand "db", DBCommands
|
|
417
|
+
end
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
```bash
|
|
421
|
+
asgard db reset # migrate → seed → reset
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
Each subcommand group can have its own `desc`, `long_desc`, `option`, `class_option`, and `map` declarations, all scoped to that group.
|
|
425
|
+
|
|
426
|
+
See [`examples/server_subcommands.loki`](examples/server_subcommands.loki) and [`examples/db_subcommands.loki`](examples/db_subcommands.loki) for full working examples.
|
|
427
|
+
|
|
428
|
+
---
|
|
429
|
+
|
|
430
|
+
## `method_option` types reference
|
|
217
431
|
|
|
432
|
+
| Type | CLI example | Ruby value |
|
|
433
|
+
|---|---|---|
|
|
434
|
+
| `:string` | `--branch main` | `"main"` |
|
|
435
|
+
| `:boolean` | `--force` / `--no-force` | `true` / `false` |
|
|
436
|
+
| `:numeric` | `--count 3` | `3` |
|
|
437
|
+
| `:array` | `--tags foo bar baz` | `["foo", "bar", "baz"]` |
|
|
438
|
+
| `:hash` | `--vars KEY:val FOO:bar` | `{"KEY"=>"val", "FOO"=>"bar"}` |
|
|
439
|
+
|
|
440
|
+
Common `method_option` keys: `aliases`, `type`, `default`, `required`, `desc`, `enum`, `banner`.
|
|
441
|
+
|
|
442
|
+
---
|
|
443
|
+
|
|
444
|
+
## Task files
|
|
445
|
+
|
|
446
|
+
Asgard searches the current directory and its ancestors for a `.loki` file. That file marks the project root. All `*.loki` files in the same directory are auto-loaded alphabetically before `.loki` is loaded.
|
|
447
|
+
|
|
448
|
+
### Single file
|
|
449
|
+
|
|
450
|
+
```
|
|
451
|
+
myproject/
|
|
452
|
+
.loki
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### Multiple files
|
|
456
|
+
|
|
457
|
+
Split tasks across files — each reopens `class Tasks`:
|
|
458
|
+
|
|
459
|
+
```
|
|
460
|
+
myproject/
|
|
461
|
+
.loki ← entry point, can be empty
|
|
462
|
+
build.loki
|
|
463
|
+
deploy.loki
|
|
464
|
+
test.loki
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
```ruby
|
|
468
|
+
# build.loki
|
|
469
|
+
class Tasks
|
|
470
|
+
desc "build", "Compile the project"
|
|
471
|
+
def build = sh "rake build"
|
|
472
|
+
end
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
```ruby
|
|
476
|
+
# test.loki
|
|
477
|
+
class Tasks
|
|
478
|
+
depends_on :build
|
|
218
479
|
desc "test", "Run the test suite"
|
|
219
|
-
def test
|
|
220
|
-
|
|
221
|
-
|
|
480
|
+
def test = sh "bundle exec rake test"
|
|
481
|
+
end
|
|
482
|
+
```
|
|
222
483
|
|
|
484
|
+
```ruby
|
|
485
|
+
# deploy.loki
|
|
486
|
+
class Tasks
|
|
223
487
|
depends_on :test
|
|
224
|
-
desc "
|
|
225
|
-
def
|
|
226
|
-
|
|
227
|
-
|
|
488
|
+
desc "deploy", "Deploy to production"
|
|
489
|
+
def deploy = sh "cap production deploy"
|
|
490
|
+
end
|
|
491
|
+
```
|
|
228
492
|
|
|
229
|
-
|
|
230
|
-
def build
|
|
231
|
-
sh "bundle exec rake build"
|
|
232
|
-
end
|
|
493
|
+
The `.loki` entry point can be completely empty — it only needs to exist to mark the project root.
|
|
233
494
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
495
|
+
### Explicit loading
|
|
496
|
+
|
|
497
|
+
Load any Ruby or `.loki` file manually from `.loki`:
|
|
498
|
+
|
|
499
|
+
```ruby
|
|
500
|
+
# .loki
|
|
501
|
+
require_relative "shared/helpers"
|
|
502
|
+
require_relative "ci.loki"
|
|
503
|
+
|
|
504
|
+
class Tasks
|
|
505
|
+
# additional tasks
|
|
239
506
|
end
|
|
240
507
|
```
|
|
241
508
|
|
|
509
|
+
---
|
|
510
|
+
|
|
511
|
+
## `Asgard` module API
|
|
512
|
+
|
|
513
|
+
| Method | Description |
|
|
514
|
+
|---|---|
|
|
515
|
+
| `Asgard.run!(argv)` | Entry point — finds `.loki`, loads task files, starts CLI |
|
|
516
|
+
| `Asgard.find_task_file` | Returns path to `.loki` searching from CWD upward, or nil |
|
|
517
|
+
| `Asgard.load_loki(dir)` | Loads all `*.loki` files in dir alphabetically |
|
|
518
|
+
|
|
519
|
+
`run!` handles its own errors — a missing `.loki` or a circular dependency both produce a clean one-line message and exit 1.
|
|
520
|
+
|
|
521
|
+
---
|
|
522
|
+
|
|
242
523
|
## Development
|
|
243
524
|
|
|
244
525
|
```bash
|
|
245
526
|
git clone git@github.com:MadBomber/asgard.git
|
|
246
527
|
cd asgard
|
|
247
528
|
bundle install
|
|
248
|
-
bundle exec rake test # run tests
|
|
249
|
-
bundle exec bin/asgard help #
|
|
529
|
+
bundle exec rake test # run tests (95% coverage minimum enforced)
|
|
530
|
+
bundle exec bin/asgard help # exercise the CLI against this gem's own .loki
|
|
250
531
|
```
|
|
251
532
|
|
|
252
|
-
Coverage threshold is enforced at 95% via SimpleCov.
|
|
253
|
-
|
|
254
533
|
## Contributing
|
|
255
534
|
|
|
256
535
|
Bug reports and pull requests are welcome at https://github.com/MadBomber/asgard.
|
data/bin/asgard
CHANGED
|
@@ -2,23 +2,4 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require "asgard"
|
|
5
|
-
|
|
6
|
-
task_files = Asgard.find_task_files
|
|
7
|
-
|
|
8
|
-
unless task_files
|
|
9
|
-
warn "asgard: no .loki or *.loki task file found in #{Dir.pwd} or any parent directory"
|
|
10
|
-
exit 1
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
task_files.each { |f| load f }
|
|
14
|
-
|
|
15
|
-
klass = Asgard::Base.subclasses.last
|
|
16
|
-
|
|
17
|
-
unless klass
|
|
18
|
-
warn "asgard: no class inheriting Asgard::Base found in #{task_files.join(', ')}"
|
|
19
|
-
exit 1
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
klass.validate_deps!
|
|
23
|
-
klass._reset_ran!
|
|
24
|
-
klass.start(ARGV)
|
|
5
|
+
Asgard.run!(ARGV)
|