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/examples/kitchen_sink.loki
DELETED
|
@@ -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
|
data/examples/subdir/.loki
DELETED
|
@@ -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,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 +0,0 @@
|
|
|
1
|
-
{ "records": ["one", "two"] }
|
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
|