asgard 0.5.1 → 0.6.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/CHANGELOG.md +179 -99
- data/README.md +141 -46
- data/bin/asgard +10 -0
- data/lib/asgard/base/dispatch.rb +257 -31
- data/lib/asgard/base/registry.rb +7 -3
- data/lib/asgard/base.rb +140 -9
- data/lib/asgard/declarations.rb +21 -0
- data/lib/asgard/doctor/task_sections.rb +11 -3
- data/lib/asgard/doctor.rb +12 -8
- data/lib/asgard/jobs/commands.rb +152 -0
- data/lib/asgard/jobs/declaration.rb +149 -0
- data/lib/asgard/jobs/launcher.rb +109 -0
- data/lib/asgard/jobs/state.rb +190 -0
- data/lib/asgard/jobs/stopper.rb +141 -0
- data/lib/asgard/jobs.rb +25 -0
- data/lib/asgard/kernel_methods.rb +19 -14
- data/lib/asgard/leading_flag.rb +10 -0
- data/lib/asgard/pid.rb +130 -0
- data/lib/asgard/schedule/commands.rb +39 -11
- data/lib/asgard/schedule/declaration.rb +139 -12
- data/lib/asgard/schedule/job_files.rb +24 -0
- data/lib/asgard/schedule/launchd.rb +55 -18
- data/lib/asgard/schedule/systemd.rb +69 -27
- data/lib/asgard/schedule/table.rb +34 -16
- data/lib/asgard/schedule.rb +49 -9
- data/lib/asgard/shell.rb +372 -44
- data/lib/asgard/state_dir.rb +84 -0
- data/lib/asgard/tasks.rb +8 -0
- data/lib/asgard/version.rb +1 -1
- data/lib/asgard/watch/changes.rb +26 -1
- data/lib/asgard/watch/commands.rb +52 -12
- data/lib/asgard/watch/daemon.rb +334 -43
- data/lib/asgard/watch/declaration.rb +135 -40
- data/lib/asgard/watch/dispatcher.rb +61 -10
- data/lib/asgard/watch/mailbox.rb +50 -0
- data/lib/asgard/watch/reporter.rb +50 -34
- data/lib/asgard/watch/spawn.rb +41 -16
- data/lib/asgard/watch/state.rb +93 -27
- data/lib/asgard/watch/worker.rb +44 -9
- data/lib/asgard/watch.rb +4 -11
- data/lib/asgard.rb +80 -14
- metadata +167 -186
- data/.envrc +0 -6
- data/.github/workflows/deploy-github-pages.yml +0 -52
- data/.loki +0 -39
- data/.reek.yml +0 -166
- data/.rubocop.yml +0 -163
- data/Archspec.rb +0 -24
- data/CLAUDE.md +0 -139
- data/COMMITS.md +0 -196
- data/bin/console +0 -11
- data/bin/setup +0 -8
- data/doc_tasks.loki +0 -21
- data/docs/api.md +0 -368
- data/docs/assets/css/custom.css +0 -93
- data/docs/assets/images/asgard.jpg +0 -0
- data/docs/changelog.md +0 -1
- data/docs/dependencies.md +0 -313
- data/docs/environment.md +0 -129
- data/docs/examples.md +0 -194
- data/docs/getting-started.md +0 -181
- data/docs/helpers.md +0 -262
- data/docs/index.md +0 -87
- data/docs/options.md +0 -224
- data/docs/schedule.md +0 -106
- data/docs/shell.md +0 -237
- data/docs/subcommands.md +0 -181
- data/docs/task-files.md +0 -407
- data/docs/tasks.md +0 -326
- data/docs/variables.md +0 -338
- data/docs/watch.md +0 -123
- data/examples/.env +0 -4
- data/examples/.envrc +0 -3
- data/examples/.loki +0 -24
- data/examples/bad.loki +0 -63
- data/examples/concurrent.loki +0 -58
- data/examples/confirm.loki +0 -48
- data/examples/db_subcommands.loki +0 -74
- data/examples/depends_on_block/bad/.loki +0 -51
- data/examples/depends_on_block/good/.loki +0 -83
- data/examples/do.loki +0 -45
- data/examples/env_usage.loki +0 -27
- data/examples/kitchen_sink.loki +0 -210
- data/examples/server_subcommands.loki +0 -65
- data/examples/subdir/.loki +0 -12
- data/examples/subdir/import_demo.loki +0 -14
- data/examples/subdir/import_up_demo.loki +0 -18
- data/examples/watcher_examples/.envrc +0 -3
- data/examples/watcher_examples/.loki +0 -54
- data/examples/watcher_examples/README.md +0 -112
- data/examples/watcher_examples/bin/hello +0 -3
- data/examples/watcher_examples/data/records.json +0 -1
- data/examples/watcher_examples/docs/drafts/ideas.md +0 -5
- data/examples/watcher_examples/docs/index.md +0 -4
- data/examples/watcher_examples/lib/greeter.rb +0 -7
- data/gem_tasks.loki +0 -51
- data/git.loki +0 -13
- data/mkdocs.yml +0 -166
- data/quality.loki +0 -266
- data/quality_rails.loki +0 -47
- data/xyzzy.loki +0 -12
data/docs/api.md
DELETED
|
@@ -1,368 +0,0 @@
|
|
|
1
|
-
# API Reference
|
|
2
|
-
|
|
3
|
-
This page documents the public Ruby API for the Asgard gem. Most users interact with Asgard through the CLI and the task DSL — this page is primarily useful when integrating Asgard into tooling or extending it programmatically.
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## `Asgard` Module Methods
|
|
8
|
-
|
|
9
|
-
These class methods are defined on the `Asgard` module itself.
|
|
10
|
-
|
|
11
|
-
| Method | Signature | Description |
|
|
12
|
-
|---|---|---|
|
|
13
|
-
| `run!` | `Asgard.run!(argv)` | Main entry point. Finds `.loki`, loads all task files, validates the dependency graph, and dispatches via Thor. Handles its own errors: missing `.loki` and circular dependencies both produce a clean one-line message and `exit 1`. |
|
|
14
|
-
| `find_task_file` | `Asgard.find_task_file → Pathname, nil` | Searches `Dir.pwd` and each ancestor directory for a `.loki` file. Returns a `Pathname` of the first match, or `nil` if none is found. |
|
|
15
|
-
|
|
16
|
-
### `run!` Details
|
|
17
|
-
|
|
18
|
-
```ruby
|
|
19
|
-
Asgard.run!(ARGV)
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
`run!` guards against direct invocation of `_`-prefixed commands before any files are loaded:
|
|
23
|
-
|
|
24
|
-
```ruby
|
|
25
|
-
abort "asgard: unknown command '#{argv.first}'" if argv.first&.start_with?("_")
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
After loading task files, it calls `Tasks.validate_deps!` (circular dependency check) and `Tasks._reset_ran!` (clears per-invocation deduplication state) before starting Thor.
|
|
29
|
-
|
|
30
|
-
If the dispatched task returns exactly `:fail`, `run!` exits with status 1 (the quality-gate convention). Any other return value, including `:warn` and `:skip`, exits 0.
|
|
31
|
-
|
|
32
|
-
---
|
|
33
|
-
|
|
34
|
-
## Kernel Methods
|
|
35
|
-
|
|
36
|
-
These methods are defined as `module_function` on `Kernel` and are therefore available everywhere in Ruby — at the top level of `.loki` files, inside class bodies, and inside task method bodies. No `require` or `include` is needed; they are loaded when `asgard` starts.
|
|
37
|
-
|
|
38
|
-
| Method | Signature | Returns | Description |
|
|
39
|
-
|---|---|---|---|
|
|
40
|
-
| `loki_up` | `loki_up(name = ".loki") → Pathname, nil` | `Pathname` or `nil` | Searches `Dir.pwd` and each ancestor directory for a file named `name`. Returns a `Pathname` for the first match, or `nil` if not found. Exact filenames only — does not expand globs. |
|
|
41
|
-
| `import` | `import(path) → true, false` | `true` if any file newly loaded | Loads one `.loki` file or a glob of `.loki` files. Relative paths resolve relative to the caller's file (like `require_relative`). Idempotent via `$LOADED_FEATURES`. Raises `ArgumentError` if `path` does not end with `.loki`. Raises `LoadError` if a non-glob path does not exist. |
|
|
42
|
-
| `import_up` | `import_up(name = ".loki") → true, false` | `true` if any file newly loaded | Combines `loki_up` and `import`. Walks ancestors to find the file or glob match, then loads it. Returns `false` if nothing is found. |
|
|
43
|
-
| `changes` | `changes → Asgard::Changes` | `Asgard::Changes` | The files the watcher saw change when it triggered this task: `modified`, `added`, `removed`, `all`, `empty?`. Read once per process from `ASGARD_MODIFIED`, `ASGARD_ADDED` and `ASGARD_REMOVED`; every list is empty when the task is run by hand. See [`Asgard::Watch`](#asgardwatch). |
|
|
44
|
-
| `debug?` | `debug? → true, false` | `$DEBUG` | Returns the current value of `$DEBUG`. Set to `true` by `--debug` on the CLI or directly via `$DEBUG = true`. |
|
|
45
|
-
| `verbose?` | `verbose? → true, false` | `$VERBOSE` | Returns the current value of `$VERBOSE`. Set to `true` by `--verbose` on the CLI or directly via `$VERBOSE = true`. |
|
|
46
|
-
| `env` | `env(name, default = nil) → String, nil` | `ENV` value or default | Fetches an environment variable by symbol or string name. The name is upcased automatically. Raises `KeyError` when the variable is missing and no default is given. |
|
|
47
|
-
|
|
48
|
-
### `loki_up` Details
|
|
49
|
-
|
|
50
|
-
Despite the name, `loki_up` is not limited to `.loki` files — it locates any file by walking up the directory tree:
|
|
51
|
-
|
|
52
|
-
```ruby
|
|
53
|
-
loki_up # find .loki (the project root marker)
|
|
54
|
-
loki_up("gem_tasks.loki") # find gem_tasks.loki in CWD or any ancestor
|
|
55
|
-
loki_up(".env") # find the nearest .env file up the tree
|
|
56
|
-
loki_up("VERSION") # find a VERSION file in CWD or any ancestor
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
Returns a `Pathname` or `nil`. Does not load the file. `Pathname` is accepted by `import`, `dotenv`, `load`, and standard Ruby file methods — no `.to_s` conversion needed in common usage.
|
|
60
|
-
|
|
61
|
-
```ruby
|
|
62
|
-
if (path = loki_up("gem_tasks.loki"))
|
|
63
|
-
import path # Pathname accepted directly
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
# Pass the located .env to dotenv — works from any subdirectory
|
|
67
|
-
dotenv loki_up(".env") || ".env"
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
### `import` Details
|
|
71
|
-
|
|
72
|
-
```ruby
|
|
73
|
-
import "build.loki" # relative — resolved from the calling file's directory
|
|
74
|
-
import "/home/shared/gem_tasks.loki" # absolute
|
|
75
|
-
import "*.loki" # all *.loki in the same directory as the caller
|
|
76
|
-
import "../shared/*.loki" # all *.loki one level up
|
|
77
|
-
import "**/*.loki" # all *.loki recursively
|
|
78
|
-
import Pathname.new("tasks.loki") # Pathname accepted
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
**Extension requirement:** the path (or glob pattern) must end with `.loki`. Passing any other extension raises `ArgumentError`.
|
|
82
|
-
|
|
83
|
-
**Glob behaviour:** `Dir.glob` is used for pattern expansion. `*.loki` does not match `.loki` (the dotfile) — Ruby's glob excludes dotfiles from `*` by default. Files are loaded in the order `Dir.glob` returns them (sorted on Ruby ≥ 2.7).
|
|
84
|
-
|
|
85
|
-
**Idempotency:** each resolved absolute path is checked against `$LOADED_FEATURES` before loading. A file already in `$LOADED_FEATURES` is silently skipped and contributes `false` to the return value.
|
|
86
|
-
|
|
87
|
-
**Return value:** `true` if at least one file was newly loaded; `false` if all matched files were already loaded or no glob pattern produced any matches.
|
|
88
|
-
|
|
89
|
-
**Verbose/debug output** (to stderr):
|
|
90
|
-
- `verbose?` true — prints each file path as it is loaded
|
|
91
|
-
- `debug?` true — also prints a skip message for each already-loaded file
|
|
92
|
-
|
|
93
|
-
### `import_up` Details
|
|
94
|
-
|
|
95
|
-
```ruby
|
|
96
|
-
import_up # find and load .loki
|
|
97
|
-
import_up "gem_tasks.loki" # find and load gem_tasks.loki up the tree
|
|
98
|
-
import_up "*.loki" # find the nearest ancestor with *.loki files and load them all
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
**Exact name:** delegates to `loki_up` to find the file, then calls `import` with the absolute path. Returns `false` without raising if the file is not found.
|
|
102
|
-
|
|
103
|
-
**Glob name:** walks ancestor directories manually using `Dir.glob`. Stops at the **first** ancestor that has any matches and loads all of them — it does not continue walking after finding a match. Returns `false` if no ancestor contains matching files.
|
|
104
|
-
|
|
105
|
-
**Verbose/debug output** (to stderr):
|
|
106
|
-
- `verbose?` true — prints `name → /full/path` when a file or directory is found
|
|
107
|
-
- `debug?` true — also prints `name not found` when the search comes up empty
|
|
108
|
-
|
|
109
|
-
---
|
|
110
|
-
|
|
111
|
-
## `Asgard::Base` DSL Class Methods
|
|
112
|
-
|
|
113
|
-
`Asgard::Base` is a `Thor` subclass that provides the task DSL. It is the superclass of `Tasks`. All DSL methods are class methods (called in the class body).
|
|
114
|
-
|
|
115
|
-
| Method | Signature | Description |
|
|
116
|
-
|---|---|---|
|
|
117
|
-
| `depends_on` | `depends_on(*tasks, &block)` | Declare prerequisites for the next `def`. Bare symbols run sequentially; arrays within the splat run as a parallel group. A sole `Proc`/lambda, or a block in place of the splat args, defers resolution to `validate_deps!` (after every `.loki` file has loaded) instead of resolving immediately — see [Dynamic Dependencies](dependencies.md#dynamic-dependencies-proc-block-form). Passing both task arguments and a block raises `Asgard::Error`. |
|
|
118
|
-
| `dotenv` | `dotenv(path = ".env")` | Load the specified `.env` file into `ENV` using the dotenv gem. Silently skipped if the file does not exist. Called at class-load time. |
|
|
119
|
-
| `header` | `header(text)` | Append a line of text shown above the commands list in `asgard help`. Each call adds another line. No-op for per-command help. |
|
|
120
|
-
| `footer` | `footer(text)` | Prepend a line of text shown below the options block in `asgard help`. Each call inserts above the previous lines. No-op for per-command help. |
|
|
121
|
-
| `no_negate` | `no_negate(*names)` | Suppress `[--no-name]` / `[--skip-name]` help entries for one or more boolean class options. Call after the `class_option` declaration. |
|
|
122
|
-
| `sh` | `sh(script, silent: false, exec: false)` | Instance method. Run a shell command or multiline heredoc. Single-line → `system(script)`; multiline → `system("bash", "-c", script)`. Exits with the command's status on failure. With `exec: true`, replaces the asgard process via `Kernel.exec` instead of forking — see [Shell Helpers](shell.md#handing-off-with-exec). |
|
|
123
|
-
| `shebang` | `shebang(interpreter, script, silent: false)` | Instance method. Write `script` to a tempfile and execute it with `interpreter`. See the [Shell Helpers](shell.md) page for the full interpreter table. |
|
|
124
|
-
| `validate_deps!` | `Tasks.validate_deps!` | Build and topologically sort the full dependency graph using stdlib `TSort`. Raises `Asgard::CircularDependencyError` on cycles. Called by `run!` at startup. |
|
|
125
|
-
| `_reset_ran!` | `Tasks._reset_ran!` | Clear the per-invocation task deduplication set. Called by `run!` before dispatching. Thread-safe via Mutex. |
|
|
126
|
-
|
|
127
|
-
### `header` and `footer` Accumulation
|
|
128
|
-
|
|
129
|
-
Both `header` and `footer` accumulate across multiple calls and across multiple imported `.loki` files, but they accumulate in opposite directions by design.
|
|
130
|
-
|
|
131
|
-
**`header` appends** — each call adds to the bottom of the header block:
|
|
132
|
-
|
|
133
|
-
```ruby
|
|
134
|
-
# .loki
|
|
135
|
-
import "quality.loki"
|
|
136
|
-
import "gem_tasks.loki"
|
|
137
|
-
|
|
138
|
-
class Tasks
|
|
139
|
-
header "Project: myapp" # line 1
|
|
140
|
-
header "Root: #{loki_up.parent}" # line 2
|
|
141
|
-
end
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
Result in `asgard help`:
|
|
145
|
-
```
|
|
146
|
-
Project: myapp
|
|
147
|
-
Root: /home/user/myapp
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
**`footer` prepends** — each call inserts at the top of the footer block:
|
|
151
|
-
|
|
152
|
-
```ruby
|
|
153
|
-
class Tasks
|
|
154
|
-
footer "Github: https://github.com/org/myapp" # ends up second
|
|
155
|
-
footer "Docs: https://myapp.example.com" # ends up first
|
|
156
|
-
end
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
Result in `asgard help`:
|
|
160
|
-
```
|
|
161
|
-
Docs: https://myapp.example.com
|
|
162
|
-
Github: https://github.com/org/myapp
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
**Why the asymmetry?** When task files are split across multiple `.loki` files, the base `.loki` is loaded first and any imported files are loaded after. With `header`, earlier-loaded content appears first (the project banner stays at the top). With `footer`, later-loaded content appears first — this allows an imported file to inject a note that appears above the base footer rather than after it.
|
|
166
|
-
|
|
167
|
-
In practice: if only one file calls `header` and one file calls `footer`, the direction doesn't matter. The difference is visible only when multiple `.loki` files both call `header` or both call `footer`.
|
|
168
|
-
|
|
169
|
-
Neither `header` nor `footer` appears when running per-command help (`asgard help <task>`).
|
|
170
|
-
|
|
171
|
-
---
|
|
172
|
-
|
|
173
|
-
### `depends_on` Argument Shapes
|
|
174
|
-
|
|
175
|
-
```ruby
|
|
176
|
-
depends_on :build # single sequential dep
|
|
177
|
-
depends_on :clean, :build # two sequential deps
|
|
178
|
-
depends_on [:lint, :typecheck] # lint and typecheck run in parallel
|
|
179
|
-
depends_on :setup, [:lint, :build], :test # setup, then lint+build concurrently, then test
|
|
180
|
-
|
|
181
|
-
# A sole Proc/lambda (or, equivalently, a block) defers resolution to
|
|
182
|
-
# validate_deps!, after every .loki file has loaded — must return the same
|
|
183
|
-
# shape the splat form above would; the shape is validated when it resolves.
|
|
184
|
-
depends_on -> { [all_commands.keys.grep(/_check\z/).map(&:to_sym)] }
|
|
185
|
-
depends_on { [all_commands.keys.grep(/_check\z/).map(&:to_sym)] }
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
See [Dynamic Dependencies](dependencies.md#dynamic-dependencies-proc-block-form) for the full explanation of the Proc/block form — why it exists, when it runs, and how errors are reported.
|
|
189
|
-
|
|
190
|
-
---
|
|
191
|
-
|
|
192
|
-
## `Tasks` Built-ins
|
|
193
|
-
|
|
194
|
-
`Tasks` is pre-defined by the gem as `class Tasks < Asgard::Base`. It adds the following:
|
|
195
|
-
|
|
196
|
-
| Item | Type | Description |
|
|
197
|
-
|---|---|---|
|
|
198
|
-
| `class_option :debug` | class option | `--debug` flag. Sets `$DEBUG = true` before any task runs. Boolean, default `false`. |
|
|
199
|
-
| `class_option :verbose` | class option | `--verbose` flag. Sets `$VERBOSE = true` before any task runs. Boolean, default `false`. |
|
|
200
|
-
| `class_option :version` | class option | `--version` flag. Handled by `Asgard.run!` before the `.loki` file is loaded — prints `Asgard::VERSION` and exits. `no_negate :version` suppresses the `[--no-version]` / `[--skip-version]` help entries. |
|
|
201
|
-
| `class_option :doctor` | class option | `--doctor` flag. Handled by `Asgard.run!` before the `.loki` file is loaded — runs `Asgard::Doctor.new.run` and exits. `no_negate :doctor` suppresses the `[--no-doctor]` / `[--skip-doctor]` help entries. |
|
|
202
|
-
| `debug?` | Kernel module function | Returns `$DEBUG`. Available everywhere via `Kernel`. |
|
|
203
|
-
| `verbose?` | Kernel module function | Returns `$VERBOSE`. Available everywhere via `Kernel`. |
|
|
204
|
-
| `schedule` | class method (DSL) | `schedule :task, at:/every:, ...` — declares a scheduled entry. Extended from `Asgard::Schedule::DSL`; see [`Asgard::Schedule`](#asgardschedule). |
|
|
205
|
-
| `_schedule` | subcommand | `asgard schedule SUBCOMMAND`, backed by `Asgard::Schedule::Commands`. `map "schedule" => :_schedule` keeps the typed name `schedule`, and a user task named `schedule` cannot shadow it. |
|
|
206
|
-
| `watch` | class method (DSL) | `watch :task, paths: ...` — declares a watched entry. Extended from `Asgard::Watch::DSL`; see [`Asgard::Watch`](#asgardwatch). |
|
|
207
|
-
| `_watcher` | subcommand | `asgard watcher SUBCOMMAND`, backed by `Asgard::Watch::Commands`. `map "watcher" => :_watcher` keeps the typed name `watcher`. |
|
|
208
|
-
|
|
209
|
-
---
|
|
210
|
-
|
|
211
|
-
## `Asgard::Doctor`
|
|
212
|
-
|
|
213
|
-
`asgard --doctor` diagnoses `.loki` resolution, import chains, and task definitions for the current directory. It deliberately bypasses the normal `Tasks` boot sequence used by `run!`, so it can still report findings in situations that would otherwise abort the whole process — a broken `.loki` file, a circular or undefined dependency, or a task silently redefined by a later `def`.
|
|
214
|
-
|
|
215
|
-
| Method | Signature | Description |
|
|
216
|
-
|---|---|---|
|
|
217
|
-
| `new` | `Asgard::Doctor.new(dir = Dir.pwd)` | Builds a doctor scoped to `dir`. |
|
|
218
|
-
| `run` | `doctor.run` | Runs the full diagnostic pass and prints a report to stdout. Does not raise or exit — callers (like `Asgard.run!`) decide what to do afterward. |
|
|
219
|
-
| `ancestor_markers` | `Asgard::Doctor.ancestor_markers(dir) → Array<String>` | Every `.loki` marker from `dir` up to the filesystem root, nearest first. Unlike `loki_up`, it collects every match instead of stopping at the first, so shadowed ancestor markers can be reported. |
|
|
220
|
-
| `duplicate_methods` | `Asgard::Doctor.duplicate_methods(method_log) → Hash` | Given an `Asgard::Base` subclass's `_method_log`, returns the subset of entries defined at more than one location — same file or different — which is exactly what a silent `def` override looks like. |
|
|
221
|
-
|
|
222
|
-
Findings fall into three levels: `:info` (what was found — the marker used, each import's result), `:warn` (shadowed markers that are never reached), and `:error` (load failures, redefined methods, dependency graph problems).
|
|
223
|
-
|
|
224
|
-
After the findings, the report prints a **Tasks by file** section: every Thor command, grouped by the `.loki` file it's defined in, as `relative/path:line` — a format an editor can jump straight to. A task name defined at more than one location gets every definition annotated inline: the earlier one(s) are marked `OVERRIDDEN by <file>:<line> — never callable`, and the winning (last) definition is marked `active — redefines <file>:<line>`. This is the exact class of bug the flag was built to catch — a later `def` silently replacing an earlier one, with no error anywhere else in the toolchain. The report ends with a one-line summary and a count of problems/warnings, where each overridden task counts as one problem.
|
|
225
|
-
|
|
226
|
-
```bash
|
|
227
|
-
asgard --doctor
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
The asgard repo ships its own live example of this: `xyzzy.loki` defines a `xyzzy` task, and the top-level `.loki` (which imports it) reopens `Tasks` and defines `xyzzy` again — so the one from `xyzzy.loki` is silently dead. Clone the repo and run `asgard --doctor` from its root to see this for yourself:
|
|
231
|
-
|
|
232
|
-
```
|
|
233
|
-
asgard doctor -- /path/to/asgard
|
|
234
|
-
============================================================
|
|
235
|
-
[INFO] using .loki marker: /path/to/asgard/.loki
|
|
236
|
-
[INFO] import "quality.loki" -> /path/to/asgard/quality.loki
|
|
237
|
-
[INFO] import "gem_tasks.loki" -> /path/to/asgard/gem_tasks.loki
|
|
238
|
-
[INFO] import "git.loki" -> /path/to/asgard/git.loki
|
|
239
|
-
[INFO] import "xyzzy.loki" -> /path/to/asgard/xyzzy.loki
|
|
240
|
-
|
|
241
|
-
Tasks by file:
|
|
242
|
-
|
|
243
|
-
.loki
|
|
244
|
-
xyzzy .loki:15 active — redefines xyzzy.loki:6
|
|
245
|
-
|
|
246
|
-
quality.loki
|
|
247
|
-
test quality.loki:6
|
|
248
|
-
test_verbose quality.loki:15
|
|
249
|
-
quality quality.loki:21
|
|
250
|
-
rubocop quality.loki:34
|
|
251
|
-
rubocop_fix quality.loki:43
|
|
252
|
-
flog_check quality.loki:48
|
|
253
|
-
flay_check quality.loki:82
|
|
254
|
-
reek quality.loki:104
|
|
255
|
-
|
|
256
|
-
gem_tasks.loki
|
|
257
|
-
console gem_tasks.loki:6
|
|
258
|
-
build gem_tasks.loki:16
|
|
259
|
-
install gem_tasks.loki:24
|
|
260
|
-
release gem_tasks.loki:31
|
|
261
|
-
|
|
262
|
-
git.loki
|
|
263
|
-
push git.loki:6
|
|
264
|
-
pull git.loki:9
|
|
265
|
-
fetch git.loki:12
|
|
266
|
-
|
|
267
|
-
xyzzy.loki
|
|
268
|
-
xyzzy xyzzy.loki:6 OVERRIDDEN by .loki:15 — never callable
|
|
269
|
-
============================================================
|
|
270
|
-
1 problem(s), 1 warning(s).
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
The warning is a shadowed ancestor `.loki` marker one directory further up the tree — unrelated to the override, and something you may or may not see depending on what's above the repo on your own machine.
|
|
274
|
-
|
|
275
|
-
---
|
|
276
|
-
|
|
277
|
-
## `Asgard::Schedule`
|
|
278
|
-
|
|
279
|
-
Scheduled tasks. The user-facing guide is [Scheduled Tasks](schedule.md); this section covers the Ruby API.
|
|
280
|
-
|
|
281
|
-
| Method | Signature | Description |
|
|
282
|
-
|---|---|---|
|
|
283
|
-
| `declare` | `Asgard::Schedule.declare(task, **settings) → Hash` | What `Tasks.schedule` calls. Validates via `normalize` and records the entry. Redeclaring an identical entry is a no-op; a different entry under the same name raises `ArgumentError`. |
|
|
284
|
-
| `declarations` | `Asgard::Schedule.declarations → Hash` | Declared entries for this run, keyed by entry name. |
|
|
285
|
-
| `normalize` | `Asgard::Schedule.normalize(task, options: nil, at: nil, on: :daily, every: nil, env: {}, as: nil) → Hash` | Validates one declaration and returns `{ name:, task:, args:, at:, on:, every:, env: }`. Pure. |
|
|
286
|
-
| `backend_class` | `Asgard::Schedule.backend_class(platform = RUBY_PLATFORM)` | `Launchd` on darwin, `Systemd` on linux; raises `Asgard::Schedule::Error` elsewhere. |
|
|
287
|
-
| `runner` / `runner=` | `Asgard::Schedule.runner → #call` | The command runner new backends use: `argv` in, `[output, success?]` out. Defaults to `RUNNER` (`Open3.capture2e`); tests assign a recording runner, and `nil` restores the default. |
|
|
288
|
-
|
|
289
|
-
Both backends, `Asgard::Schedule::Launchd` and `Asgard::Schedule::Systemd`, implement the same instance API: `files`, `install`, `uninstall`, `start`, `stop`, `trigger`, `installed_names`, `status`, `log_path`, `notes`. Each class also exposes the pure helpers that build its job files (`Launchd.plist`, `Systemd.service_unit`, `Systemd.timer_unit`, ...), so they can be tested without a scheduler.
|
|
290
|
-
|
|
291
|
-
---
|
|
292
|
-
|
|
293
|
-
## `Asgard::Watch`
|
|
294
|
-
|
|
295
|
-
Watched files. The user-facing guide is [Watched Files](watch.md); this section covers the Ruby API.
|
|
296
|
-
|
|
297
|
-
| Method | Signature | Description |
|
|
298
|
-
|---|---|---|
|
|
299
|
-
| `declare` | `Asgard::Watch.declare(task, **settings) → Hash` | What `Tasks.watch` calls. Validates via `normalize` and records the entry. Redeclaring an identical entry is a no-op; a different entry under the same name raises `ArgumentError`. |
|
|
300
|
-
| `declarations` | `Asgard::Watch.declarations → Hash` | Declared entries for this run, keyed by entry name. |
|
|
301
|
-
| `normalize` | `Asgard::Watch.normalize(task, paths:, only: nil, ignore: nil, events: :all, debounce: 0.5, env: {}, as: nil) → Hash` | Validates one declaration and returns `{ name:, task:, paths:, dirs:, only:, ignore:, events:, debounce:, env: }`: the directories listen watches and one `only:` Regexp (or nil) built from the globs and the declared filter. Pure, as are its helpers `split_glob`, `glob_regexp`, `combine_only` and `normalize_events`. |
|
|
302
|
-
| `filter_events` | `Asgard::Watch.filter_events(batch, events) → Asgard::Changes, nil` | A listener batch narrowed to the entry's `events:`; `nil` when nothing is left. |
|
|
303
|
-
|
|
304
|
-
`Asgard::Changes` is a `Data` class with `modified`, `added` and `removed` path lists, `all`, `empty?`, `to_env` (the three trigger variables) and `Changes.from_env(env = ENV)`. `Changes.current` is the per-process memo behind the `changes` Kernel method; `Changes.forget` clears it.
|
|
305
|
-
|
|
306
|
-
The daemon is assembled from small classes, each testable alone: `Spawn` builds the frozen, Ractor-shareable description of one run (argv, whitelisted environment, root, log, changes); `Worker` runs one task's triggers serially in its own Ractor and reports every spawn and exit; `Dispatcher` routes listener batches to the worker queues; `Reporter` is the single writer of the log and the per-entry records and tracks in-flight pids; `State` owns the pidfile, log, recorded declarations and entry records under the XDG state directory; `Daemon` wires them to listen and handles signals and reloads.
|
|
307
|
-
|
|
308
|
-
---
|
|
309
|
-
|
|
310
|
-
## `Asgard::Base` Internal Class Methods
|
|
311
|
-
|
|
312
|
-
These are implementation details exposed for extensibility. Prefer the DSL methods above in normal use.
|
|
313
|
-
|
|
314
|
-
| Method | Description |
|
|
315
|
-
|---|---|
|
|
316
|
-
| `_deps` | Hash mapping task name symbols to their stage arrays. Set by `depends_on` + `method_added`. |
|
|
317
|
-
| `_done` | `Set` of task name symbols that have completed in the current invocation. |
|
|
318
|
-
| `_running` | `Set` of task name symbols currently executing (started but not yet finished). |
|
|
319
|
-
| `_cond` | Hash of `ConditionVariable` objects keyed by task name; threads wait here when a dep is in-flight. |
|
|
320
|
-
| `_ran_mutex` | `Mutex` protecting `_done`, `_running`, and `_cond` for thread-safe access. |
|
|
321
|
-
|
|
322
|
-
---
|
|
323
|
-
|
|
324
|
-
## `invoke_command` Hook
|
|
325
|
-
|
|
326
|
-
`Asgard::Base` overrides Thor's `invoke_command` to implement dependency resolution and deduplication:
|
|
327
|
-
|
|
328
|
-
1. Sets `$DEBUG` / `$VERBOSE` from `options` if the corresponding flags are present.
|
|
329
|
-
2. Tries to acquire a run token (`acquire_run_token`): if the task is already in `_done`, returns immediately (skip); if it is in `_running`, waits on the `_cond` ConditionVariable until it finishes, then returns (skip); otherwise adds the task to `_running` and continues.
|
|
330
|
-
3. Resolves dependency stages from `_deps` — already the parallel-group execution plan `depends_on` built — and executes each group in order (parallel groups in threads, sequential groups one at a time).
|
|
331
|
-
4. Calls `command.run(self, *args)` to execute the task itself.
|
|
332
|
-
5. In an `ensure` block, adds the task to `_done` and broadcasts on its `_cond` to wake any waiting threads.
|
|
333
|
-
|
|
334
|
-
---
|
|
335
|
-
|
|
336
|
-
## Error Classes
|
|
337
|
-
|
|
338
|
-
| Class | Superclass | Description |
|
|
339
|
-
|---|---|---|
|
|
340
|
-
| `Asgard::Error` | `StandardError` | Base error class for all Asgard errors. |
|
|
341
|
-
| `Asgard::Schedule::Error` | `Asgard::Error` | Raised when a `launchctl`/`systemctl` command fails, or on an unsupported platform. `run!` reports it as `asgard: <message>`. |
|
|
342
|
-
| `Asgard::Watch::Error` | `Asgard::Error` | Raised when the watcher cannot run: a `watch` names a task the project does not define. `run!` reports it as `asgard: <message>`. |
|
|
343
|
-
| `Asgard::CircularDependencyError` | `Asgard::Error` | Raised by `validate_deps!` when a cycle is detected in the dependency graph. `run!` catches this and calls `abort` with a clean message. |
|
|
344
|
-
|
|
345
|
-
```ruby
|
|
346
|
-
begin
|
|
347
|
-
Asgard.run!(ARGV)
|
|
348
|
-
rescue Asgard::CircularDependencyError => e
|
|
349
|
-
# This is already handled inside run! — you only need this
|
|
350
|
-
# if you call validate_deps! directly in your own tooling.
|
|
351
|
-
abort "circular dependency: #{e.message}"
|
|
352
|
-
end
|
|
353
|
-
```
|
|
354
|
-
|
|
355
|
-
---
|
|
356
|
-
|
|
357
|
-
## Dependencies
|
|
358
|
-
|
|
359
|
-
| Gem | Version | Purpose |
|
|
360
|
-
|---|---|---|
|
|
361
|
-
| [thor](https://github.com/rails/thor) | `~> 1.0` | CLI framework; provides the full task DSL |
|
|
362
|
-
| [dotenv](https://github.com/bkeepers/dotenv) | `~> 3.0` | `.env` file loading |
|
|
363
|
-
|
|
364
|
-
---
|
|
365
|
-
|
|
366
|
-
## Ruby Version Requirement
|
|
367
|
-
|
|
368
|
-
Asgard requires **Ruby >= 3.2.0**.
|
data/docs/assets/css/custom.css
DELETED
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
/* ==========================================================================
|
|
2
|
-
Asgard documentation — custom styles
|
|
3
|
-
Material theme handles the heavy lifting; this file adds targeted polish.
|
|
4
|
-
========================================================================== */
|
|
5
|
-
|
|
6
|
-
/* --------------------------------------------------------------------------
|
|
7
|
-
.loki filename display
|
|
8
|
-
Used whenever the filename ".loki" or "*.loki" appears inline or in code.
|
|
9
|
-
-------------------------------------------------------------------------- */
|
|
10
|
-
|
|
11
|
-
/* Give inline code that looks like a .loki filename a subtle Norse-gold tint */
|
|
12
|
-
code:is([class*="language-"]) .token.string:has-text(".loki"),
|
|
13
|
-
.md-typeset code.loki-file {
|
|
14
|
-
color: var(--md-accent-fg-color);
|
|
15
|
-
font-weight: 600;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/* Highlight .loki filenames in directory tree code blocks */
|
|
19
|
-
.md-typeset .highlight .filename {
|
|
20
|
-
background-color: color-mix(in srgb, var(--md-primary-fg-color) 12%, transparent);
|
|
21
|
-
border-bottom: 2px solid var(--md-accent-fg-color);
|
|
22
|
-
border-radius: 4px 4px 0 0;
|
|
23
|
-
color: var(--md-default-fg-color);
|
|
24
|
-
font-size: 0.75rem;
|
|
25
|
-
font-weight: 600;
|
|
26
|
-
letter-spacing: 0.04em;
|
|
27
|
-
padding: 0.3rem 0.8rem;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/* --------------------------------------------------------------------------
|
|
31
|
-
Admonition tweaks — slightly warmer warning border for the "argument"
|
|
32
|
-
class-level scope warning that appears on the tasks page.
|
|
33
|
-
-------------------------------------------------------------------------- */
|
|
34
|
-
|
|
35
|
-
.md-typeset .admonition.warning,
|
|
36
|
-
.md-typeset details.warning {
|
|
37
|
-
border-left-color: #e6a817;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
.md-typeset .admonition.warning > .admonition-title,
|
|
41
|
-
.md-typeset details.warning > summary {
|
|
42
|
-
background-color: rgba(230, 168, 23, 0.12);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/* --------------------------------------------------------------------------
|
|
46
|
-
Table of Contents — emphasise the current section a touch more
|
|
47
|
-
-------------------------------------------------------------------------- */
|
|
48
|
-
|
|
49
|
-
.md-nav__link--active {
|
|
50
|
-
font-weight: 600;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/* --------------------------------------------------------------------------
|
|
54
|
-
Home page feature table (HTML table in index.md)
|
|
55
|
-
-------------------------------------------------------------------------- */
|
|
56
|
-
|
|
57
|
-
.md-typeset table:not([class]) td {
|
|
58
|
-
vertical-align: top;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/* --------------------------------------------------------------------------
|
|
62
|
-
Execution diagram in dependencies.md — keep it tight and readable
|
|
63
|
-
-------------------------------------------------------------------------- */
|
|
64
|
-
|
|
65
|
-
.md-typeset pre code {
|
|
66
|
-
font-size: 0.85em;
|
|
67
|
-
line-height: 1.5;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/* --------------------------------------------------------------------------
|
|
71
|
-
Subtle Norse-shield watermark on the hero block (index page only).
|
|
72
|
-
Relies on the Material "primary: indigo" palette.
|
|
73
|
-
-------------------------------------------------------------------------- */
|
|
74
|
-
|
|
75
|
-
.md-header {
|
|
76
|
-
box-shadow: 0 2px 8px rgba(63, 81, 181, 0.25);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/* --------------------------------------------------------------------------
|
|
80
|
-
Code annotations — keep them readable across both light and dark palettes
|
|
81
|
-
-------------------------------------------------------------------------- */
|
|
82
|
-
|
|
83
|
-
.md-typeset .md-annotation__index > * {
|
|
84
|
-
font-size: 0.7rem;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/* --------------------------------------------------------------------------
|
|
88
|
-
Task-runner specific: distinguish shell prompt lines from output lines
|
|
89
|
-
in bash code blocks by dimming lines that don't start with '#' or 'asgard'
|
|
90
|
-
-------------------------------------------------------------------------- */
|
|
91
|
-
|
|
92
|
-
/* (Future: add targeted styles once MkDocs Material supports per-line
|
|
93
|
-
highlighting via config; for now this is intentionally minimal.) */
|
|
Binary file
|
data/docs/changelog.md
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
--8<-- "CHANGELOG.md"
|