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.
Files changed (102) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +179 -99
  3. data/README.md +141 -46
  4. data/bin/asgard +10 -0
  5. data/lib/asgard/base/dispatch.rb +257 -31
  6. data/lib/asgard/base/registry.rb +7 -3
  7. data/lib/asgard/base.rb +140 -9
  8. data/lib/asgard/declarations.rb +21 -0
  9. data/lib/asgard/doctor/task_sections.rb +11 -3
  10. data/lib/asgard/doctor.rb +12 -8
  11. data/lib/asgard/jobs/commands.rb +152 -0
  12. data/lib/asgard/jobs/declaration.rb +149 -0
  13. data/lib/asgard/jobs/launcher.rb +109 -0
  14. data/lib/asgard/jobs/state.rb +190 -0
  15. data/lib/asgard/jobs/stopper.rb +141 -0
  16. data/lib/asgard/jobs.rb +25 -0
  17. data/lib/asgard/kernel_methods.rb +19 -14
  18. data/lib/asgard/leading_flag.rb +10 -0
  19. data/lib/asgard/pid.rb +130 -0
  20. data/lib/asgard/schedule/commands.rb +39 -11
  21. data/lib/asgard/schedule/declaration.rb +139 -12
  22. data/lib/asgard/schedule/job_files.rb +24 -0
  23. data/lib/asgard/schedule/launchd.rb +55 -18
  24. data/lib/asgard/schedule/systemd.rb +69 -27
  25. data/lib/asgard/schedule/table.rb +34 -16
  26. data/lib/asgard/schedule.rb +49 -9
  27. data/lib/asgard/shell.rb +372 -44
  28. data/lib/asgard/state_dir.rb +84 -0
  29. data/lib/asgard/tasks.rb +8 -0
  30. data/lib/asgard/version.rb +1 -1
  31. data/lib/asgard/watch/changes.rb +26 -1
  32. data/lib/asgard/watch/commands.rb +52 -12
  33. data/lib/asgard/watch/daemon.rb +334 -43
  34. data/lib/asgard/watch/declaration.rb +135 -40
  35. data/lib/asgard/watch/dispatcher.rb +61 -10
  36. data/lib/asgard/watch/mailbox.rb +50 -0
  37. data/lib/asgard/watch/reporter.rb +50 -34
  38. data/lib/asgard/watch/spawn.rb +41 -16
  39. data/lib/asgard/watch/state.rb +93 -27
  40. data/lib/asgard/watch/worker.rb +44 -9
  41. data/lib/asgard/watch.rb +4 -11
  42. data/lib/asgard.rb +80 -14
  43. metadata +167 -186
  44. data/.envrc +0 -6
  45. data/.github/workflows/deploy-github-pages.yml +0 -52
  46. data/.loki +0 -39
  47. data/.reek.yml +0 -166
  48. data/.rubocop.yml +0 -163
  49. data/Archspec.rb +0 -24
  50. data/CLAUDE.md +0 -139
  51. data/COMMITS.md +0 -196
  52. data/bin/console +0 -11
  53. data/bin/setup +0 -8
  54. data/doc_tasks.loki +0 -21
  55. data/docs/api.md +0 -368
  56. data/docs/assets/css/custom.css +0 -93
  57. data/docs/assets/images/asgard.jpg +0 -0
  58. data/docs/changelog.md +0 -1
  59. data/docs/dependencies.md +0 -313
  60. data/docs/environment.md +0 -129
  61. data/docs/examples.md +0 -194
  62. data/docs/getting-started.md +0 -181
  63. data/docs/helpers.md +0 -262
  64. data/docs/index.md +0 -87
  65. data/docs/options.md +0 -224
  66. data/docs/schedule.md +0 -106
  67. data/docs/shell.md +0 -237
  68. data/docs/subcommands.md +0 -181
  69. data/docs/task-files.md +0 -407
  70. data/docs/tasks.md +0 -326
  71. data/docs/variables.md +0 -338
  72. data/docs/watch.md +0 -123
  73. data/examples/.env +0 -4
  74. data/examples/.envrc +0 -3
  75. data/examples/.loki +0 -24
  76. data/examples/bad.loki +0 -63
  77. data/examples/concurrent.loki +0 -58
  78. data/examples/confirm.loki +0 -48
  79. data/examples/db_subcommands.loki +0 -74
  80. data/examples/depends_on_block/bad/.loki +0 -51
  81. data/examples/depends_on_block/good/.loki +0 -83
  82. data/examples/do.loki +0 -45
  83. data/examples/env_usage.loki +0 -27
  84. data/examples/kitchen_sink.loki +0 -210
  85. data/examples/server_subcommands.loki +0 -65
  86. data/examples/subdir/.loki +0 -12
  87. data/examples/subdir/import_demo.loki +0 -14
  88. data/examples/subdir/import_up_demo.loki +0 -18
  89. data/examples/watcher_examples/.envrc +0 -3
  90. data/examples/watcher_examples/.loki +0 -54
  91. data/examples/watcher_examples/README.md +0 -112
  92. data/examples/watcher_examples/bin/hello +0 -3
  93. data/examples/watcher_examples/data/records.json +0 -1
  94. data/examples/watcher_examples/docs/drafts/ideas.md +0 -5
  95. data/examples/watcher_examples/docs/index.md +0 -4
  96. data/examples/watcher_examples/lib/greeter.rb +0 -7
  97. data/gem_tasks.loki +0 -51
  98. data/git.loki +0 -13
  99. data/mkdocs.yml +0 -166
  100. data/quality.loki +0 -266
  101. data/quality_rails.loki +0 -47
  102. 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**.
@@ -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"