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/variables.md
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
# Variables
|
|
2
|
+
|
|
3
|
+
Asgard task files are plain Ruby. Shared configuration values are declared using Ruby class variables (`@@name`) at the top of the `Tasks` class body.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Ruby Variable Types
|
|
8
|
+
|
|
9
|
+
Ruby has four kinds of variables plus constants, each with a distinct prefix and scope. Understanding the differences matters because tasks are instance methods — the wrong variable type will simply not be visible where you expect it.
|
|
10
|
+
|
|
11
|
+
| Kind | Prefix | Example | Scope |
|
|
12
|
+
|------|--------|---------|-------|
|
|
13
|
+
| Local | none | `count = 0` | The method or block it is defined in only |
|
|
14
|
+
| Instance | `@` | `@name = "myapp"` | One specific object instance |
|
|
15
|
+
| Class | `@@` | `@@name = "myapp"` | The class and all its subclasses |
|
|
16
|
+
| Global | `$` | `$DEBUG = true` | Everywhere in the process |
|
|
17
|
+
| Constant | uppercase first letter | `APP = "myapp"` | Everywhere (namespaced to where defined) |
|
|
18
|
+
|
|
19
|
+
### Local variables
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
def build
|
|
23
|
+
output_dir = "dist" # only visible inside this method
|
|
24
|
+
sh "rake build OUTDIR=#{output_dir}"
|
|
25
|
+
end
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`output_dir` disappears when the method returns. It cannot be seen by any other task.
|
|
29
|
+
|
|
30
|
+
### Instance variables (`@`)
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
class Tasks
|
|
34
|
+
@app = "myapp" # class instance variable — lives on the Tasks class object
|
|
35
|
+
|
|
36
|
+
def build
|
|
37
|
+
puts @app # nil — this @app is on the instance, not the class
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`@` in the class body sets a variable on the class object itself, not on the instances that run tasks. Inside a task method body, `@name` refers to the instance, which is a different object. They do not share state.
|
|
43
|
+
|
|
44
|
+
`@` inside a method is useful for memoization within a single task invocation:
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
def version
|
|
48
|
+
@version ||= `git describe --tags`.strip # computed once, cached for this run
|
|
49
|
+
end
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Class variables (`@@`)
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
class Tasks
|
|
56
|
+
@@app = "myapp" # visible in every task method and every subclass
|
|
57
|
+
|
|
58
|
+
def build
|
|
59
|
+
puts @@app # "myapp"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`@@` is shared across the class body, all instance methods, and all subclasses (including Thor subcommand classes). This makes it the right choice for configuration values in Asgard task files.
|
|
65
|
+
|
|
66
|
+
### Global variables (`$`)
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
$DEBUG = true # visible everywhere in the Ruby process
|
|
70
|
+
$VERBOSE = true
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Asgard uses `$DEBUG` and `$VERBOSE` internally — they are set by the `--debug` and `--verbose` CLI flags. Avoid declaring your own `$` variables in `.loki` files; they affect the entire Ruby process including all loaded gems.
|
|
74
|
+
|
|
75
|
+
Several important Ruby globals you may encounter in task files:
|
|
76
|
+
|
|
77
|
+
| Variable | Purpose |
|
|
78
|
+
|----------|---------|
|
|
79
|
+
| `$stdout` / `$STDOUT` | Standard output stream — `puts` writes here |
|
|
80
|
+
| `$stderr` / `$STDERR` | Standard error stream — `warn` writes here |
|
|
81
|
+
| `$DEBUG` | Enables debug mode when `true` |
|
|
82
|
+
| `$VERBOSE` | Enables verbose warnings when `true` |
|
|
83
|
+
| `$PROGRAM_NAME` / `$0` | The name of the running script |
|
|
84
|
+
|
|
85
|
+
The uppercase versions (`$STDOUT`, `$STDERR`) are the original stream objects. The lowercase versions (`$stdout`, `$stderr`) are reassignable aliases — libraries sometimes redirect them temporarily to capture output. In task files, use `$stdout.puts` or `$stderr.puts` when you need explicit stream control; use plain `puts` and `warn` for normal output.
|
|
86
|
+
|
|
87
|
+
### Constants
|
|
88
|
+
|
|
89
|
+
Any name that begins with an uppercase letter is a constant in Ruby. Constants are available everywhere — inside methods, across files, and across classes — without any prefix:
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
APP_NAME = "myapp".freeze
|
|
93
|
+
MAX_RETRIES = 3
|
|
94
|
+
BASE_URL = "https://example.com".freeze
|
|
95
|
+
|
|
96
|
+
class Tasks
|
|
97
|
+
desc "Deploy the app"
|
|
98
|
+
def deploy
|
|
99
|
+
puts "Deploying #{APP_NAME} to #{BASE_URL}"
|
|
100
|
+
sh "cap deploy"
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The convention is `ALL_CAPS_WITH_UNDERSCORES` for values that are truly fixed. Class and module names are also constants — `Tasks`, `Asgard`, `String`, `Integer` all start with an uppercase letter.
|
|
106
|
+
|
|
107
|
+
Ruby will issue a warning if you reassign a constant but will not prevent it. Use `.freeze` to make the value itself immutable:
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
APP_NAME = "myapp".freeze # value cannot be mutated; reassignment still warns
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**Constants vs `@@` class variables in task files:**
|
|
114
|
+
|
|
115
|
+
| | Constant | `@@` class variable |
|
|
116
|
+
|---|---|---|
|
|
117
|
+
| Accessible in task methods | Yes | Yes |
|
|
118
|
+
| Accessible in subcommand subclasses | Yes | Yes |
|
|
119
|
+
| Visible outside the class | Yes — anywhere | Only within the class hierarchy |
|
|
120
|
+
| Reassignment warning | Yes | No |
|
|
121
|
+
| Convention | `ALL_CAPS` | `snake_case` |
|
|
122
|
+
|
|
123
|
+
For fixed values that will never change — app names, version strings, URLs, port numbers — constants are often the clearest choice. For values that might reasonably vary across environments or be overridden in a different `.loki` file, `@@` with `||=` is more flexible.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Strings and Interpolation
|
|
128
|
+
|
|
129
|
+
### Always use double quotes
|
|
130
|
+
|
|
131
|
+
Ruby supports both single and double quoted strings. In Asgard task files, always use double quotes:
|
|
132
|
+
|
|
133
|
+
```ruby
|
|
134
|
+
@@app ||= "myapp".freeze # correct
|
|
135
|
+
sh "bundle exec rake test" # correct
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Single-quoted strings look similar but behave differently — they do not support interpolation or escape sequences. Mixing the two styles adds confusion for no benefit. Double quotes work everywhere single quotes do, and more.
|
|
139
|
+
|
|
140
|
+
### String interpolation
|
|
141
|
+
|
|
142
|
+
Embedding a variable's value inside a string uses the `#{}` syntax. Everything inside the braces is Ruby code — the result is converted to a string and inserted in place:
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
class Tasks
|
|
146
|
+
@@app ||= "myapp".freeze
|
|
147
|
+
@@env ||= "production".freeze
|
|
148
|
+
|
|
149
|
+
desc "Deploy the app"
|
|
150
|
+
def deploy
|
|
151
|
+
sh "cap #{@@env} deploy APP=#{@@app}"
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Any Ruby expression works inside `#{}`:
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
puts "build started at #{Time.now}"
|
|
160
|
+
sh "puma -p #{env(:port, '3000').to_i + 1}"
|
|
161
|
+
sh "git tag #{@@app}-#{`git describe --tags`.strip}"
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Interpolation only works inside double-quoted strings. This is the primary reason Asgard tasks use double quotes exclusively — shell commands almost always need to embed variable values.
|
|
165
|
+
|
|
166
|
+
### Multi-line strings
|
|
167
|
+
|
|
168
|
+
For shell scripts with multiple lines, use a heredoc. The `~` modifier strips leading indentation so the script aligns with your code:
|
|
169
|
+
|
|
170
|
+
```ruby
|
|
171
|
+
desc "Bootstrap the project"
|
|
172
|
+
def bootstrap
|
|
173
|
+
sh <<~SHELL
|
|
174
|
+
bundle install
|
|
175
|
+
rails db:create db:migrate
|
|
176
|
+
echo "#{@@app} ready on #{env(:port, '3000')}"
|
|
177
|
+
SHELL
|
|
178
|
+
end
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Interpolation works inside heredocs the same as in double-quoted strings.
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## System Environment Variables
|
|
186
|
+
|
|
187
|
+
System environment variables are set outside Ruby — in the shell, a CI environment, or a `.env` file — and are accessed inside tasks via the `env` Kernel method:
|
|
188
|
+
|
|
189
|
+
```ruby
|
|
190
|
+
class Tasks
|
|
191
|
+
desc "Start the server"
|
|
192
|
+
def start
|
|
193
|
+
sh "puma -p #{env(:port, '3000')} -e #{env(:rack_env, 'development')}"
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
desc "Deploy the app"
|
|
197
|
+
def deploy
|
|
198
|
+
sh "cap #{env(:deploy_target)} deploy" # raises KeyError if DEPLOY_TARGET is not set
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
`env` accepts a symbol or string and converts it to an uppercase `ENV` key automatically:
|
|
204
|
+
|
|
205
|
+
| Call | Equivalent | Behaviour |
|
|
206
|
+
|------|-----------|-----------|
|
|
207
|
+
| `env(:port, "3000")` | `ENV.fetch("PORT", "3000")` | Returns `"3000"` if `PORT` is unset |
|
|
208
|
+
| `env(:api_key)` | `ENV.fetch("API_KEY")` | Raises `KeyError` if `API_KEY` is unset |
|
|
209
|
+
| `env("DATABASE_URL")` | `ENV.fetch("DATABASE_URL")` | Raises `KeyError` if unset |
|
|
210
|
+
| `env("database_url")` | `ENV.fetch("DATABASE_URL")` | Same — name is always upcased |
|
|
211
|
+
|
|
212
|
+
!!! note
|
|
213
|
+
All environment variable values are strings. Convert to other types explicitly: `env(:port, "3000").to_i`, `env(:debug, "false") == "true"`.
|
|
214
|
+
|
|
215
|
+
Use `dotenv` to load a `.env` file before tasks run — see [Environment](environment.md).
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## The Pattern
|
|
220
|
+
|
|
221
|
+
```ruby
|
|
222
|
+
class Tasks
|
|
223
|
+
@@app ||= "myapp".freeze
|
|
224
|
+
@@port ||= 3000
|
|
225
|
+
@@env ||= "production".freeze
|
|
226
|
+
|
|
227
|
+
desc "Print app info"
|
|
228
|
+
def info
|
|
229
|
+
puts "#{@@app} running on port #{@@port} in #{@@env}"
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
**Why `||=` instead of `=`?**
|
|
235
|
+
Because multiple `.loki` files reopen the same `class Tasks`. Using `||=` means the first file to declare a value wins, and subsequent files that reopen `Tasks` won't accidentally overwrite it.
|
|
236
|
+
|
|
237
|
+
**Why `.freeze`?**
|
|
238
|
+
It prevents mutation of the value (e.g. `@@app << "-extra"` raises a `FrozenError`). Numbers and symbols are already frozen. Use `.freeze` on strings, arrays, and hashes.
|
|
239
|
+
|
|
240
|
+
**Why `@@` instead of `@`?**
|
|
241
|
+
A single `@` in the class body sets a class instance variable — it lives on the `Tasks` class object and is **not** accessible inside task method bodies. `@@` is a class variable and is visible everywhere: in all instance methods, and in any subclass (including Thor subcommand classes).
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Sharing Values Across Subcommands
|
|
246
|
+
|
|
247
|
+
Class variables are visible in subclasses, which makes them the right choice when you have Thor subcommands defined in separate classes:
|
|
248
|
+
|
|
249
|
+
```ruby
|
|
250
|
+
# config.loki
|
|
251
|
+
class Tasks
|
|
252
|
+
@@app ||= "myapp".freeze
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# deploy.loki
|
|
256
|
+
class DeployCommands < Tasks
|
|
257
|
+
desc "Deploy to production"
|
|
258
|
+
def production
|
|
259
|
+
sh "cap production deploy APP=#{@@app}" # @@app is visible here
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
class Tasks
|
|
264
|
+
desc "deploy SUBCOMMAND", "Deployment tasks"
|
|
265
|
+
subcommand "deploy", DeployCommands
|
|
266
|
+
end
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## Computed Values
|
|
272
|
+
|
|
273
|
+
For values that require a shell call, file read, or any runtime computation, define a method instead:
|
|
274
|
+
|
|
275
|
+
```ruby
|
|
276
|
+
class Tasks
|
|
277
|
+
def version = `git describe --tags`.strip
|
|
278
|
+
def sha = `git rev-parse --short HEAD`.strip
|
|
279
|
+
|
|
280
|
+
desc "Show version info"
|
|
281
|
+
def info = puts "#{version} (#{sha})"
|
|
282
|
+
end
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Methods defined without `desc` do not appear in `--help` output or as CLI commands. If you need memoization (the computation is expensive and called multiple times), use `||=` on an instance variable inside the method:
|
|
286
|
+
|
|
287
|
+
```ruby
|
|
288
|
+
class Tasks
|
|
289
|
+
def version
|
|
290
|
+
@version ||= `git describe --tags`.strip
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## Sharing Values Across Files
|
|
298
|
+
|
|
299
|
+
Because all `.loki` files reopen the same `class Tasks`, a `@@` variable declared in one file is available in all other files loaded in the same session:
|
|
300
|
+
|
|
301
|
+
```ruby
|
|
302
|
+
# config.loki
|
|
303
|
+
class Tasks
|
|
304
|
+
@@app ||= "myapp".freeze
|
|
305
|
+
@@port ||= 8080
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# deploy.loki
|
|
309
|
+
class Tasks
|
|
310
|
+
desc "Deploy the app"
|
|
311
|
+
def deploy = sh "cap deploy APP=#{@@app} PORT=#{@@port}"
|
|
312
|
+
end
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
## Naming Conventions
|
|
318
|
+
|
|
319
|
+
Class variable names use `snake_case` — the standard Ruby convention for variables and methods:
|
|
320
|
+
|
|
321
|
+
```ruby
|
|
322
|
+
class Tasks
|
|
323
|
+
@@app_name ||= "myapp".freeze
|
|
324
|
+
@@deploy_host ||= "production.example.com".freeze
|
|
325
|
+
@@max_workers ||= 4
|
|
326
|
+
end
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
Multi-word names are separated by underscores, not camelCase or hyphens.
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
## Naming Caution
|
|
334
|
+
|
|
335
|
+
!!! warning
|
|
336
|
+
Avoid `@@` names that conflict with built-in Ruby or Thor internals. Safe practice: use descriptive names that are unlikely to clash.
|
|
337
|
+
|
|
338
|
+
Names to avoid: `options`, `shell`, `invoke`, `command`, `args`.
|
data/examples/.env
ADDED
data/examples/.loki
CHANGED
|
@@ -1,2 +1,24 @@
|
|
|
1
|
-
#
|
|
2
|
-
#
|
|
1
|
+
# examples/.loki — root task file for the examples directory.
|
|
2
|
+
#
|
|
3
|
+
# Asgard loads only this file by default. Everything else must be
|
|
4
|
+
# explicitly pulled in with import.
|
|
5
|
+
#
|
|
6
|
+
# import(path)
|
|
7
|
+
# Load a specific file or a glob of files, idempotently. Absolute
|
|
8
|
+
# paths, relative paths, and globs are all accepted.
|
|
9
|
+
#
|
|
10
|
+
# import_up(name)
|
|
11
|
+
# Walk CWD and every ancestor directory looking for `name`, then
|
|
12
|
+
# import it. Useful inside a nested task file that needs to pull in
|
|
13
|
+
# a shared config from the project root without knowing how deep it is.
|
|
14
|
+
#
|
|
15
|
+
# loki_up(name)
|
|
16
|
+
# Same walk as import_up but just returns the absolute path (or nil)
|
|
17
|
+
# without loading it — handy when you need the path for another purpose.
|
|
18
|
+
|
|
19
|
+
# Load all *.loki files in this directory (kitchen_sink, concurrent, etc.)
|
|
20
|
+
import "*.loki"
|
|
21
|
+
|
|
22
|
+
# Load a task file from a subdirectory.
|
|
23
|
+
# import also accepts a direct path, not just a glob.
|
|
24
|
+
import "subdir/import_demo.loki"
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# Demonstrates concurrent task execution via parallel depends_on groups.
|
|
3
|
+
#
|
|
4
|
+
# Each worker prints its letter repeatedly with random delays. Because
|
|
5
|
+
# worker_a, worker_b, and worker_c run in separate threads their output
|
|
6
|
+
# interleaves on stdout, proving real concurrency.
|
|
7
|
+
#
|
|
8
|
+
# Execution order:
|
|
9
|
+
# start → worker_a + worker_b + worker_c (all three concurrent) → finish
|
|
10
|
+
#
|
|
11
|
+
# Run with:
|
|
12
|
+
# asgard finish
|
|
13
|
+
#
|
|
14
|
+
# Sample output (character order varies every run):
|
|
15
|
+
# start
|
|
16
|
+
# ABCBACBACBABCBACBACB
|
|
17
|
+
# end
|
|
18
|
+
|
|
19
|
+
$stdout.sync = true # flush every print immediately across all threads
|
|
20
|
+
|
|
21
|
+
CONCURRENT_REPS = 10 # how many times each worker prints its character
|
|
22
|
+
|
|
23
|
+
class Tasks
|
|
24
|
+
desc "Print start marker"
|
|
25
|
+
def start
|
|
26
|
+
puts "starting demo of concurrent task execution ..."
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
desc "Print 'A' repeatedly with random delays"
|
|
30
|
+
def worker_a
|
|
31
|
+
CONCURRENT_REPS.times do
|
|
32
|
+
print "A"
|
|
33
|
+
sleep rand(0.05..0.3)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
desc "Print 'B' repeatedly with random delays"
|
|
38
|
+
def worker_b
|
|
39
|
+
CONCURRENT_REPS.times do
|
|
40
|
+
print "B"
|
|
41
|
+
sleep rand(0.05..0.3)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
desc "Print 'C' repeatedly with random delays"
|
|
46
|
+
def worker_c
|
|
47
|
+
CONCURRENT_REPS.times do
|
|
48
|
+
print "C"
|
|
49
|
+
sleep rand(0.05..0.3)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
depends_on :start, [:worker_a, :worker_b, :worker_c]
|
|
54
|
+
desc "Print end marker after all workers complete"
|
|
55
|
+
def finish
|
|
56
|
+
puts "\nfini - the end of concurrent task demo"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -39,7 +39,7 @@ class DBCommands < Tasks
|
|
|
39
39
|
# depends_on chains within the subcommand group:
|
|
40
40
|
# rollback → migrate → seed → reset
|
|
41
41
|
depends_on :rollback, :migrate, :seed
|
|
42
|
-
desc "
|
|
42
|
+
desc "Rollback all migrations, re-migrate, and reseed"
|
|
43
43
|
def reset
|
|
44
44
|
puts "Database reset complete."
|
|
45
45
|
end
|
|
@@ -54,7 +54,7 @@ class DBCommands < Tasks
|
|
|
54
54
|
asgard db console\x5
|
|
55
55
|
asgard db console --env staging
|
|
56
56
|
DESC
|
|
57
|
-
desc "
|
|
57
|
+
desc "Open an interactive database console"
|
|
58
58
|
option :env, type: :string, default: "development",
|
|
59
59
|
enum: %w[development staging production],
|
|
60
60
|
desc: "Environment to connect to"
|
|
@@ -62,7 +62,7 @@ class DBCommands < Tasks
|
|
|
62
62
|
puts "Opening #{options[:env]} database console..."
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
-
desc "
|
|
65
|
+
desc "Show applied and pending migrations"
|
|
66
66
|
def status
|
|
67
67
|
puts "Checking migration status..."
|
|
68
68
|
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# Demonstrates the env() Kernel helper for reading system environment variables.
|
|
3
|
+
#
|
|
4
|
+
# env() is cleaner than ENV[] — it accepts symbols or strings, upcases
|
|
5
|
+
# automatically, and raises KeyError when a required variable is missing
|
|
6
|
+
# rather than silently returning nil.
|
|
7
|
+
#
|
|
8
|
+
# Run from examples/ or any subdirectory — loki_up locates .env automatically:
|
|
9
|
+
# asgard env_demo
|
|
10
|
+
|
|
11
|
+
class Tasks
|
|
12
|
+
# loki_up walks CWD and every ancestor until it finds .env, returning
|
|
13
|
+
# its absolute path. This works regardless of which directory asgard
|
|
14
|
+
# is run from — no hardcoded relative path needed.
|
|
15
|
+
dotenv loki_up(".env") || ".env"
|
|
16
|
+
|
|
17
|
+
desc "Print environment variable values resolved via env()"
|
|
18
|
+
def env_demo
|
|
19
|
+
puts <<~OUT
|
|
20
|
+
APP_NAME : #{env(:app_name)}
|
|
21
|
+
PORT : #{env(:port)}
|
|
22
|
+
API_KEY : #{env(:api_key)}
|
|
23
|
+
DATABASE_URL : #{env(:database_url)}
|
|
24
|
+
LOG_LEVEL : #{env(:log_level, "info")}
|
|
25
|
+
OUT
|
|
26
|
+
end
|
|
27
|
+
end
|
data/examples/kitchen_sink.loki
CHANGED
|
@@ -5,9 +5,20 @@
|
|
|
5
5
|
# install, release) so this file can be loaded alongside them without conflict.
|
|
6
6
|
|
|
7
7
|
class Tasks
|
|
8
|
-
# ──
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
# ── Ruby class variables — shared across all tasks and subcommands ──────────
|
|
9
|
+
@@app_name ||= "my_app".freeze
|
|
10
|
+
@@build_dir ||= "builds/#{@@app_name}".freeze
|
|
11
|
+
|
|
12
|
+
# ── Computed values — plain private methods replace the removed var DSL ──────
|
|
13
|
+
# Declare as private so Thor does not try to register them as commands.
|
|
14
|
+
# Use @ivar ||= inside the method body to memoize expensive calls.
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def version = `git describe --tags --always`.strip
|
|
18
|
+
def sha = `git rev-parse --short HEAD`.strip
|
|
19
|
+
def branch = @branch ||= `git rev-parse --abbrev-ref HEAD`.strip
|
|
20
|
+
|
|
21
|
+
public
|
|
11
22
|
|
|
12
23
|
# ── Asgard: dotenv — load environment variables ────────────────────────────
|
|
13
24
|
# Uncomment to activate:
|
|
@@ -37,9 +48,9 @@ class Tasks
|
|
|
37
48
|
map "pl" => :pipeline
|
|
38
49
|
|
|
39
50
|
# ── Basic task — no parameters ─────────────────────────────────────────────
|
|
40
|
-
desc "
|
|
51
|
+
desc "Say hello (default task when no command is given)"
|
|
41
52
|
def greet
|
|
42
|
-
puts "Hello from #{app_name} (#{options[:env]}
|
|
53
|
+
puts "Hello from #{@@app_name} #{version} (#{branch}) in #{options[:env]} mode!"
|
|
43
54
|
end
|
|
44
55
|
|
|
45
56
|
# ── Positional parameter with default ──────────────────────────────────────
|
|
@@ -72,14 +83,14 @@ class Tasks
|
|
|
72
83
|
end
|
|
73
84
|
|
|
74
85
|
# ── method_option — all five option types ──────────────────────────────────
|
|
75
|
-
desc "
|
|
86
|
+
desc "Compile the project"
|
|
76
87
|
option :output, aliases: "-o", type: :string, default: "dist/", desc: "Output directory"
|
|
77
88
|
option :verbose, aliases: "-v", type: :boolean, default: false, desc: "Enable verbose output"
|
|
78
89
|
option :jobs, aliases: "-j", type: :numeric, default: 1, desc: "Number of parallel jobs"
|
|
79
90
|
option :tags, type: :array, desc: "Build tags to apply"
|
|
80
91
|
option :defines, type: :hash, desc: "Preprocessor defines (KEY:VALUE)"
|
|
81
92
|
def compile
|
|
82
|
-
puts "Compiling #{app_name} → #{options[:output]}"
|
|
93
|
+
puts "Compiling #{@@app_name} → #{options[:output]}"
|
|
83
94
|
end
|
|
84
95
|
|
|
85
96
|
# ── required option + enum + banner ────────────────────────────────────────
|
|
@@ -99,7 +110,7 @@ class Tasks
|
|
|
99
110
|
default: "main",
|
|
100
111
|
desc: "Git branch to deploy"
|
|
101
112
|
def deploy(env = "staging")
|
|
102
|
-
puts "Deploying #{app_name}@#{options[:branch]} to #{env}..."
|
|
113
|
+
puts "Deploying #{@@app_name}@#{options[:branch]} to #{env}..."
|
|
103
114
|
end
|
|
104
115
|
|
|
105
116
|
# ── long_desc — extended help shown by `asgard help report` ────────────────
|
|
@@ -115,7 +126,7 @@ class Tasks
|
|
|
115
126
|
asgard report --format json --output report.json\x5
|
|
116
127
|
asgard rp --format text
|
|
117
128
|
LONGDESC
|
|
118
|
-
desc "
|
|
129
|
+
desc "Generate a project report"
|
|
119
130
|
option :format, type: :string, default: "text", enum: %w[text html json], desc: "Output format"
|
|
120
131
|
option :since, type: :string, banner: "DATE", desc: "Limit to changes after DATE"
|
|
121
132
|
option :output, type: :string, banner: "FILE", desc: "Write output to FILE"
|
|
@@ -124,30 +135,52 @@ class Tasks
|
|
|
124
135
|
end
|
|
125
136
|
|
|
126
137
|
# ── Asgard depends_on: sequential — analyze runs before spec ───────────────
|
|
127
|
-
desc "
|
|
138
|
+
desc "Check code style and complexity"
|
|
128
139
|
def analyze = puts "Analyzing..."
|
|
129
140
|
|
|
130
141
|
depends_on :analyze
|
|
131
|
-
desc "
|
|
142
|
+
desc "Run the test suite (depends on: analyze)"
|
|
132
143
|
def spec = puts "Running specs..."
|
|
133
144
|
|
|
134
145
|
# ── Asgard depends_on: parallel — analyze and typecheck run concurrently ───
|
|
135
|
-
desc "
|
|
146
|
+
desc "Run the type checker"
|
|
136
147
|
def typecheck = puts "Type checking..."
|
|
137
148
|
|
|
138
149
|
depends_on [:analyze, :typecheck]
|
|
139
|
-
desc "
|
|
150
|
+
desc "Run analyze and typecheck in parallel"
|
|
140
151
|
def check = puts "All checks passed."
|
|
141
152
|
|
|
142
153
|
# ── Asgard depends_on: mixed sequential + parallel ─────────────────────────
|
|
143
|
-
desc "
|
|
154
|
+
desc "Create distribution archive"
|
|
144
155
|
def pack = puts "Packing..."
|
|
145
156
|
|
|
146
157
|
# check → compile+spec (parallel) → pack → pipeline
|
|
147
158
|
depends_on :check, [:compile, :spec], :pack
|
|
148
|
-
desc "
|
|
159
|
+
desc "Full pipeline: check → compile+spec → pack"
|
|
149
160
|
def pipeline = puts "Pipeline complete."
|
|
150
161
|
|
|
162
|
+
# ── Asgard: debug? / verbose? — Kernel predicates for conditional output ──────
|
|
163
|
+
# debug? returns true when --debug is passed (sets $DEBUG)
|
|
164
|
+
# verbose? returns true when --verbose is passed (sets $VERBOSE)
|
|
165
|
+
# Both are set by Asgard before invoke_command runs, so they are safe
|
|
166
|
+
# to read inside any task body.
|
|
167
|
+
#
|
|
168
|
+
# Run with:
|
|
169
|
+
# asgard status --verbose
|
|
170
|
+
# asgard status --debug
|
|
171
|
+
# asgard status --verbose --debug
|
|
172
|
+
desc "Show application status"
|
|
173
|
+
def status
|
|
174
|
+
puts "#{@@app_name} is running in #{options[:env]} mode."
|
|
175
|
+
puts " build dir : #{@@build_dir}" if verbose?
|
|
176
|
+
puts " sha : #{current_sha}" if verbose?
|
|
177
|
+
if debug?
|
|
178
|
+
puts " $DEBUG : #{$DEBUG.inspect}"
|
|
179
|
+
puts " $VERBOSE : #{$VERBOSE.inspect}"
|
|
180
|
+
puts " options : #{options.inspect}"
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
151
184
|
# ── Thor: no_commands — public helper excluded from CLI and --help ──────────
|
|
152
185
|
no_commands do
|
|
153
186
|
def current_sha
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# Demonstrates Thor subcommands registered on the top-level Tasks class.
|
|
3
3
|
#
|
|
4
4
|
# The subcommand class inherits from Tasks so it has access to sh, shebang,
|
|
5
|
-
#
|
|
5
|
+
# depends_on, and the built-in --debug/--verbose class options.
|
|
6
6
|
#
|
|
7
7
|
# Usage:
|
|
8
8
|
# asgard server # shows subcommand help
|
|
@@ -26,7 +26,7 @@ class ServerCommands < Tasks
|
|
|
26
26
|
]
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
desc "
|
|
29
|
+
desc "Stop the running server"
|
|
30
30
|
option :force, aliases: "-f", type: :boolean, default: false, desc: "Force-kill without draining"
|
|
31
31
|
option :wait, type: :numeric, default: 30, desc: "Seconds to wait for shutdown"
|
|
32
32
|
def stop
|
|
@@ -37,7 +37,7 @@ class ServerCommands < Tasks
|
|
|
37
37
|
end
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
-
desc "
|
|
40
|
+
desc "Show server status and process info"
|
|
41
41
|
def status
|
|
42
42
|
puts "Checking server status..."
|
|
43
43
|
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# examples/subdir/.loki — root task file for this subdirectory.
|
|
2
|
+
#
|
|
3
|
+
# When asgard is run from examples/subdir/, it loads this file.
|
|
4
|
+
# From here, import pulls in sibling task files explicitly.
|
|
5
|
+
#
|
|
6
|
+
# import accepts a direct path or a glob:
|
|
7
|
+
# import "import_up_demo.loki" # a single file
|
|
8
|
+
# import "*.loki" # all loki files in this directory
|
|
9
|
+
# import "tasks/**/*.loki" # all loki files under a tasks/ tree
|
|
10
|
+
# import "../shared/*.loki" # files in a sibling directory
|
|
11
|
+
|
|
12
|
+
import "import_up_demo.loki"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# This file lives in a subdirectory and is explicitly imported by the
|
|
3
|
+
# parent examples/.loki via import "subdir/import_demo.loki".
|
|
4
|
+
#
|
|
5
|
+
# In a real project a subdirectory might hold task files scoped to a
|
|
6
|
+
# specific concern (deploy, database, CI) while the root .loki wires
|
|
7
|
+
# them all together with import.
|
|
8
|
+
|
|
9
|
+
class Tasks
|
|
10
|
+
desc "Confirm this task was loaded from a subdirectory via import"
|
|
11
|
+
def subdir_task
|
|
12
|
+
puts "Loaded from examples/subdir/import_demo.loki"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# Demonstrates import_up — find a file by walking up the directory tree
|
|
3
|
+
# and load it, without needing to know how deep the current file is.
|
|
4
|
+
#
|
|
5
|
+
# import_up(name) combines loki_up(name) + import(path) in one call.
|
|
6
|
+
# It searches CWD, then each ancestor in turn, and loads the first match.
|
|
7
|
+
#
|
|
8
|
+
# This is useful when a nested task file needs to pull in something from
|
|
9
|
+
# the project root — a shared config, a common helpers file — without
|
|
10
|
+
# hardcoding a relative path that would break if the file moves.
|
|
11
|
+
#
|
|
12
|
+
# import_up also accepts a glob:
|
|
13
|
+
# import_up "*.loki" # first directory (walking up) that contains any .loki file
|
|
14
|
+
# import_up "config/settings.loki" # first ancestor with config/settings.loki
|
|
15
|
+
# import_up ".env" # locate a .env file anywhere up the tree
|
|
16
|
+
|
|
17
|
+
# Load env_usage.loki from the nearest ancestor directory that contains it.
|
|
18
|
+
import_up "env_usage.loki"
|