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.
@@ -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.
@@ -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 |