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.
data/docs/shell.md CHANGED
@@ -14,10 +14,10 @@ Pass a single-line string to run it via `system`:
14
14
 
15
15
  ```ruby
16
16
  class Tasks
17
- desc "build", "Compile the project"
17
+ desc "Compile the project"
18
18
  def build = sh "rake build"
19
19
 
20
- desc "clean", "Remove build artifacts"
20
+ desc "Remove build artifacts"
21
21
  def clean = sh "rm -rf dist/ tmp/"
22
22
  end
23
23
  ```
@@ -34,7 +34,7 @@ Pass a multiline string (e.g., a heredoc) to run it as a single `bash -c` script
34
34
 
35
35
  ```ruby
36
36
  class Tasks
37
- desc "setup", "Bootstrap the development environment"
37
+ desc "Bootstrap the development environment"
38
38
  def setup
39
39
  sh <<~SHELL
40
40
  brew install redis postgresql
@@ -54,10 +54,10 @@ Pass `silent: true` to suppress the command echo. The command still runs and sti
54
54
 
55
55
  ```ruby
56
56
  class Tasks
57
- desc "build", "Compile (quiet)"
57
+ desc "Compile (quiet)"
58
58
  def build = sh "rake build", silent: true
59
59
 
60
- desc "info", "Print environment info without noise"
60
+ desc "Print environment info without noise"
61
61
  def info
62
62
  sh "printenv | grep APP_", silent: true
63
63
  end
@@ -71,7 +71,7 @@ end
71
71
  ```ruby
72
72
  class Tasks
73
73
  depends_on :test
74
- desc "release", "Test then release"
74
+ desc "Test then release"
75
75
  def release
76
76
  sh "bundle exec rake release"
77
77
  # Never reached if rake release fails
@@ -90,7 +90,7 @@ end
90
90
 
91
91
  ```ruby
92
92
  class Tasks
93
- desc "analyze", "Run Python data analysis"
93
+ desc "Run Python data analysis"
94
94
  def analyze
95
95
  shebang :python3, <<~PYTHON
96
96
  import json
@@ -105,7 +105,7 @@ end
105
105
 
106
106
  ```ruby
107
107
  class Tasks
108
- desc "bundle_assets", "Build frontend assets with esbuild"
108
+ desc "Build frontend assets with esbuild"
109
109
  def bundle_assets
110
110
  shebang :node, <<~JS
111
111
  const esbuild = require("esbuild")
@@ -123,7 +123,7 @@ end
123
123
 
124
124
  ```ruby
125
125
  class Tasks
126
- desc "transform", "Transform data with Ruby"
126
+ desc "Transform data with Ruby"
127
127
  def transform
128
128
  shebang :ruby, <<~RUBY
129
129
  require "json"
@@ -138,7 +138,7 @@ end
138
138
 
139
139
  ```ruby
140
140
  class Tasks
141
- desc "provision", "Run a bash provisioning script"
141
+ desc "Run a bash provisioning script"
142
142
  def provision
143
143
  shebang :bash, <<~BASH
144
144
  set -euo pipefail
@@ -192,7 +192,7 @@ You can mix both in the same task:
192
192
 
193
193
  ```ruby
194
194
  class Tasks
195
- desc "pipeline", "Run a mixed shell + Python pipeline"
195
+ desc "Run a mixed shell + Python pipeline"
196
196
  def pipeline
197
197
  sh "bundle exec rake build"
198
198
 
data/docs/subcommands.md CHANGED
@@ -10,10 +10,10 @@ Define a subcommand class that inherits from `Tasks`, then register it on the to
10
10
 
11
11
  ```ruby
12
12
  class DeployCommands < Tasks
13
- desc "staging", "Deploy to staging"
13
+ desc "Deploy to staging"
14
14
  def staging = sh "cap staging deploy"
15
15
 
16
- desc "production", "Deploy to production"
16
+ desc "Deploy to production"
17
17
  def production = sh "cap production deploy"
18
18
  end
19
19
 
@@ -37,8 +37,8 @@ Inheriting from `Tasks` (rather than `Asgard::Base` or `Thor`) gives the subcomm
37
37
 
38
38
  - `sh` and `shebang` shell helpers (from `Asgard::Shell`)
39
39
  - `depends_on` for dependency declarations
40
- - `var` for variables
41
40
  - `dotenv` for environment loading
41
+ - `@@` class variables declared on `Tasks` (visible in all subclasses)
42
42
  - The built-in `--debug` and `--verbose` class options
43
43
  - The `debug?` and `verbose?` private predicates
44
44
  - Any private helpers or `no_commands` methods defined on `Tasks`
@@ -54,14 +54,14 @@ Inheriting from `Tasks` (rather than `Asgard::Base` or `Thor`) gives the subcomm
54
54
 
55
55
  ```ruby
56
56
  class DBCommands < Tasks
57
- desc "migrate", "Run pending migrations"
57
+ desc "Run pending migrations"
58
58
  def migrate = sh "rails db:migrate"
59
59
 
60
- desc "seed", "Load seed data"
60
+ desc "Load seed data"
61
61
  def seed = sh "rails db:seed"
62
62
 
63
63
  depends_on :migrate, :seed
64
- desc "reset", "Migrate then seed"
64
+ desc "Migrate then seed"
65
65
  def reset = puts "Done."
66
66
  end
67
67
 
@@ -94,13 +94,13 @@ class ServerCommands < Tasks
94
94
  sh "puma -p #{port} #{flags.join(' ')}"
95
95
  end
96
96
 
97
- desc "stop", "Stop the running server"
97
+ desc "Stop the running server"
98
98
  option :force, aliases: "-f", type: :boolean, default: false, desc: "Force-kill without draining"
99
99
  def stop
100
100
  options[:force] ? sh "pkill -9 puma" : sh "pumactl stop"
101
101
  end
102
102
 
103
- desc "status", "Show server status"
103
+ desc "Show server status"
104
104
  def status = sh "pumactl stats"
105
105
 
106
106
  depends_on :stop, :start
@@ -176,6 +176,6 @@ myproject/
176
176
  db_subcommands.loki ← defines DBCommands
177
177
  ```
178
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.
179
+ Because siblings loaded via `import "*.loki"` execute before `.loki`'s own class body, both `DBCommands` and `ServerCommands` are defined by the time `.loki` runs its `subcommand` calls.
180
180
 
181
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 CHANGED
@@ -1,14 +1,12 @@
1
1
  # Task Files
2
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`.
3
+ Asgard uses a convention-based file discovery system. A hidden `.loki` file marks the project root. Everything else — loading sibling files, shared task libraries, monorepo-wide tasks is controlled explicitly from inside your `.loki` file using the `import` and `import_up` Kernel methods.
4
4
 
5
5
  ---
6
6
 
7
7
  ## The `.loki` Root Marker
8
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:
9
+ When you run `asgard`, it searches for a `.loki` file starting in the current working directory and walking upward through parent directories until it finds one or reaches the filesystem root. The first `.loki` file found marks the project root and is the only file Asgard loads automatically.
12
10
 
13
11
  ```
14
12
  myproject/
@@ -18,143 +16,223 @@ myproject/
18
16
  # asgard still works from here
19
17
  ```
20
18
 
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.
19
+ The `.loki` file can be completely empty — its presence alone marks the project root. It can also contain task definitions, `import` calls, or any valid Ruby.
23
20
 
24
21
  ---
25
22
 
26
- ## Loading `*.loki` Files with `--auto-load`
23
+ ## Loading Files with `import`
24
+
25
+ `import` is a Kernel method available everywhere in Ruby — at the top level of `.loki` files, inside class bodies, and inside task method bodies. It loads `.loki` files with `require`-like idempotency: a file is loaded at most once per process, no matter how many times `import` is called with the same path.
27
26
 
28
- By default, `asgard` only loads `.loki`. To also load `*.loki` files, pass `--auto-load`:
27
+ ### Single file by absolute path
29
28
 
30
- ```bash
31
- asgard --auto-load <task>
29
+ ```ruby
30
+ import "/home/shared/gem_tasks.loki"
32
31
  ```
33
32
 
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.
33
+ ### Single file by relative path
35
34
 
36
- **Load order when `--auto-load` is passed:**
35
+ Relative paths are resolved relative to the **caller's file location**, like `require_relative`:
37
36
 
38
- 1. All `*.loki` files alphabetically (e.g., `build.loki`, `deploy.loki`, `test.loki`)
39
- 2. `.loki` itself (the entry point)
37
+ ```ruby
38
+ # .loki relative to this file's directory
39
+ import "build.loki"
40
+ import "../shared/gem_tasks.loki"
41
+ import "tasks/ci.loki"
42
+ ```
40
43
 
41
- This means any tasks, classes, or variables defined in `*.loki` files are available when `.loki` runs.
44
+ ### All files in the same directory (glob)
42
45
 
43
- ### Task Name Overloading
46
+ ```ruby
47
+ import "*.loki" # all *.loki files in the same directory as the calling file
48
+ ```
44
49
 
45
- Because all `*.loki` files reopen the same `class Tasks`, it is possibleby 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.
50
+ `*.loki` never matches `.loki` (the dotfile entry point) — Ruby's `Dir.glob` excludes dotfiles from `*` patterns by default.
46
51
 
47
- Three things are overwritten when a task name is reused:
52
+ ### All files recursively (recursive glob)
48
53
 
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
+ ```ruby
55
+ import "**/*.loki" # every .loki file in this directory and all subdirectories
56
+ ```
54
57
 
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.
58
+ ### Specific named files
56
59
 
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.
60
+ ```ruby
61
+ import "gem_tasks.loki"
62
+ import "ci_tasks.loki"
63
+ ```
59
64
 
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:
65
+ ### Combining patterns
61
66
 
62
67
  ```ruby
63
- # build.loki (loaded first)
68
+ # .loki
69
+ import "*.loki" # load all siblings
70
+ import "../shared/*.loki" # load a parent-level shared library
71
+ ```
72
+
73
+ ### Typical `.loki` entry point
74
+
75
+ ```ruby
76
+ # .loki
77
+ import "*.loki" # load all sibling task files
78
+
64
79
  class Tasks
65
- desc "build", "Compile the project"
66
- def build
67
- sh "rake build"
68
- end
80
+ # any top-level task definitions or overrides
69
81
  end
70
82
  ```
71
83
 
84
+ ### Return value
85
+
86
+ `import` returns `true` if at least one file was newly loaded, `false` if all files were already loaded or no glob pattern matched any file. If a specific (non-glob) file does not exist, `import` raises `LoadError`.
87
+
72
88
  ```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 }
89
+ import("gem_tasks.loki") ? "loaded now" : "already loaded"
90
+ ```
77
91
 
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
92
+ ---
93
+
94
+ ## Finding Files with `loki_up`
95
+
96
+ `loki_up(name = ".loki")` searches `Dir.pwd` and each ancestor directory for a file with the given name, returning a `Pathname` or `nil`. It does **not** load the file — it only finds it.
97
+
98
+ Despite the name, `loki_up` is not limited to `.loki` files — it will locate any file by name. This makes it useful for finding shared config files, `.env` files, or any other resource that lives somewhere up the directory tree:
99
+
100
+ ```ruby
101
+ loki_up # finds .loki (the project root marker)
102
+ loki_up("gem_tasks.loki") # finds gem_tasks.loki in CWD or any ancestor
103
+ loki_up(".env") # finds the nearest .env file up the tree
104
+ loki_up("VERSION") # finds a VERSION file in CWD or any ancestor
105
+ ```
106
+
107
+ Use `loki_up` when you need the path for other purposes, or to check whether a file exists before deciding to load it:
108
+
109
+ ```ruby
110
+ if (path = loki_up("gem_tasks.loki"))
111
+ import path
83
112
  end
113
+
114
+ # Pass the located .env to dotenv — works from any subdirectory
115
+ dotenv loki_up(".env") || ".env"
84
116
  ```
85
117
 
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.
118
+ `loki_up` accepts exact filenames only. Glob patterns are not expanded by `loki_up` use `import_up` for glob-aware ancestor search.
87
119
 
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.
120
+ ---
90
121
 
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.
122
+ ## Loading Files Found up the Tree with `import_up`
93
123
 
94
- The idiomatic Asgard solution is to express the relationship explicitly with `depends_on`:
124
+ `import_up(name = ".loki")` combines `loki_up` and `import` into a single call. It finds the file (or files) up the ancestor chain and loads them.
95
125
 
96
- ```ruby
97
- # build.loki
98
- class Tasks
99
- desc "build", "Compile the project"
100
- def build = sh "rake build"
126
+ ### Exact filename
101
127
 
102
- desc "copy_assets", "Copy build output to public/"
103
- def copy_assets = sh "cp -r dist/ public/"
128
+ ```ruby
129
+ import_up "gem_tasks.loki"
130
+ ```
104
131
 
105
- depends_on :build, :copy_assets
106
- desc "build_all", "Compile and copy assets"
107
- def build_all; end
108
- end
109
- ```
132
+ Walks up from `Dir.pwd` until it finds `gem_tasks.loki`, then loads it. Returns `false` if not found anywhere.
133
+
134
+ ### Glob pattern
135
+
136
+ ```ruby
137
+ import_up "*.loki"
138
+ ```
139
+
140
+ Walks up from `Dir.pwd` and stops at the **first ancestor directory** that contains any `*.loki` files, loading all of them. It does not aggregate matches from multiple levels — it loads only the nearest match, then stops.
141
+
142
+ ```
143
+ ~/sandbox/
144
+ gem_tasks.loki ← loaded by import_up("*.loki") from ~/sandbox/myproject/sub/
145
+ ci_tasks.loki ← also loaded — same directory as the first match
146
+ myproject/
147
+ .loki
148
+ sub/
149
+ # Dir.pwd here; import_up("*.loki") finds ~/sandbox/*.loki files
150
+ ```
151
+
152
+ ### Return value
153
+
154
+ Returns `true` if any file was newly loaded, `false` if the file was not found or was already loaded.
155
+
156
+ ### Conditional load
157
+
158
+ Since `import_up` returns `false` when a file is not found (rather than raising), it composes naturally with `||`:
110
159
 
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.
160
+ ```ruby
161
+ import_up("project_tasks.loki") || import_up("gem_tasks.loki")
162
+ ```
163
+
164
+ ---
165
+
166
+ ## Idempotency
167
+
168
+ Both `import` and `import_up` track loaded files in Ruby's `$LOADED_FEATURES`. A second call with the same path is a no-op and returns `false`. This means:
169
+
170
+ - You can call `import "*.loki"` from both `.loki` and a shared task file without double-loading.
171
+ - `import_up("gem_tasks.loki")` from two different projects in the same process each load their nearest match once.
172
+ - Swapping `require` for `import` in a `.loki` file gives the same once-per-process guarantee.
112
173
 
113
174
  ---
114
175
 
115
- ## Single File Layout
176
+ ## Verbose and Debug Feedback
116
177
 
117
- The simplest structure: all tasks in `.loki`, nothing else:
178
+ `import` and `import_up` emit diagnostic messages to stderr when the `verbose?` or `debug?` flags are active (set via `--verbose` or `--debug` on the CLI, or by setting `$VERBOSE`/`$DEBUG` directly):
179
+
180
+ | Flag | `import` output | `import_up` output |
181
+ |---|---|---|
182
+ | `--verbose` | Prints each file path as it is loaded | Prints `name → /full/path` when found |
183
+ | `--debug` | Same as verbose, plus prints a skip message for already-loaded files | Same as verbose, plus prints `name not found` when the search comes up empty |
118
184
 
119
185
  ```
120
- myproject/
121
- .loki
186
+ $ asgard --verbose build
187
+ import: /home/user/myproject/build.loki
188
+ import: /home/user/myproject/test.loki
122
189
  ```
123
190
 
191
+ ---
192
+
193
+ ## Loading Patterns
194
+
195
+ ### Single-file project
196
+
197
+ All tasks in `.loki`, nothing else:
198
+
124
199
  ```ruby
125
200
  # .loki
126
201
  class Tasks
127
- var :app, "myapp"
202
+ @@app ||= "myapp".freeze
128
203
 
129
- desc "build", "Compile the project"
204
+ desc "Compile the project"
130
205
  def build = sh "rake build"
131
206
 
132
- desc "test", "Run the test suite"
207
+ desc "Run the test suite"
133
208
  def test = sh "rake test"
134
209
 
135
- desc "release", "Build and push the gem"
136
- def release = sh "gem push pkg/#{app}-*.gem"
210
+ desc "Build and push the gem"
211
+ def release = sh "gem push pkg/#{@@app}-*.gem"
137
212
  end
138
213
  ```
139
214
 
140
- ---
141
-
142
- ## Multi-File Layout
215
+ ### Multi-file project
143
216
 
144
- Split tasks across files by concern each file reopens `class Tasks`:
217
+ Split tasks across files by concern. Load them all from `.loki` with a glob:
145
218
 
146
219
  ```
147
220
  myproject/
148
- .loki ← entry point (may be empty or contain top-level task)
149
- build.loki ← build-related tasks
150
- deploy.loki ← deployment tasks
221
+ .loki ← entry point; imports siblings
222
+ build.loki ← build tasks
223
+ deploy.loki ← deploy tasks
151
224
  test.loki ← test tasks
152
225
  ```
153
226
 
227
+ ```ruby
228
+ # .loki
229
+ import "*.loki"
230
+ ```
231
+
154
232
  ```ruby
155
233
  # build.loki
156
234
  class Tasks
157
- desc "build", "Compile the project"
235
+ desc "Compile the project"
158
236
  def build = sh "rake build"
159
237
  end
160
238
  ```
@@ -163,7 +241,7 @@ end
163
241
  # test.loki
164
242
  class Tasks
165
243
  depends_on :build
166
- desc "test", "Run the test suite"
244
+ desc "Run the test suite"
167
245
  def test = sh "bundle exec rake test"
168
246
  end
169
247
  ```
@@ -172,83 +250,158 @@ end
172
250
  # deploy.loki
173
251
  class Tasks
174
252
  depends_on :test
175
- desc "deploy", "Deploy to production"
253
+ desc "Deploy to production"
176
254
  def deploy = sh "cap production deploy"
177
255
  end
178
256
  ```
179
257
 
180
- ```ruby
181
- # .loki — can be empty, or can register subcommands, add top-level vars, etc.
182
- ```
258
+ Files loaded via glob are sorted alphabetically by `Dir.glob`, so `build.loki` loads before `test.loki`. Tasks defined in earlier files are available to later files via `depends_on`.
183
259
 
184
- Load order: `build.loki` → `deploy.loki` → `test.loki` → `.loki`.
260
+ ### Controlled load order
185
261
 
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.
262
+ When alphabetical order does not match your dependency order, import explicitly:
188
263
 
189
- ---
264
+ ```ruby
265
+ # .loki
266
+ import "infra.loki" # must be first
267
+ import "build.loki" # depends on infra
268
+ import "deploy.loki" # depends on build
269
+ ```
190
270
 
191
- ## Explicit Loading
271
+ ### Shared task library in a monorepo
192
272
 
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:
273
+ Place shared tasks in a parent directory and load them from any sub-project:
274
+
275
+ ```
276
+ ~/sandbox/
277
+ gem_tasks.loki ← shared: build, install, release tasks for any gem
278
+ myproject/
279
+ .loki ← loads gem_tasks.loki via import_up
280
+ other_project/
281
+ .loki ← also loads gem_tasks.loki via import_up
282
+ ```
194
283
 
195
284
  ```ruby
196
- # .loki
197
- require_relative "shared/helpers"
198
- require_relative "ci.loki"
285
+ # myproject/.loki
286
+ import_up "gem_tasks.loki" # finds ~/sandbox/gem_tasks.loki
199
287
 
200
288
  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'"
289
+ # project-specific overrides here
205
290
  end
206
291
  ```
207
292
 
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`.
293
+ ### Conditional shared library
209
294
 
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`.
295
+ ```ruby
296
+ # .loki
297
+ import_up("ci_tasks.loki") || import_up("gem_tasks.loki")
298
+ ```
212
299
 
213
- ---
300
+ Loads `ci_tasks.loki` if found up the tree, otherwise falls back to `gem_tasks.loki`.
214
301
 
215
- ## Subcommands Across Files
302
+ ### Subcommand classes across files
216
303
 
217
- Subcommand classes defined in separate `*.loki` files are available in `.loki` when `--auto-load` is used, because the `*.loki` files load first:
304
+ Define subcommand classes in separate files and register them in `.loki`:
218
305
 
219
306
  ```
220
307
  myproject/
221
- .loki ← registers subcommands
222
- db_subcommands.loki ← defines DBCommands
223
- server_subcommands.loki ← defines ServerCommands
308
+ .loki
309
+ db.loki
310
+ server.loki
224
311
  ```
225
312
 
226
313
  ```ruby
227
- # db_subcommands.loki
314
+ # db.loki
228
315
  class DBCommands < Tasks
229
- desc "migrate", "Run migrations"
316
+ desc "Run migrations"
230
317
  def migrate = sh "rails db:migrate"
231
318
  end
319
+ ```
232
320
 
233
- # server_subcommands.loki
321
+ ```ruby
322
+ # server.loki
234
323
  class ServerCommands < Tasks
235
- desc "start", "Start the server"
324
+ desc "Start the server"
236
325
  def start = sh "rails server"
237
326
  end
327
+ ```
238
328
 
329
+ ```ruby
239
330
  # .loki
331
+ import "*.loki" # db.loki and server.loki load first
332
+
240
333
  class Tasks
241
334
  desc "db SUBCOMMAND", "Manage the database"; subcommand "db", DBCommands
242
335
  desc "server SUBCOMMAND", "Manage the server"; subcommand "server", ServerCommands
243
336
  end
244
337
  ```
245
338
 
339
+ Subcommand classes are available in `.loki` because siblings loaded via `import "*.loki"` execute before `.loki`'s own class body.
340
+
341
+ ---
342
+
343
+ ## Task Name Overloading
344
+
345
+ Because all `*.loki` files reopen the same `class Tasks`, two files can define a method with the same name. Ruby's class reopening semantics apply: the last definition loaded wins, silently replacing the earlier one.
346
+
347
+ Three things are overwritten when a task name is reused:
348
+
349
+ | What | Effect |
350
+ |---|---|
351
+ | `def method_name` | The Ruby method body — the earlier implementation is gone |
352
+ | `desc` metadata | Thor registers the new usage/description string, discarding the old one |
353
+ | `depends_on` stages | `method_added` captures the pending deps for the new definition; the earlier dep chain is replaced |
354
+
355
+ **Accidental overloading** is a silent bug. Keep task names unique across files.
356
+
357
+ !!! warning
358
+ There is no runtime error when a task is overloaded. If a task is not behaving as expected, check whether another `.loki` file defines the same method name and loads after it.
359
+
360
+ **Intentional overloading** lets you extend a task defined in an earlier file using `alias_method`:
361
+
362
+ ```ruby
363
+ # build.loki (loaded first)
364
+ class Tasks
365
+ desc "Compile the project"
366
+ def build = sh "rake build"
367
+ end
368
+
369
+ # postbuild.loki (loaded after build.loki, alphabetically)
370
+ class Tasks
371
+ no_commands { alias_method :_build_original, :build }
372
+
373
+ desc "Compile the project and copy assets"
374
+ def build
375
+ _build_original
376
+ sh "cp -r dist/ public/"
377
+ end
378
+ end
379
+ ```
380
+
381
+ !!! warning "Prefer `depends_on` over intentional overloading"
382
+ Using `alias_method` to bolt post-task behaviour onto an existing task is fragile and load-order dependent. The idiomatic alternative is `depends_on`:
383
+
384
+ ```ruby
385
+ class Tasks
386
+ desc "Compile the project"
387
+ def build = sh "rake build"
388
+
389
+ desc "Copy build output to public/"
390
+ def copy_assets = sh "cp -r dist/ public/"
391
+
392
+ depends_on :build, :copy_assets
393
+ desc "Compile and copy assets"
394
+ def build_all; end
395
+ end
396
+ ```
397
+
246
398
  ---
247
399
 
248
400
  ## Summary of Loading Rules
249
401
 
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 |
402
+ | Method | Finds? | Loads? | Glob? | Ancestor search? |
403
+ |---|---|---|---|---|
404
+ | `loki_up(name)` | Yes (`Pathname`) | No | No | Yes |
405
+ | `import(path)` | No | Yes | Yes | No |
406
+ | `import_up(name)` | Yes | Yes | Yes | Yes |
407
+ | Asgard's `run!` | Yes | `.loki` only | No | Yes |