asgard 0.1.2 → 0.2.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/CHANGELOG.md +54 -1
- data/CLAUDE.md +2 -2
- data/README.md +39 -12
- data/docs/api.md +131 -0
- data/docs/assets/css/custom.css +93 -0
- data/docs/assets/images/asgard.jpg +0 -0
- data/docs/changelog.md +100 -0
- data/docs/dependencies.md +221 -0
- data/docs/environment.md +113 -0
- data/docs/examples.md +140 -0
- data/docs/getting-started.md +180 -0
- data/docs/helpers.md +154 -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 +254 -0
- data/docs/tasks.md +284 -0
- data/docs/variables.md +122 -0
- data/examples/concurrent.loki +58 -0
- data/lib/asgard/base.rb +90 -27
- data/lib/asgard/shell.rb +3 -1
- data/lib/asgard/tasks.rb +6 -0
- data/lib/asgard/version.rb +1 -1
- data/lib/asgard.rb +7 -2
- data/mkdocs.yml +164 -0
- metadata +20 -1
data/docs/subcommands.md
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Subcommands
|
|
2
|
+
|
|
3
|
+
Subcommands group related tasks under a common namespace, giving you commands like `asgard server start` or `asgard db migrate`. Asgard uses Thor's `subcommand` method for this, with one important convention: subcommand classes inherit from `Tasks` rather than from `Asgard::Base` or `Thor` directly.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Basic Pattern
|
|
8
|
+
|
|
9
|
+
Define a subcommand class that inherits from `Tasks`, then register it on the top-level `Tasks` class with `subcommand`:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
class DeployCommands < Tasks
|
|
13
|
+
desc "staging", "Deploy to staging"
|
|
14
|
+
def staging = sh "cap staging deploy"
|
|
15
|
+
|
|
16
|
+
desc "production", "Deploy to production"
|
|
17
|
+
def production = sh "cap production deploy"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class Tasks
|
|
21
|
+
desc "deploy SUBCOMMAND", "Deploy the application"
|
|
22
|
+
subcommand "deploy", DeployCommands
|
|
23
|
+
end
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
asgard deploy # shows deploy subcommand help
|
|
28
|
+
asgard deploy staging
|
|
29
|
+
asgard deploy production
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Why Inherit from `Tasks`?
|
|
35
|
+
|
|
36
|
+
Inheriting from `Tasks` (rather than `Asgard::Base` or `Thor`) gives the subcommand class access to:
|
|
37
|
+
|
|
38
|
+
- `sh` and `shebang` shell helpers (from `Asgard::Shell`)
|
|
39
|
+
- `depends_on` for dependency declarations
|
|
40
|
+
- `var` for variables
|
|
41
|
+
- `dotenv` for environment loading
|
|
42
|
+
- The built-in `--debug` and `--verbose` class options
|
|
43
|
+
- The `debug?` and `verbose?` private predicates
|
|
44
|
+
- Any private helpers or `no_commands` methods defined on `Tasks`
|
|
45
|
+
|
|
46
|
+
!!! warning
|
|
47
|
+
Do **not** redeclare `class_option :debug` or `class_option :verbose` in your subcommand class — they are already inherited from `Tasks`. Redeclaring them causes duplicate option errors.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## depends_on Within a Subcommand
|
|
52
|
+
|
|
53
|
+
`depends_on` works exactly as at the top level, scoped to the subcommand's own dependency graph:
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
class DBCommands < Tasks
|
|
57
|
+
desc "migrate", "Run pending migrations"
|
|
58
|
+
def migrate = sh "rails db:migrate"
|
|
59
|
+
|
|
60
|
+
desc "seed", "Load seed data"
|
|
61
|
+
def seed = sh "rails db:seed"
|
|
62
|
+
|
|
63
|
+
depends_on :migrate, :seed
|
|
64
|
+
desc "reset", "Migrate then seed"
|
|
65
|
+
def reset = puts "Done."
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
class Tasks
|
|
69
|
+
desc "db SUBCOMMAND", "Manage the database"
|
|
70
|
+
subcommand "db", DBCommands
|
|
71
|
+
end
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
asgard db reset # migrate → seed → reset
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Server Subcommand Example
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
class ServerCommands < Tasks
|
|
84
|
+
desc "start [PORT]", "Start the server on PORT (default: 3000)"
|
|
85
|
+
option :daemon, aliases: "-d", type: :boolean, default: false, desc: "Run as a background daemon"
|
|
86
|
+
option :workers, aliases: "-w", type: :numeric, default: 2, desc: "Number of worker processes"
|
|
87
|
+
option :log, type: :string, default: "log/server.log",
|
|
88
|
+
banner: "FILE", desc: "Write logs to FILE"
|
|
89
|
+
def start(port = "3000")
|
|
90
|
+
flags = []
|
|
91
|
+
flags << "--daemon" if options[:daemon]
|
|
92
|
+
flags << "--workers #{options[:workers]}"
|
|
93
|
+
flags << "--log #{options[:log]}"
|
|
94
|
+
sh "puma -p #{port} #{flags.join(' ')}"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
desc "stop", "Stop the running server"
|
|
98
|
+
option :force, aliases: "-f", type: :boolean, default: false, desc: "Force-kill without draining"
|
|
99
|
+
def stop
|
|
100
|
+
options[:force] ? sh "pkill -9 puma" : sh "pumactl stop"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
desc "status", "Show server status"
|
|
104
|
+
def status = sh "pumactl stats"
|
|
105
|
+
|
|
106
|
+
depends_on :stop, :start
|
|
107
|
+
desc "restart [PORT]", "Stop then start"
|
|
108
|
+
def restart(port = "3000") = puts "Server restarted on :#{port}."
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
class Tasks
|
|
112
|
+
desc "server SUBCOMMAND", "Manage the application server"
|
|
113
|
+
subcommand "server", ServerCommands
|
|
114
|
+
end
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
asgard server start
|
|
119
|
+
asgard server start 4000 --workers 4 --daemon
|
|
120
|
+
asgard server stop --force
|
|
121
|
+
asgard server restart
|
|
122
|
+
asgard server status
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Scoped DSL
|
|
128
|
+
|
|
129
|
+
Each subcommand class has its own independent scope for:
|
|
130
|
+
|
|
131
|
+
- `desc` / `long_desc` — documentation strings
|
|
132
|
+
- `method_option` / `option` — per-command options
|
|
133
|
+
- `class_option` — options shared across the subcommand's tasks (in addition to inherited ones)
|
|
134
|
+
- `map` — aliases within the subcommand group
|
|
135
|
+
- `default_task` — which command runs when the subcommand is invoked with no further arguments
|
|
136
|
+
- `depends_on` — dependency declarations scoped to this class
|
|
137
|
+
|
|
138
|
+
These do not bleed into the parent `Tasks` class or other subcommand classes.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Multiple Subcommands
|
|
143
|
+
|
|
144
|
+
You can register as many subcommand groups as needed:
|
|
145
|
+
|
|
146
|
+
```ruby
|
|
147
|
+
class ServerCommands < Tasks
|
|
148
|
+
# ... server tasks ...
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
class DBCommands < Tasks
|
|
152
|
+
# ... database tasks ...
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
class DeployCommands < Tasks
|
|
156
|
+
# ... deploy tasks ...
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
class Tasks
|
|
160
|
+
desc "server SUBCOMMAND", "Manage the server"; subcommand "server", ServerCommands
|
|
161
|
+
desc "db SUBCOMMAND", "Manage the database"; subcommand "db", DBCommands
|
|
162
|
+
desc "deploy SUBCOMMAND", "Deploy"; subcommand "deploy", DeployCommands
|
|
163
|
+
end
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Subcommands Across Files
|
|
169
|
+
|
|
170
|
+
Define each subcommand class in its own `.loki` file. Because all files reopen the same Ruby classes, the classes are available when `.loki` registers them:
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
myproject/
|
|
174
|
+
.loki ← registers all subcommands
|
|
175
|
+
server_subcommands.loki ← defines ServerCommands
|
|
176
|
+
db_subcommands.loki ← defines DBCommands
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
When `--auto-load` is used, `*.loki` files are loaded alphabetically before `.loki`, so both `DBCommands` and `ServerCommands` are defined by the time `.loki` runs its `subcommand` calls.
|
|
180
|
+
|
|
181
|
+
See [`examples/server_subcommands.loki`](examples.md#server-subcommands) and [`examples/db_subcommands.loki`](examples.md#db-subcommands) for complete working examples.
|
data/docs/task-files.md
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# Task Files
|
|
2
|
+
|
|
3
|
+
Asgard uses a convention-based file discovery system. A hidden `.loki` file marks the project root; `*.loki` files in the same directory contain tasks that are loaded on demand via `--auto-load`.
|
|
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. It may also contain the main task definitions for the project.
|
|
10
|
+
|
|
11
|
+
This means you can run `asgard` from any subdirectory of your project and it will find your tasks:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
myproject/
|
|
15
|
+
.loki ← found regardless of which subdirectory you're in
|
|
16
|
+
src/
|
|
17
|
+
app/
|
|
18
|
+
# asgard still works from here
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
!!! note
|
|
22
|
+
The `.loki` file can be completely empty. Its presence alone is sufficient to mark the project root. If it is empty and you have `*.loki` files, you must pass `--auto-load` when running `asgard` — otherwise Asgard has nothing to do.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Loading `*.loki` Files with `--auto-load`
|
|
27
|
+
|
|
28
|
+
By default, `asgard` only loads `.loki`. To also load `*.loki` files, pass `--auto-load`:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
asgard --auto-load <task>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
When `--auto-load` is active, Asgard loads all files matching `*.loki` in the same directory in alphabetical order before loading `.loki`. Each file typically reopens `class Tasks` to add more tasks. The `*.loki` glob specifically excludes `.loki` (note the leading dot) — the entry point is always loaded last.
|
|
35
|
+
|
|
36
|
+
**Load order when `--auto-load` is passed:**
|
|
37
|
+
|
|
38
|
+
1. All `*.loki` files alphabetically (e.g., `build.loki`, `deploy.loki`, `test.loki`)
|
|
39
|
+
2. `.loki` itself (the entry point)
|
|
40
|
+
|
|
41
|
+
This means any tasks, classes, or variables defined in `*.loki` files are available when `.loki` runs.
|
|
42
|
+
|
|
43
|
+
### Task Name Overloading
|
|
44
|
+
|
|
45
|
+
Because all `*.loki` files reopen the same `class Tasks`, it is possible — by accident or by design — for two files to define a method with the same name. This is **task overloading**. Ruby's class reopening semantics apply: the last definition loaded wins, silently replacing the earlier one.
|
|
46
|
+
|
|
47
|
+
Three things are overwritten when a task name is reused:
|
|
48
|
+
|
|
49
|
+
| What | Effect |
|
|
50
|
+
|---|---|
|
|
51
|
+
| `def method_name` | The Ruby method body — the earlier implementation is gone |
|
|
52
|
+
| `desc` metadata | Thor registers the new usage/description string, discarding the old one |
|
|
53
|
+
| `depends_on` stages | `method_added` captures the pending deps for the new definition; the earlier dep chain is replaced |
|
|
54
|
+
|
|
55
|
+
**Accidental overloading** is a silent bug. If `build.loki` and `ci.loki` both define `def build`, only the alphabetically-later file's version runs — with no warning. Keep task names unique across files, or move shared tasks into a dedicated file loaded first.
|
|
56
|
+
|
|
57
|
+
!!! warning
|
|
58
|
+
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.
|
|
59
|
+
|
|
60
|
+
**Intentional overloading** lets you extend or wrap a task defined in an earlier file. Use `alias_method` inside a `no_commands` block to preserve the original implementation under a private name, then redefine the task to call it:
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
# build.loki (loaded first)
|
|
64
|
+
class Tasks
|
|
65
|
+
desc "build", "Compile the project"
|
|
66
|
+
def build
|
|
67
|
+
sh "rake build"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
# postbuild.loki (loaded after build.loki, alphabetically)
|
|
74
|
+
class Tasks
|
|
75
|
+
# Preserve the original under a private name before overwriting it.
|
|
76
|
+
no_commands { alias_method :_build_original, :build }
|
|
77
|
+
|
|
78
|
+
desc "build", "Compile the project and copy assets"
|
|
79
|
+
def build
|
|
80
|
+
_build_original # runs the original sh "rake build"
|
|
81
|
+
sh "cp -r dist/ public/" # adds post-build step
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`no_commands` prevents `_build_original` from appearing as a CLI command. The aliased method retains the original's full body including any `sh` calls, `var` access, and private helper calls.
|
|
87
|
+
|
|
88
|
+
!!! tip
|
|
89
|
+
The `_` prefix on the alias name (`_build_original`) follows Asgard's convention for non-user-facing methods and reinforces that it is an implementation detail, not a task to be invoked directly.
|
|
90
|
+
|
|
91
|
+
!!! warning "Prefer `depends_on` over intentional overloading"
|
|
92
|
+
Using `alias_method` to bolt post-task behaviour onto an existing task is a code smell. It is fragile (load-order dependent), obscures intent, and makes the dependency chain invisible to Asgard's cycle-detection and deduplication logic.
|
|
93
|
+
|
|
94
|
+
The idiomatic Asgard solution is to express the relationship explicitly with `depends_on`:
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
# build.loki
|
|
98
|
+
class Tasks
|
|
99
|
+
desc "build", "Compile the project"
|
|
100
|
+
def build = sh "rake build"
|
|
101
|
+
|
|
102
|
+
desc "copy_assets", "Copy build output to public/"
|
|
103
|
+
def copy_assets = sh "cp -r dist/ public/"
|
|
104
|
+
|
|
105
|
+
depends_on :build, :copy_assets
|
|
106
|
+
desc "build_all", "Compile and copy assets"
|
|
107
|
+
def build_all; end
|
|
108
|
+
end
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
This approach is transparent, testable, and benefits from Asgard's deduplication — `build` will never run twice even if multiple tasks declare it as a dependency.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Single File Layout
|
|
116
|
+
|
|
117
|
+
The simplest structure: all tasks in `.loki`, nothing else:
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
myproject/
|
|
121
|
+
.loki
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
# .loki
|
|
126
|
+
class Tasks
|
|
127
|
+
var :app, "myapp"
|
|
128
|
+
|
|
129
|
+
desc "build", "Compile the project"
|
|
130
|
+
def build = sh "rake build"
|
|
131
|
+
|
|
132
|
+
desc "test", "Run the test suite"
|
|
133
|
+
def test = sh "rake test"
|
|
134
|
+
|
|
135
|
+
desc "release", "Build and push the gem"
|
|
136
|
+
def release = sh "gem push pkg/#{app}-*.gem"
|
|
137
|
+
end
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Multi-File Layout
|
|
143
|
+
|
|
144
|
+
Split tasks across files by concern — each file reopens `class Tasks`:
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
myproject/
|
|
148
|
+
.loki ← entry point (may be empty or contain top-level task)
|
|
149
|
+
build.loki ← build-related tasks
|
|
150
|
+
deploy.loki ← deployment tasks
|
|
151
|
+
test.loki ← test tasks
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
# build.loki
|
|
156
|
+
class Tasks
|
|
157
|
+
desc "build", "Compile the project"
|
|
158
|
+
def build = sh "rake build"
|
|
159
|
+
end
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
# test.loki
|
|
164
|
+
class Tasks
|
|
165
|
+
depends_on :build
|
|
166
|
+
desc "test", "Run the test suite"
|
|
167
|
+
def test = sh "bundle exec rake test"
|
|
168
|
+
end
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
```ruby
|
|
172
|
+
# deploy.loki
|
|
173
|
+
class Tasks
|
|
174
|
+
depends_on :test
|
|
175
|
+
desc "deploy", "Deploy to production"
|
|
176
|
+
def deploy = sh "cap production deploy"
|
|
177
|
+
end
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
```ruby
|
|
181
|
+
# .loki — can be empty, or can register subcommands, add top-level vars, etc.
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Load order: `build.loki` → `deploy.loki` → `test.loki` → `.loki`.
|
|
185
|
+
|
|
186
|
+
!!! tip
|
|
187
|
+
When `--auto-load` is used, `*.loki` files are sorted alphabetically, so `build.loki` loads before `test.loki`, which means `depends_on :build` in `test.loki` correctly references a task that already exists.
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Explicit Loading
|
|
192
|
+
|
|
193
|
+
You can explicitly load files from `.loki` using `require_relative`. This gives you control over load order, and lets you load plain Ruby files that are not `.loki` files:
|
|
194
|
+
|
|
195
|
+
```ruby
|
|
196
|
+
# .loki
|
|
197
|
+
require_relative "shared/helpers"
|
|
198
|
+
require_relative "ci.loki"
|
|
199
|
+
|
|
200
|
+
class Tasks
|
|
201
|
+
include BuildHelpers # defined in shared/helpers.rb
|
|
202
|
+
|
|
203
|
+
desc "full-ci", "Complete CI run"
|
|
204
|
+
def full_ci = sh "echo 'full CI complete'"
|
|
205
|
+
end
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Explicitly required files are loaded before `.loki`'s own class body is evaluated. Files loaded via `require_relative` are **not** re-loaded by the alphabetical glob — Ruby's `require_relative` marks them as loaded in `$LOADED_FEATURES`.
|
|
209
|
+
|
|
210
|
+
!!! warning
|
|
211
|
+
If you `require_relative "ci.loki"` from `.loki` and also run `asgard --auto-load`, Asgard's glob will also load `ci.loki`. To prevent double-loading, either: (a) put explicitly loaded files in a subdirectory outside the alphabetical sweep, or (b) rely solely on `--auto-load` without `require_relative`.
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Subcommands Across Files
|
|
216
|
+
|
|
217
|
+
Subcommand classes defined in separate `*.loki` files are available in `.loki` when `--auto-load` is used, because the `*.loki` files load first:
|
|
218
|
+
|
|
219
|
+
```
|
|
220
|
+
myproject/
|
|
221
|
+
.loki ← registers subcommands
|
|
222
|
+
db_subcommands.loki ← defines DBCommands
|
|
223
|
+
server_subcommands.loki ← defines ServerCommands
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
```ruby
|
|
227
|
+
# db_subcommands.loki
|
|
228
|
+
class DBCommands < Tasks
|
|
229
|
+
desc "migrate", "Run migrations"
|
|
230
|
+
def migrate = sh "rails db:migrate"
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# server_subcommands.loki
|
|
234
|
+
class ServerCommands < Tasks
|
|
235
|
+
desc "start", "Start the server"
|
|
236
|
+
def start = sh "rails server"
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# .loki
|
|
240
|
+
class Tasks
|
|
241
|
+
desc "db SUBCOMMAND", "Manage the database"; subcommand "db", DBCommands
|
|
242
|
+
desc "server SUBCOMMAND", "Manage the server"; subcommand "server", ServerCommands
|
|
243
|
+
end
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## Summary of Loading Rules
|
|
249
|
+
|
|
250
|
+
| File | When loaded | Purpose |
|
|
251
|
+
|---|---|---|
|
|
252
|
+
| `.loki` | After all `*.loki` | Project root marker; entry point |
|
|
253
|
+
| `*.loki` | When `--auto-load` is passed, alphabetically before `.loki` | Task definitions that reopen `class Tasks` |
|
|
254
|
+
| `require_relative` targets | At the point of the `require_relative` call | Shared helpers, explicit task files |
|