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
@@ -1,210 +0,0 @@
1
- # frozen_string_literal: true
2
- # kitchen_sink.loki — demonstrates every Thor DSL feature available in Asgard.
3
- #
4
- # Task names deliberately avoid the gem's own .loki tasks (test, build, quality,
5
- # install, release) so this file can be loaded alongside them without conflict.
6
-
7
- class Tasks
8
- # ── Ruby class variables — shared across all tasks and subcommands ──────────
9
- @@app_name ||= "my_app".freeze
10
- @@build_dir ||= "builds/#{@@app_name}".freeze
11
-
12
- # ── Computed values — plain private methods replace the removed var DSL ──────
13
- # Declare as private so Thor does not try to register them as commands.
14
- # Use @ivar ||= inside the method body to memoize expensive calls.
15
- private
16
-
17
- def version = `git describe --tags --always`.strip
18
- def sha = `git rev-parse --short HEAD`.strip
19
- def branch = @branch ||= `git rev-parse --abbrev-ref HEAD`.strip
20
-
21
- public
22
-
23
- # ── Asgard: dotenv — load environment variables ────────────────────────────
24
- # Uncomment to activate:
25
- # dotenv # loads .env
26
- # dotenv ".env.local" # or a specific file
27
-
28
- # ── Thor: class_option — option present on every task ──────────────────────
29
- class_option :dry_run,
30
- aliases: "-n",
31
- type: :boolean,
32
- default: false,
33
- desc: "Print commands without executing them"
34
-
35
- class_option :env,
36
- type: :string,
37
- default: "development",
38
- enum: %w[development staging production],
39
- desc: "Target environment"
40
-
41
- # ── Thor: default_task — runs when no command is given ─────────────────────
42
- default_task :greet
43
-
44
- # ── Thor: map — short aliases for tasks ────────────────────────────────────
45
- map "g" => :greet
46
- map "ck" => :check
47
- map "rp" => :report
48
- map "pl" => :pipeline
49
-
50
- # ── Basic task — no parameters ─────────────────────────────────────────────
51
- desc "Say hello (default task when no command is given)"
52
- def greet
53
- puts "Hello from #{@@app_name} #{version} (#{branch}) in #{options[:env]} mode!"
54
- end
55
-
56
- # ── Positional parameter with default ──────────────────────────────────────
57
- desc "hello NAME", "Greet NAME; omit NAME to greet the world"
58
- def hello(name = "World")
59
- puts "Hello, #{name}!"
60
- end
61
-
62
- # ── Positional parameter with method_option ────────────────────────────────
63
- desc "farewell NAME", "Say goodbye to NAME"
64
- option :formal, aliases: "-f", type: :boolean, default: false, desc: "Use formal language"
65
- def farewell(name = "friend")
66
- msg = options[:formal] ? "Goodbye, #{name}." : "See ya, #{name}!"
67
- puts msg
68
- end
69
-
70
- # ── argument — formal positional with type metadata ────────────────────────
71
- # WARNING: argument is a CLASS-LEVEL declaration that pollutes every task's
72
- # usage line. Only use it when every task in the class shares the same input,
73
- # or in a single-command CLI. Shown here as a commented-out reference only.
74
- #
75
- # argument :target,
76
- # type: :string,
77
- # default: "localhost",
78
- # desc: "Deployment target host"
79
-
80
- desc "notify RECIPIENT", "Send a notification to RECIPIENT"
81
- def notify(recipient = "team")
82
- puts "Notifying #{recipient}..."
83
- end
84
-
85
- # ── method_option — all five option types ──────────────────────────────────
86
- desc "Compile the project"
87
- option :output, aliases: "-o", type: :string, default: "dist/", desc: "Output directory"
88
- option :verbose, aliases: "-v", type: :boolean, default: false, desc: "Enable verbose output"
89
- option :jobs, aliases: "-j", type: :numeric, default: 1, desc: "Number of parallel jobs"
90
- option :tags, type: :array, desc: "Build tags to apply"
91
- option :defines, type: :hash, desc: "Preprocessor defines (KEY:VALUE)"
92
- def compile
93
- puts "Compiling #{@@app_name} → #{options[:output]}"
94
- end
95
-
96
- # ── required option + enum + banner ────────────────────────────────────────
97
- desc "deploy ENV", "Deploy to ENV (default: staging)"
98
- option :strategy,
99
- type: :string,
100
- required: true,
101
- enum: %w[blue-green rolling canary],
102
- desc: "Deployment strategy"
103
- option :timeout,
104
- type: :numeric,
105
- default: 300,
106
- banner: "SECONDS",
107
- desc: "Abort deployment after SECONDS"
108
- option :branch,
109
- type: :string,
110
- default: "main",
111
- desc: "Git branch to deploy"
112
- def deploy(env = "staging")
113
- puts "Deploying #{@@app_name}@#{options[:branch]} to #{env}..."
114
- end
115
-
116
- # ── long_desc — extended help shown by `asgard help report` ────────────────
117
- long_desc <<~LONGDESC
118
- Generates a project report covering test coverage, lint results,
119
- and a dependency audit.
120
-
121
- Pass --format to control output style. Use --since to scope the
122
- report to changes after a given date.
123
-
124
- Examples:\x5
125
- asgard report --format html --since 2024-01-01\x5
126
- asgard report --format json --output report.json\x5
127
- asgard rp --format text
128
- LONGDESC
129
- desc "Generate a project report"
130
- option :format, type: :string, default: "text", enum: %w[text html json], desc: "Output format"
131
- option :since, type: :string, banner: "DATE", desc: "Limit to changes after DATE"
132
- option :output, type: :string, banner: "FILE", desc: "Write output to FILE"
133
- def report
134
- puts "Generating #{options[:format]} report..."
135
- end
136
-
137
- # ── Asgard depends_on: sequential — analyze runs before spec ───────────────
138
- desc "Check code style and complexity"
139
- def analyze = puts "Analyzing..."
140
-
141
- depends_on :analyze
142
- desc "Run the test suite (depends on: analyze)"
143
- def spec = puts "Running specs..."
144
-
145
- # ── Asgard depends_on: parallel — analyze and typecheck run concurrently ───
146
- desc "Run the type checker"
147
- def typecheck = puts "Type checking..."
148
-
149
- depends_on [:analyze, :typecheck]
150
- desc "Run analyze and typecheck in parallel"
151
- def check = puts "All checks passed."
152
-
153
- # ── Asgard depends_on: mixed sequential + parallel ─────────────────────────
154
- desc "Create distribution archive"
155
- def pack = puts "Packing..."
156
-
157
- # check → compile+spec (parallel) → pack → pipeline
158
- depends_on :check, [:compile, :spec], :pack
159
- desc "Full pipeline: check → compile+spec → pack"
160
- def pipeline = puts "Pipeline complete."
161
-
162
- # ── Asgard: debug? / verbose? — Kernel predicates for conditional output ──────
163
- # debug? returns true when --debug is passed (sets $DEBUG)
164
- # verbose? returns true when --verbose is passed (sets $VERBOSE)
165
- # Both are set by Asgard before invoke_command runs, so they are safe
166
- # to read inside any task body.
167
- #
168
- # Run with:
169
- # asgard status --verbose
170
- # asgard status --debug
171
- # asgard status --verbose --debug
172
- desc "Show application status"
173
- def status
174
- puts "#{@@app_name} is running in #{options[:env]} mode."
175
- puts " build dir : #{@@build_dir}" if verbose?
176
- puts " sha : #{current_sha}" if verbose?
177
- if debug?
178
- puts " $DEBUG : #{$DEBUG.inspect}"
179
- puts " $VERBOSE : #{$VERBOSE.inspect}"
180
- puts " options : #{options.inspect}"
181
- end
182
- end
183
-
184
- # ── Asgard: sh — run shell commands, single-line and multi-line heredoc ────
185
- desc "Show environment info (single-line sh)"
186
- def env_info = sh "echo Running #{@@app_name} on $(uname -s)"
187
-
188
- desc "Bootstrap the local environment (multi-line sh / heredoc)"
189
- def bootstrap
190
- sh <<~SHELL
191
- echo "App: #{@@app_name}"
192
- echo "Ruby: $(ruby -v)"
193
- echo "Date: $(date)"
194
- SHELL
195
- end
196
-
197
- # ── Thor: no_commands — public helper excluded from CLI and --help ──────────
198
- no_commands do
199
- def current_sha
200
- `git rev-parse --short HEAD`.strip
201
- end
202
- end
203
-
204
- # ── private — also excluded from CLI and --help ────────────────────────────
205
- private
206
-
207
- def banner(msg)
208
- puts "=== #{msg} ==="
209
- end
210
- end
@@ -1,65 +0,0 @@
1
- # frozen_string_literal: true
2
- # Demonstrates Thor subcommands registered on the top-level Tasks class.
3
- #
4
- # Port state is persisted to .server.port so stop and restart always
5
- # operate on the same port the server was started on.
6
- #
7
- # Usage:
8
- # asgard server # shows subcommand help
9
- # asgard server start
10
- # asgard server start -p 8000
11
- # asgard server start -p 4000 --workers 4 --daemon
12
- # asgard server stop
13
- # asgard server stop --force
14
- # asgard server status
15
- # asgard server restart # stops then starts on the persisted port
16
-
17
- SERVER_PORT_FILE = "tmp/.server.port".freeze
18
-
19
- class ServerCommands < Tasks
20
- default_task :help # This is the default value for the default_task
21
-
22
- helper(:server_port) {
23
- File.exist?(SERVER_PORT_FILE) ? File.read(SERVER_PORT_FILE).strip.to_i : 3000
24
- }
25
-
26
- desc "Start the server"
27
- option :port, aliases: "-p", type: :numeric, default: 3000, desc: "Port to listen on"
28
- option :daemon, aliases: "-d", type: :boolean, default: false, desc: "Run as a background daemon"
29
- option :workers, aliases: "-w", type: :numeric, default: 2, desc: "Number of worker processes"
30
- option :log, type: :string, default: "log/server.log",
31
- banner: "FILE", desc: "Write logs to FILE"
32
- def start
33
- FileUtils.mkdir_p("tmp")
34
- File.write(SERVER_PORT_FILE, options[:port].to_s)
35
- puts "Starting server on :#{options[:port]} with #{options[:workers]} workers#{options[:daemon] ? " (daemon)" : ""}..."
36
- end
37
-
38
- desc "Stop the running server"
39
- option :force, aliases: "-f", type: :boolean, default: false, desc: "Force-kill without draining"
40
- option :wait, type: :numeric, default: 30, desc: "Seconds to wait for shutdown"
41
- def stop
42
- if options[:force]
43
- puts "Force-stopping server on :#{server_port}..."
44
- else
45
- puts "Gracefully stopping server on :#{server_port} (timeout: #{options[:wait] || 30}s)..."
46
- end
47
- end
48
-
49
- desc "Show server status and process info"
50
- def status
51
- puts "Server is listening on :#{server_port}"
52
- end
53
-
54
- depends_on :stop
55
- desc "Restart the server on the same port it was started on"
56
- def restart
57
- puts "Starting server on :#{server_port}..."
58
- puts "Server restarted."
59
- end
60
- end
61
-
62
- class Tasks
63
- desc "server SUBCOMMAND", "Manage the application server"
64
- subcommand "server", ServerCommands
65
- end
@@ -1,12 +0,0 @@
1
- # examples/subdir/.loki — root task file for this subdirectory.
2
- #
3
- # When asgard is run from examples/subdir/, it loads this file.
4
- # From here, import pulls in sibling task files explicitly.
5
- #
6
- # import accepts a direct path or a glob:
7
- # import "import_up_demo.loki" # a single file
8
- # import "*.loki" # all loki files in this directory
9
- # import "tasks/**/*.loki" # all loki files under a tasks/ tree
10
- # import "../shared/*.loki" # files in a sibling directory
11
-
12
- import "import_up_demo.loki"
@@ -1,14 +0,0 @@
1
- # frozen_string_literal: true
2
- # This file lives in a subdirectory and is explicitly imported by the
3
- # parent examples/.loki via import "subdir/import_demo.loki".
4
- #
5
- # In a real project a subdirectory might hold task files scoped to a
6
- # specific concern (deploy, database, CI) while the root .loki wires
7
- # them all together with import.
8
-
9
- class Tasks
10
- desc "Confirm this task was loaded from a subdirectory via import"
11
- def subdir_task
12
- puts "Loaded from examples/subdir/import_demo.loki"
13
- end
14
- end
@@ -1,18 +0,0 @@
1
- # frozen_string_literal: true
2
- # Demonstrates import_up — find a file by walking up the directory tree
3
- # and load it, without needing to know how deep the current file is.
4
- #
5
- # import_up(name) combines loki_up(name) + import(path) in one call.
6
- # It searches CWD, then each ancestor in turn, and loads the first match.
7
- #
8
- # This is useful when a nested task file needs to pull in something from
9
- # the project root — a shared config, a common helpers file — without
10
- # hardcoding a relative path that would break if the file moves.
11
- #
12
- # import_up also accepts a glob:
13
- # import_up "*.loki" # first directory (walking up) that contains any .loki file
14
- # import_up "config/settings.loki" # first ancestor with config/settings.loki
15
- # import_up ".env" # locate a .env file anywhere up the tree
16
-
17
- # Load env_usage.loki from the nearest ancestor directory that contains it.
18
- import_up "env_usage.loki"
@@ -1,3 +0,0 @@
1
- # asgard/examples/watcher_examples/.envrc
2
-
3
- source_up
@@ -1,54 +0,0 @@
1
- # examples/watcher_examples/.loki
2
- # A small project whose tasks run when its files change.
3
- #
4
- # `watch :task, paths: ...` at class level says which files a task cares
5
- # about. `asgard watcher start` runs one background process for this
6
- # project that listens for changes and runs the matching tasks, each as a
7
- # separate `asgard <task>` process started from this directory. The task
8
- # reads what changed through `changes`.
9
- #
10
- # Run with (from this directory):
11
- # asgard watcher start # background; log and state under ~/.local/state/asgard/watch/
12
- # asgard watcher list # the four entries, daemon state, each entry's last run
13
- # # ... edit lib/greeter.rb, docs/index.md, add or remove a data/*.json ...
14
- # asgard watcher log # one line per trigger, spawn and exit, plus each task's output
15
- # asgard watcher stop # a task still running finishes first
16
- #
17
- # Each task also runs by hand (`asgard lint`), when `changes` is empty.
18
- # See README.md here for a walkthrough with expected output.
19
-
20
- class Tasks
21
- # A glob: watches lib/ and keeps the paths that match, like a shell would.
22
- watch :lint, paths: "lib/**/*.rb"
23
-
24
- # A directory with filters. only: narrows to Markdown; ignore: silences
25
- # the drafts folder; env: adds a variable the task can read with env().
26
- watch :docs, paths: "docs", only: /\.md\z/, ignore: %r{\Adrafts/}, env: { BUILD_ENV: "demo" }
27
-
28
- # events: picks which kinds of change count. Editing a data file does
29
- # nothing; creating or deleting one triggers a reindex. debounce: waits
30
- # a full second after the last event, so a batch of files lands at once.
31
- watch :reindex, paths: "data/**/*.json", events: %i[added removed], debounce: 1
32
-
33
- # The same task watched from a second declaration needs its own entry
34
- # name. Both entries share one worker, so lint never overlaps itself.
35
- watch :lint, paths: "bin/**/*", as: "lint-bin"
36
-
37
- desc "lint", "Report the Ruby files that changed"
38
- def lint
39
- files = changes.modified + changes.added
40
- return puts "lint: nothing changed (run by hand), so linting everything under lib/" if files.empty?
41
-
42
- files.each { |file| puts "lint: #{file} (#{File.foreach(file).count} lines) triggered by #{env(:asgard_watch, 'nothing')}" }
43
- end
44
-
45
- desc "docs", "Rebuild the documentation"
46
- def docs
47
- puts "docs: building for #{env(:build_env, 'development')}; changed: #{changes.all.join(', ')}"
48
- end
49
-
50
- desc "reindex", "Rebuild the data index"
51
- def reindex
52
- puts "reindex: #{changes.added.size} added, #{changes.removed.size} removed; #{Dir['data/**/*.json'].size} data file(s) now"
53
- end
54
- end
@@ -1,112 +0,0 @@
1
- # Watcher example
2
-
3
- A small project whose tasks run when its files change. The `.loki` here
4
- declares four watches; `lib/`, `bin/`, `docs/` and `data/` hold one
5
- placeholder file each so the watcher has something to watch right away.
6
-
7
- ## Before you start
8
-
9
- The watcher runs its daemon and every triggered task as separate `asgard`
10
- processes. If you are running asgard from a git checkout of this
11
- repository rather than an installed gem, install the checkout as the gem
12
- first, from the repository root:
13
-
14
- ```bash
15
- asgard install # builds pkg/asgard-<version>.gem and gem-installs it
16
- gem list asgard # the checkout's version should be listed
17
- ```
18
-
19
- Otherwise an older asgard gem already on the machine can be the one the
20
- spawned processes run, and it will not understand `watch`.
21
-
22
- ## Walkthrough
23
-
24
- From this directory:
25
-
26
- ```bash
27
- asgard watcher start
28
- ```
29
-
30
- ```
31
- started watcher for watcher_examples (pid 12345); log: ~/.local/state/asgard/watch/watcher-examples/watcher.log
32
- ```
33
-
34
- Now make some changes and watch the log fill up:
35
-
36
- ```bash
37
- echo '# a change' >> lib/greeter.rb # lint runs, with the path in changes.modified
38
- echo 'more' >> docs/index.md # docs runs; BUILD_ENV=demo comes from env:
39
- echo 'more' >> docs/drafts/ideas.md # nothing: drafts/ is ignored
40
- echo '{}' > data/new.json # reindex runs: a file was added
41
- echo '{"edited": true}' > data/new.json # nothing: events: is added and removed only
42
- rm data/new.json # reindex runs again: a file was removed
43
- asgard watcher log
44
- ```
45
-
46
- ```
47
- 2026-10-18 23:40:01 trigger lint task=lint lib/greeter.rb
48
- 2026-10-18 23:40:01 spawn lint task=lint pid=12351 lib/greeter.rb
49
- lint: lib/greeter.rb (7 lines) triggered by lint
50
- 2026-10-18 23:40:02 exit lint task=lint pid=12351 status=0 seconds=0.41
51
- 2026-10-18 23:40:05 trigger docs task=docs docs/index.md
52
- 2026-10-18 23:40:05 spawn docs task=docs pid=12360 docs/index.md
53
- docs: building for demo; changed: docs/index.md
54
- 2026-10-18 23:40:06 exit docs task=docs pid=12360 status=0 seconds=0.40
55
- 2026-10-18 23:40:09 trigger reindex task=reindex data/new.json
56
- 2026-10-18 23:40:09 spawn reindex task=reindex pid=12371 data/new.json
57
- reindex: 1 added, 0 removed; 2 data file(s) now
58
- 2026-10-18 23:40:10 exit reindex task=reindex pid=12371 status=0 seconds=0.40
59
- ...
60
- ```
61
-
62
- Each trigger line names the entry and the changed paths. Each spawn and
63
- exit line carries the task's pid, so the lines of overlapping tasks can be
64
- told apart; a task's own output sits between its spawn and its exit. Save
65
- `lib/greeter.rb` twice in quick succession and the second trigger says
66
- `(queued behind pid N)`: the same task never overlaps itself, while
67
- different tasks run at the same time.
68
-
69
- ```bash
70
- asgard watcher list
71
- ```
72
-
73
- ```
74
- ┌──────────┬────────────────┬──────────────────────────────────────┬──────────┬────────────────────────────────┐
75
- │ NAME │ TASK │ WATCHES │ DAEMON │ LAST RUN │
76
- ├──────────┼────────────────┼──────────────────────────────────────┼──────────┼────────────────────────────────┤
77
- │ lint │ asgard lint │ lib/**/*.rb │ running │ 0 (0.4s, 2026-10-18 23:40:02) │
78
- │ docs │ asgard docs │ docs │ running │ 0 (0.4s, 2026-10-18 23:40:06) │
79
- │ reindex │ asgard reindex │ data/**/*.json [added, removed] │ running │ 0 (0.4s, 2026-10-18 23:40:14) │
80
- │ lint-bin │ asgard lint │ bin/**/* │ running │ never │
81
- └──────────┴────────────────┴──────────────────────────────────────┴──────────┴────────────────────────────────┘
82
- ```
83
-
84
- ```bash
85
- asgard watcher stop
86
- ```
87
-
88
- A task that is still running finishes first; `stop` names it while it
89
- waits.
90
-
91
- ## Running the tasks by hand
92
-
93
- Every task works without the watcher too. `changes` is then empty, which
94
- `lint` turns into "lint everything":
95
-
96
- ```bash
97
- asgard lint
98
- asgard docs
99
- asgard reindex
100
- ```
101
-
102
- ## What the watcher restricts
103
-
104
- A triggered task does not inherit the shell the watcher was started from.
105
- It sees `PATH`, `HOME`, `USER`, `LANG` and a few other basics, the entry's
106
- `env:`, a project `.envrc` through `direnv`, and the trigger variables
107
- `ASGARD_WATCH`, `ASGARD_WATCH_ROOT`, `ASGARD_MODIFIED`, `ASGARD_ADDED` and
108
- `ASGARD_REMOVED`. `asgard watcher preview` prints the exact command and
109
- environment each entry would run.
110
-
111
- Edit `.loki` while the watcher runs and it restarts itself with the new
112
- declarations.
@@ -1,3 +0,0 @@
1
- #!/bin/sh
2
- # Edit this file while the watcher runs: the lint-bin entry picks it up.
3
- echo "hello from bin/"
@@ -1 +0,0 @@
1
- { "records": ["one", "two"] }
@@ -1,5 +0,0 @@
1
- # Drafts are ignored
2
-
3
- Editing this file never triggers the docs entry: its declaration has
4
- `ignore: %r{\Adrafts/}`.
5
- more
@@ -1,4 +0,0 @@
1
- # Watcher example
2
-
3
- Edit this file while the watcher runs: the docs entry picks it up.
4
- more
@@ -1,7 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Edit this file while the watcher runs: the lint entry picks it up.
4
- module Greeter
5
- def self.greet(name) = "Hello, #{name}!"
6
- end
7
- # a change
data/gem_tasks.loki DELETED
@@ -1,51 +0,0 @@
1
- # frozen_string_literal: true
2
- # Gem lifecycle tasks — imported by .loki
3
-
4
- class Tasks
5
- desc "Open IRB console with the gem loaded"
6
- def console
7
- if File.exist?("bin/console")
8
- sh "bin/console"
9
- else
10
- sh "bundle exec irb -Ilib -r #{@@project}"
11
- end
12
- end
13
-
14
- desc "Build the gem package"
15
- depends_on :quality
16
- def build
17
- sh "mkdir -p pkg"
18
- sh "gem build asgard.gemspec"
19
- sh "mv asgard-#{project_version}.gem pkg/"
20
- end
21
-
22
- desc "Build and install gem locally"
23
- depends_on :build
24
- def install
25
- sh "gem install pkg/asgard-#{project_version}.gem"
26
- end
27
-
28
- desc "release", "Release gem to RubyGems (runs quality gate first)"
29
- option :yes, aliases: "-y", type: :boolean, default: false, desc: "Skip confirmation prompt"
30
- depends_on :quality
31
- def release
32
- tag = "v#{project_version}"
33
- gem_file = "pkg/asgard-#{project_version}.gem"
34
-
35
- unless options[:yes]
36
- print "Release #{@@project} v#{project_version} to RubyGems? [y/N] "
37
- $stdout.flush
38
- return puts "Aborted." unless $stdin.gets.strip.downcase == "y"
39
- end
40
-
41
- abort "Working directory is not clean — commit or stash changes first." unless `git status --porcelain`.strip.empty?
42
- abort "Tag #{tag} already exists." unless `git tag -l #{tag}`.strip.empty?
43
-
44
- sh "mkdir -p pkg"
45
- sh "gem build asgard.gemspec"
46
- sh "mv asgard-#{project_version}.gem pkg/"
47
- sh "git tag #{tag}"
48
- sh "git push origin #{tag}"
49
- sh "gem push #{gem_file}"
50
- end
51
- end
data/git.loki DELETED
@@ -1,13 +0,0 @@
1
- # frozen_string_literal: true
2
- # Per-repo git tasks — imported by .loki
3
-
4
- class Tasks
5
- desc "Push the current branch to its remote"
6
- def push = sh "git push"
7
-
8
- desc "Pull (fast-forward only) from the remote"
9
- def pull = sh "git pull --ff-only"
10
-
11
- desc "Fetch from the remote"
12
- def fetch = sh "git fetch"
13
- end