asgard 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/deploy-github-pages.yml +52 -0
- data/CHANGELOG.md +56 -3
- data/CLAUDE.md +117 -0
- data/README.md +152 -10
- data/docs/api.md +131 -0
- data/docs/assets/css/custom.css +93 -0
- data/docs/assets/images/asgard.jpg +0 -0
- data/docs/changelog.md +100 -0
- data/docs/dependencies.md +221 -0
- data/docs/environment.md +113 -0
- data/docs/examples.md +140 -0
- data/docs/getting-started.md +180 -0
- data/docs/helpers.md +154 -0
- data/docs/index.md +85 -0
- data/docs/options.md +180 -0
- data/docs/shell.md +208 -0
- data/docs/subcommands.md +181 -0
- data/docs/task-files.md +254 -0
- data/docs/tasks.md +284 -0
- data/docs/variables.md +122 -0
- data/examples/.loki +2 -0
- data/examples/concurrent.loki +58 -0
- data/examples/db_subcommands.loki +74 -0
- data/examples/kitchen_sink.loki +164 -0
- data/examples/server_subcommands.loki +56 -0
- data/lib/asgard/base.rb +95 -30
- data/lib/asgard/shell.rb +3 -1
- data/lib/asgard/tasks.rb +29 -1
- data/lib/asgard/version.rb +1 -1
- data/lib/asgard.rb +11 -6
- data/mkdocs.yml +164 -0
- metadata +29 -5
|
@@ -0,0 +1,164 @@
|
|
|
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
|
+
# ── Asgard: var — static value and lazy lambda ─────────────────────────────
|
|
9
|
+
var :app_name, "my_app"
|
|
10
|
+
var :build_dir, -> { "builds/#{app_name}" }
|
|
11
|
+
|
|
12
|
+
# ── Asgard: dotenv — load environment variables ────────────────────────────
|
|
13
|
+
# Uncomment to activate:
|
|
14
|
+
# dotenv # loads .env
|
|
15
|
+
# dotenv ".env.local" # or a specific file
|
|
16
|
+
|
|
17
|
+
# ── Thor: class_option — option present on every task ──────────────────────
|
|
18
|
+
class_option :dry_run,
|
|
19
|
+
aliases: "-n",
|
|
20
|
+
type: :boolean,
|
|
21
|
+
default: false,
|
|
22
|
+
desc: "Print commands without executing them"
|
|
23
|
+
|
|
24
|
+
class_option :env,
|
|
25
|
+
type: :string,
|
|
26
|
+
default: "development",
|
|
27
|
+
enum: %w[development staging production],
|
|
28
|
+
desc: "Target environment"
|
|
29
|
+
|
|
30
|
+
# ── Thor: default_task — runs when no command is given ─────────────────────
|
|
31
|
+
default_task :greet
|
|
32
|
+
|
|
33
|
+
# ── Thor: map — short aliases for tasks ────────────────────────────────────
|
|
34
|
+
map "g" => :greet
|
|
35
|
+
map "ck" => :check
|
|
36
|
+
map "rp" => :report
|
|
37
|
+
map "pl" => :pipeline
|
|
38
|
+
|
|
39
|
+
# ── Basic task — no parameters ─────────────────────────────────────────────
|
|
40
|
+
desc "greet", "Say hello (default task when no command is given)"
|
|
41
|
+
def greet
|
|
42
|
+
puts "Hello from #{app_name} (#{options[:env]})!"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# ── Positional parameter with default ──────────────────────────────────────
|
|
46
|
+
desc "hello NAME", "Greet NAME; omit NAME to greet the world"
|
|
47
|
+
def hello(name = "World")
|
|
48
|
+
puts "Hello, #{name}!"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# ── Positional parameter with method_option ────────────────────────────────
|
|
52
|
+
desc "farewell NAME", "Say goodbye to NAME"
|
|
53
|
+
option :formal, aliases: "-f", type: :boolean, default: false, desc: "Use formal language"
|
|
54
|
+
def farewell(name = "friend")
|
|
55
|
+
msg = options[:formal] ? "Goodbye, #{name}." : "See ya, #{name}!"
|
|
56
|
+
puts msg
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# ── argument — formal positional with type metadata ────────────────────────
|
|
60
|
+
# WARNING: argument is a CLASS-LEVEL declaration that pollutes every task's
|
|
61
|
+
# usage line. Only use it when every task in the class shares the same input,
|
|
62
|
+
# or in a single-command CLI. Shown here as a commented-out reference only.
|
|
63
|
+
#
|
|
64
|
+
# argument :target,
|
|
65
|
+
# type: :string,
|
|
66
|
+
# default: "localhost",
|
|
67
|
+
# desc: "Deployment target host"
|
|
68
|
+
|
|
69
|
+
desc "notify RECIPIENT", "Send a notification to RECIPIENT"
|
|
70
|
+
def notify(recipient = "team")
|
|
71
|
+
puts "Notifying #{recipient}..."
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# ── method_option — all five option types ──────────────────────────────────
|
|
75
|
+
desc "compile", "Compile the project"
|
|
76
|
+
option :output, aliases: "-o", type: :string, default: "dist/", desc: "Output directory"
|
|
77
|
+
option :verbose, aliases: "-v", type: :boolean, default: false, desc: "Enable verbose output"
|
|
78
|
+
option :jobs, aliases: "-j", type: :numeric, default: 1, desc: "Number of parallel jobs"
|
|
79
|
+
option :tags, type: :array, desc: "Build tags to apply"
|
|
80
|
+
option :defines, type: :hash, desc: "Preprocessor defines (KEY:VALUE)"
|
|
81
|
+
def compile
|
|
82
|
+
puts "Compiling #{app_name} → #{options[:output]}"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# ── required option + enum + banner ────────────────────────────────────────
|
|
86
|
+
desc "deploy ENV", "Deploy to ENV (default: staging)"
|
|
87
|
+
option :strategy,
|
|
88
|
+
type: :string,
|
|
89
|
+
required: true,
|
|
90
|
+
enum: %w[blue-green rolling canary],
|
|
91
|
+
desc: "Deployment strategy"
|
|
92
|
+
option :timeout,
|
|
93
|
+
type: :numeric,
|
|
94
|
+
default: 300,
|
|
95
|
+
banner: "SECONDS",
|
|
96
|
+
desc: "Abort deployment after SECONDS"
|
|
97
|
+
option :branch,
|
|
98
|
+
type: :string,
|
|
99
|
+
default: "main",
|
|
100
|
+
desc: "Git branch to deploy"
|
|
101
|
+
def deploy(env = "staging")
|
|
102
|
+
puts "Deploying #{app_name}@#{options[:branch]} to #{env}..."
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# ── long_desc — extended help shown by `asgard help report` ────────────────
|
|
106
|
+
long_desc <<~LONGDESC
|
|
107
|
+
Generates a project report covering test coverage, lint results,
|
|
108
|
+
and a dependency audit.
|
|
109
|
+
|
|
110
|
+
Pass --format to control output style. Use --since to scope the
|
|
111
|
+
report to changes after a given date.
|
|
112
|
+
|
|
113
|
+
Examples:\x5
|
|
114
|
+
asgard report --format html --since 2024-01-01\x5
|
|
115
|
+
asgard report --format json --output report.json\x5
|
|
116
|
+
asgard rp --format text
|
|
117
|
+
LONGDESC
|
|
118
|
+
desc "report", "Generate a project report"
|
|
119
|
+
option :format, type: :string, default: "text", enum: %w[text html json], desc: "Output format"
|
|
120
|
+
option :since, type: :string, banner: "DATE", desc: "Limit to changes after DATE"
|
|
121
|
+
option :output, type: :string, banner: "FILE", desc: "Write output to FILE"
|
|
122
|
+
def report
|
|
123
|
+
puts "Generating #{options[:format]} report..."
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# ── Asgard depends_on: sequential — analyze runs before spec ───────────────
|
|
127
|
+
desc "analyze", "Check code style and complexity"
|
|
128
|
+
def analyze = puts "Analyzing..."
|
|
129
|
+
|
|
130
|
+
depends_on :analyze
|
|
131
|
+
desc "spec", "Run the test suite (depends on: analyze)"
|
|
132
|
+
def spec = puts "Running specs..."
|
|
133
|
+
|
|
134
|
+
# ── Asgard depends_on: parallel — analyze and typecheck run concurrently ───
|
|
135
|
+
desc "typecheck", "Run the type checker"
|
|
136
|
+
def typecheck = puts "Type checking..."
|
|
137
|
+
|
|
138
|
+
depends_on [:analyze, :typecheck]
|
|
139
|
+
desc "check", "Run analyze and typecheck in parallel"
|
|
140
|
+
def check = puts "All checks passed."
|
|
141
|
+
|
|
142
|
+
# ── Asgard depends_on: mixed sequential + parallel ─────────────────────────
|
|
143
|
+
desc "pack", "Create distribution archive"
|
|
144
|
+
def pack = puts "Packing..."
|
|
145
|
+
|
|
146
|
+
# check → compile+spec (parallel) → pack → pipeline
|
|
147
|
+
depends_on :check, [:compile, :spec], :pack
|
|
148
|
+
desc "pipeline", "Full pipeline: check → compile+spec → pack"
|
|
149
|
+
def pipeline = puts "Pipeline complete."
|
|
150
|
+
|
|
151
|
+
# ── Thor: no_commands — public helper excluded from CLI and --help ──────────
|
|
152
|
+
no_commands do
|
|
153
|
+
def current_sha
|
|
154
|
+
`git rev-parse --short HEAD`.strip
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# ── private — also excluded from CLI and --help ────────────────────────────
|
|
159
|
+
private
|
|
160
|
+
|
|
161
|
+
def banner(msg)
|
|
162
|
+
puts "=== #{msg} ==="
|
|
163
|
+
end
|
|
164
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# Demonstrates Thor subcommands registered on the top-level Tasks class.
|
|
3
|
+
#
|
|
4
|
+
# The subcommand class inherits from Tasks so it has access to sh, shebang,
|
|
5
|
+
# var, depends_on, and the built-in --debug/--verbose class options.
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# asgard server # shows subcommand help
|
|
9
|
+
# asgard server start
|
|
10
|
+
# asgard server start 4000 --workers 4 --daemon
|
|
11
|
+
# asgard server stop --force
|
|
12
|
+
# asgard server status
|
|
13
|
+
# asgard server restart 4000
|
|
14
|
+
|
|
15
|
+
class ServerCommands < Tasks
|
|
16
|
+
desc "start [PORT]", "Start the server on PORT (default: 3000)"
|
|
17
|
+
option :daemon, aliases: "-d", type: :boolean, default: false, desc: "Run as a background daemon"
|
|
18
|
+
option :workers, aliases: "-w", type: :numeric, default: 2, desc: "Number of worker processes"
|
|
19
|
+
option :log, type: :string, default: "log/server.log",
|
|
20
|
+
banner: "FILE", desc: "Write logs to FILE"
|
|
21
|
+
def start(port = "3000")
|
|
22
|
+
puts "Starting server on :%s with %d workers%s" % [
|
|
23
|
+
port,
|
|
24
|
+
options[:workers],
|
|
25
|
+
options[:daemon] ? " (daemon)" : ""
|
|
26
|
+
]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
desc "stop", "Stop the running server"
|
|
30
|
+
option :force, aliases: "-f", type: :boolean, default: false, desc: "Force-kill without draining"
|
|
31
|
+
option :wait, type: :numeric, default: 30, desc: "Seconds to wait for shutdown"
|
|
32
|
+
def stop
|
|
33
|
+
if options[:force]
|
|
34
|
+
puts "Force-stopping server..."
|
|
35
|
+
else
|
|
36
|
+
puts "Gracefully stopping server (timeout: #{options[:wait]}s)..."
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
desc "status", "Show server status and process info"
|
|
41
|
+
def status
|
|
42
|
+
puts "Checking server status..."
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# depends_on works inside subcommand groups — stop runs before start
|
|
46
|
+
depends_on :stop, :start
|
|
47
|
+
desc "restart [PORT]", "Restart the server on PORT (stop, then start)"
|
|
48
|
+
def restart(port = "3000")
|
|
49
|
+
puts "Server restarted on port #{port}."
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
class Tasks
|
|
54
|
+
desc "server SUBCOMMAND", "Manage the application server"
|
|
55
|
+
subcommand "server", ServerCommands
|
|
56
|
+
end
|
data/lib/asgard/base.rb
CHANGED
|
@@ -19,8 +19,10 @@ module Asgard
|
|
|
19
19
|
subclass.instance_variable_set(:@_deps, {})
|
|
20
20
|
subclass.instance_variable_set(:@_vars, {})
|
|
21
21
|
subclass.instance_variable_set(:@_pending_deps, [])
|
|
22
|
-
subclass.instance_variable_set(:@
|
|
23
|
-
subclass.instance_variable_set(:@
|
|
22
|
+
subclass.instance_variable_set(:@_running, Set.new)
|
|
23
|
+
subclass.instance_variable_set(:@_done, Set.new)
|
|
24
|
+
subclass.instance_variable_set(:@_cond, Hash.new { |h, k| h[k] = ConditionVariable.new })
|
|
25
|
+
subclass.instance_variable_set(:@_ran_mutex, Mutex.new)
|
|
24
26
|
end
|
|
25
27
|
|
|
26
28
|
def _deps
|
|
@@ -31,8 +33,16 @@ module Asgard
|
|
|
31
33
|
@_vars ||= {}
|
|
32
34
|
end
|
|
33
35
|
|
|
34
|
-
def
|
|
35
|
-
@
|
|
36
|
+
def _running
|
|
37
|
+
@_running ||= Set.new
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def _done
|
|
41
|
+
@_done ||= Set.new
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def _cond
|
|
45
|
+
@_cond ||= Hash.new { |h, k| h[k] = ConditionVariable.new }
|
|
36
46
|
end
|
|
37
47
|
|
|
38
48
|
def _ran_mutex
|
|
@@ -41,7 +51,11 @@ module Asgard
|
|
|
41
51
|
|
|
42
52
|
# Reset execution tracking for a fresh asgard invocation.
|
|
43
53
|
def _reset_ran!
|
|
44
|
-
_ran_mutex.synchronize
|
|
54
|
+
_ran_mutex.synchronize do
|
|
55
|
+
@_running = Set.new
|
|
56
|
+
@_done = Set.new
|
|
57
|
+
@_cond = Hash.new { |h, k| h[k] = ConditionVariable.new }
|
|
58
|
+
end
|
|
45
59
|
end
|
|
46
60
|
|
|
47
61
|
# Translate stages into a DependencyGraph-compatible hash.
|
|
@@ -57,15 +71,15 @@ module Asgard
|
|
|
57
71
|
graph
|
|
58
72
|
end
|
|
59
73
|
|
|
60
|
-
# Declare dependencies for the next
|
|
74
|
+
# Declare dependencies for the next task.
|
|
61
75
|
# Bare symbols run sequentially; arrays within the splat run in parallel.
|
|
62
76
|
#
|
|
63
77
|
# depends_on :build # sequential
|
|
64
78
|
# depends_on :build, :lint # both sequential
|
|
65
79
|
# depends_on [:build, :lint] # build and lint in parallel
|
|
66
80
|
# depends_on :setup, [:build, :lint], :test # setup, then build+lint, then test
|
|
67
|
-
def depends_on(*
|
|
68
|
-
@_pending_deps =
|
|
81
|
+
def depends_on(*tasks)
|
|
82
|
+
@_pending_deps = tasks
|
|
69
83
|
end
|
|
70
84
|
|
|
71
85
|
def var(name, value = nil, &block)
|
|
@@ -73,8 +87,12 @@ module Asgard
|
|
|
73
87
|
_vars[name.to_sym] = value
|
|
74
88
|
no_commands do
|
|
75
89
|
define_method(name) do
|
|
76
|
-
|
|
77
|
-
|
|
90
|
+
ivar = :"@__var_#{name}"
|
|
91
|
+
unless instance_variable_defined?(ivar)
|
|
92
|
+
v = self.class._vars[name.to_sym]
|
|
93
|
+
instance_variable_set(ivar, v.respond_to?(:call) ? v.call : v)
|
|
94
|
+
end
|
|
95
|
+
instance_variable_get(ivar)
|
|
78
96
|
end
|
|
79
97
|
end
|
|
80
98
|
end
|
|
@@ -90,19 +108,44 @@ module Asgard
|
|
|
90
108
|
|
|
91
109
|
# Validate the full dep graph for cycles using Dagwood::DependencyGraph.
|
|
92
110
|
def validate_deps!
|
|
111
|
+
pending = Array(@_pending_deps)
|
|
112
|
+
if pending.any?
|
|
113
|
+
raise Asgard::Error,
|
|
114
|
+
"depends_on(#{pending.join(', ')}) declared without a following task definition"
|
|
115
|
+
end
|
|
116
|
+
|
|
93
117
|
return if _deps.empty?
|
|
94
118
|
|
|
95
|
-
|
|
96
|
-
full_graph
|
|
119
|
+
all_task_names = all_commands.keys.map(&:to_sym)
|
|
120
|
+
full_graph = all_task_names.each_with_object({}) do |task, hash|
|
|
97
121
|
hash[task] = _deps.fetch(task, []).flatten
|
|
98
122
|
end
|
|
99
123
|
|
|
124
|
+
undefined = _deps.values.flatten.uniq - all_task_names
|
|
125
|
+
if undefined.any?
|
|
126
|
+
raise Asgard::Error, "undefined task(s) in depends_on: #{undefined.sort.join(', ')}"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
_deps.each do |_task, stages|
|
|
130
|
+
stages.flatten.each do |dep|
|
|
131
|
+
meth = instance_method(dep.to_s) rescue nil
|
|
132
|
+
next unless meth
|
|
133
|
+
required = meth.parameters.count { |type, _| type == :req }
|
|
134
|
+
if required > 0
|
|
135
|
+
raise Asgard::Error,
|
|
136
|
+
"task '#{dep}' has #{required} required argument(s) and cannot be used as a dependency"
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
100
141
|
Dagwood::DependencyGraph.new(full_graph).order
|
|
101
142
|
rescue TSort::Cyclic => e
|
|
102
143
|
raise Asgard::CircularDependencyError, e.message
|
|
103
144
|
end
|
|
104
145
|
|
|
105
146
|
def method_added(method_name)
|
|
147
|
+
return super unless @usage
|
|
148
|
+
|
|
106
149
|
pending = Array(@_pending_deps).dup
|
|
107
150
|
@_pending_deps = []
|
|
108
151
|
|
|
@@ -117,34 +160,56 @@ module Asgard
|
|
|
117
160
|
|
|
118
161
|
no_commands do
|
|
119
162
|
# Dispatch hook: resolves and runs all deps (in parallel where declared)
|
|
120
|
-
# before executing the target command.
|
|
121
|
-
#
|
|
163
|
+
# before executing the target command.
|
|
164
|
+
#
|
|
165
|
+
# Completion-based deduplication: a task is only marked done after its
|
|
166
|
+
# body finishes. Threads that arrive at an already-running shared dep
|
|
167
|
+
# wait on its ConditionVariable rather than proceeding immediately,
|
|
168
|
+
# preventing the race where parallel tasks start before a shared dep
|
|
169
|
+
# has actually completed.
|
|
122
170
|
def invoke_command(command, *args)
|
|
171
|
+
$DEBUG = true if options[:debug]
|
|
172
|
+
$VERBOSE = true if options[:verbose]
|
|
123
173
|
target = command.name.to_sym
|
|
124
174
|
|
|
125
175
|
should_run = self.class._ran_mutex.synchronize do
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
176
|
+
if self.class._done.include?(target)
|
|
177
|
+
false
|
|
178
|
+
elsif self.class._running.include?(target)
|
|
179
|
+
self.class._cond[target].wait(self.class._ran_mutex) until self.class._done.include?(target)
|
|
180
|
+
false
|
|
181
|
+
else
|
|
182
|
+
self.class._running.add(target)
|
|
183
|
+
true
|
|
184
|
+
end
|
|
129
185
|
end
|
|
130
186
|
return unless should_run
|
|
131
187
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
188
|
+
begin
|
|
189
|
+
stages = self.class._deps[target]
|
|
190
|
+
if stages&.any?
|
|
191
|
+
graph = self.class._build_dep_graph(stages)
|
|
192
|
+
groups = Dagwood::DependencyGraph.new(graph).parallel_order
|
|
193
|
+
|
|
194
|
+
groups.each do |group|
|
|
195
|
+
if group.size > 1
|
|
196
|
+
threads = group.map { |task| Thread.new { _run_dep(task) } }
|
|
197
|
+
errors = []
|
|
198
|
+
threads.each { |t| begin; t.join; rescue => e; errors << e; end }
|
|
199
|
+
raise errors.first if errors.any?
|
|
200
|
+
else
|
|
201
|
+
_run_dep(group.first)
|
|
202
|
+
end
|
|
143
203
|
end
|
|
144
204
|
end
|
|
145
|
-
end
|
|
146
205
|
|
|
147
|
-
|
|
206
|
+
command.run(self, *args)
|
|
207
|
+
ensure
|
|
208
|
+
self.class._ran_mutex.synchronize do
|
|
209
|
+
self.class._done.add(target)
|
|
210
|
+
self.class._cond[target].broadcast
|
|
211
|
+
end
|
|
212
|
+
end
|
|
148
213
|
end
|
|
149
214
|
|
|
150
215
|
def _run_dep(task)
|
data/lib/asgard/shell.rb
CHANGED
|
@@ -32,7 +32,9 @@ module Asgard
|
|
|
32
32
|
}
|
|
33
33
|
ext = extensions.fetch(interpreter.to_sym, ".tmp")
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
$stdout.puts script unless silent
|
|
36
|
+
|
|
37
|
+
Tempfile.create(["asgard_", ext]) do |f|
|
|
36
38
|
f.write(script)
|
|
37
39
|
f.flush
|
|
38
40
|
system(interpreter.to_s, f.path)
|
data/lib/asgard/tasks.rb
CHANGED
|
@@ -3,4 +3,32 @@
|
|
|
3
3
|
# Tasks is the single conventional entry point for all .loki files.
|
|
4
4
|
# It is pre-defined by the gem so .loki files never need to declare a class.
|
|
5
5
|
# Auxiliary *.loki files define modules which are imported into Tasks.
|
|
6
|
-
class Tasks < Asgard::Base
|
|
6
|
+
class Tasks < Asgard::Base
|
|
7
|
+
class_option :debug,
|
|
8
|
+
type: :boolean,
|
|
9
|
+
default: false,
|
|
10
|
+
desc: "Enable debug mode ($DEBUG = true)"
|
|
11
|
+
|
|
12
|
+
class_option :verbose,
|
|
13
|
+
type: :boolean,
|
|
14
|
+
default: false,
|
|
15
|
+
desc: "Enable verbose output ($VERBOSE = true)"
|
|
16
|
+
|
|
17
|
+
desc "--auto-load", "Load all *.loki files from the project root before running"
|
|
18
|
+
map "--auto-load" => :_auto_load
|
|
19
|
+
def _auto_load
|
|
20
|
+
# Consumed by run! before Thor dispatch — never called directly.
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
desc "--version", "Show asgard version"
|
|
24
|
+
map "--version" => :_version
|
|
25
|
+
def _version
|
|
26
|
+
puts Asgard::VERSION
|
|
27
|
+
exit
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def debug? = $DEBUG
|
|
33
|
+
def verbose? = $VERBOSE
|
|
34
|
+
end
|
data/lib/asgard/version.rb
CHANGED
data/lib/asgard.rb
CHANGED
|
@@ -24,7 +24,7 @@ module Asgard
|
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
# Load all *.loki files from dir in alphabetical order.
|
|
27
|
-
# Each file typically reopens class Tasks to add
|
|
27
|
+
# Each file typically reopens class Tasks to add tasks.
|
|
28
28
|
# The .loki entry point is excluded — it is loaded separately by run!.
|
|
29
29
|
def self.load_loki(dir)
|
|
30
30
|
Dir.glob(File.join(dir, "*.loki")).sort.each { |f| load f }
|
|
@@ -32,14 +32,19 @@ module Asgard
|
|
|
32
32
|
|
|
33
33
|
# Main entry point invoked by the asgard executable.
|
|
34
34
|
def self.run!(argv)
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
auto_load = argv.delete("--auto-load")
|
|
36
|
+
abort "asgard: unknown command '#{argv.first}'" if argv.first&.start_with?("_")
|
|
37
|
+
task_file = find_task_file or abort "asgard: no .loki file found in #{Dir.pwd}"
|
|
38
|
+
before = Asgard::Base.subclasses.dup
|
|
39
|
+
load_loki(File.dirname(task_file)) if auto_load
|
|
37
40
|
load task_file
|
|
38
|
-
|
|
41
|
+
newly_defined = Asgard::Base.subclasses - before
|
|
42
|
+
(newly_defined + [Tasks]).uniq.each(&:validate_deps!)
|
|
39
43
|
Tasks._reset_ran!
|
|
40
44
|
Tasks.start(argv)
|
|
41
45
|
rescue CircularDependencyError => e
|
|
42
|
-
|
|
43
|
-
|
|
46
|
+
abort "asgard: circular dependency — #{e.message}"
|
|
47
|
+
rescue Error => e
|
|
48
|
+
abort "asgard: #{e.message}"
|
|
44
49
|
end
|
|
45
50
|
end
|
data/mkdocs.yml
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# MkDocs Configuration for Asgard Documentation
|
|
2
|
+
site_name: Asgard
|
|
3
|
+
site_description: Thor-based Ruby task runner with dependency graphs and concurrent execution
|
|
4
|
+
site_author: Dewayne VanHoozer
|
|
5
|
+
site_url: https://madbomber.github.io/asgard
|
|
6
|
+
copyright: Copyright © 2026 Dewayne VanHoozer
|
|
7
|
+
|
|
8
|
+
# Repository information
|
|
9
|
+
repo_name: madbomber/asgard
|
|
10
|
+
repo_url: https://github.com/MadBomber/asgard
|
|
11
|
+
edit_uri: edit/main/docs/
|
|
12
|
+
|
|
13
|
+
# Configuration
|
|
14
|
+
theme:
|
|
15
|
+
name: material
|
|
16
|
+
|
|
17
|
+
# Color scheme
|
|
18
|
+
palette:
|
|
19
|
+
- scheme: default
|
|
20
|
+
primary: indigo
|
|
21
|
+
accent: amber
|
|
22
|
+
toggle:
|
|
23
|
+
icon: material/brightness-7
|
|
24
|
+
name: Switch to dark mode
|
|
25
|
+
|
|
26
|
+
- scheme: slate
|
|
27
|
+
primary: indigo
|
|
28
|
+
accent: amber
|
|
29
|
+
toggle:
|
|
30
|
+
icon: material/brightness-4
|
|
31
|
+
name: Switch to light mode
|
|
32
|
+
|
|
33
|
+
# Typography
|
|
34
|
+
font:
|
|
35
|
+
text: Roboto
|
|
36
|
+
code: Roboto Mono
|
|
37
|
+
|
|
38
|
+
# Logo and icon
|
|
39
|
+
icon:
|
|
40
|
+
repo: fontawesome/brands/github
|
|
41
|
+
logo: material/shield
|
|
42
|
+
|
|
43
|
+
# Theme features
|
|
44
|
+
features:
|
|
45
|
+
- navigation.instant
|
|
46
|
+
- navigation.tracking
|
|
47
|
+
- navigation.tabs
|
|
48
|
+
- navigation.tabs.sticky
|
|
49
|
+
- navigation.path
|
|
50
|
+
- navigation.indexes
|
|
51
|
+
- navigation.top
|
|
52
|
+
- navigation.footer
|
|
53
|
+
- toc.follow
|
|
54
|
+
- search.suggest
|
|
55
|
+
- search.highlight
|
|
56
|
+
- search.share
|
|
57
|
+
- header.autohide
|
|
58
|
+
- content.code.copy
|
|
59
|
+
- content.code.annotate
|
|
60
|
+
- content.tabs.link
|
|
61
|
+
- content.tooltips
|
|
62
|
+
- content.action.edit
|
|
63
|
+
- content.action.view
|
|
64
|
+
|
|
65
|
+
# Plugins
|
|
66
|
+
plugins:
|
|
67
|
+
- search:
|
|
68
|
+
separator: '[\s\-,:!=\[\]()"`/]+|\.(?!\d)|&[lg]t;|(?!\b)(?=[A-Z][a-z])'
|
|
69
|
+
- tags
|
|
70
|
+
|
|
71
|
+
# Extensions
|
|
72
|
+
markdown_extensions:
|
|
73
|
+
- abbr
|
|
74
|
+
- admonition
|
|
75
|
+
- attr_list
|
|
76
|
+
- def_list
|
|
77
|
+
- footnotes
|
|
78
|
+
- md_in_html
|
|
79
|
+
- tables
|
|
80
|
+
- toc:
|
|
81
|
+
permalink: true
|
|
82
|
+
title: On this page
|
|
83
|
+
|
|
84
|
+
- pymdownx.arithmatex:
|
|
85
|
+
generic: true
|
|
86
|
+
- pymdownx.betterem:
|
|
87
|
+
smart_enable: all
|
|
88
|
+
- pymdownx.caret
|
|
89
|
+
- pymdownx.critic
|
|
90
|
+
- pymdownx.details
|
|
91
|
+
- pymdownx.emoji:
|
|
92
|
+
emoji_generator: !!python/name:material.extensions.emoji.to_svg
|
|
93
|
+
emoji_index: !!python/name:material.extensions.emoji.twemoji
|
|
94
|
+
- pymdownx.highlight:
|
|
95
|
+
anchor_linenums: true
|
|
96
|
+
line_spans: __span
|
|
97
|
+
pygments_lang_class: true
|
|
98
|
+
- pymdownx.inlinehilite
|
|
99
|
+
- pymdownx.keys
|
|
100
|
+
- pymdownx.magiclink:
|
|
101
|
+
repo_url_shorthand: true
|
|
102
|
+
user: madbomber
|
|
103
|
+
repo: asgard
|
|
104
|
+
normalize_issue_symbols: true
|
|
105
|
+
- pymdownx.mark
|
|
106
|
+
- pymdownx.smartsymbols
|
|
107
|
+
- pymdownx.snippets:
|
|
108
|
+
check_paths: true
|
|
109
|
+
- pymdownx.superfences:
|
|
110
|
+
custom_fences:
|
|
111
|
+
- name: mermaid
|
|
112
|
+
class: mermaid
|
|
113
|
+
format: !!python/name:pymdownx.superfences.fence_code_format
|
|
114
|
+
- pymdownx.tabbed:
|
|
115
|
+
alternate_style: true
|
|
116
|
+
- pymdownx.tasklist:
|
|
117
|
+
custom_checkbox: true
|
|
118
|
+
- pymdownx.tilde
|
|
119
|
+
|
|
120
|
+
# Extra CSS
|
|
121
|
+
extra_css:
|
|
122
|
+
- assets/css/custom.css
|
|
123
|
+
|
|
124
|
+
# Social media and extra configuration
|
|
125
|
+
extra:
|
|
126
|
+
social:
|
|
127
|
+
- icon: fontawesome/brands/github
|
|
128
|
+
link: https://github.com/MadBomber/asgard
|
|
129
|
+
name: Asgard on GitHub
|
|
130
|
+
- icon: fontawesome/solid/gem
|
|
131
|
+
link: https://rubygems.org/gems/asgard
|
|
132
|
+
name: Asgard on RubyGems
|
|
133
|
+
|
|
134
|
+
analytics:
|
|
135
|
+
feedback:
|
|
136
|
+
title: Was this page helpful?
|
|
137
|
+
ratings:
|
|
138
|
+
- icon: material/emoticon-happy-outline
|
|
139
|
+
name: This page was helpful
|
|
140
|
+
data: 1
|
|
141
|
+
note: Thanks for your feedback!
|
|
142
|
+
- icon: material/emoticon-sad-outline
|
|
143
|
+
name: This page could be improved
|
|
144
|
+
data: 0
|
|
145
|
+
note: Thanks for your feedback! Help us improve by creating an issue.
|
|
146
|
+
|
|
147
|
+
# Navigation
|
|
148
|
+
nav:
|
|
149
|
+
- Home: index.md
|
|
150
|
+
- Getting Started: getting-started.md
|
|
151
|
+
- Tasks:
|
|
152
|
+
- Defining Tasks: tasks.md
|
|
153
|
+
- Dependencies: dependencies.md
|
|
154
|
+
- Variables: variables.md
|
|
155
|
+
- Helper Methods: helpers.md
|
|
156
|
+
- CLI:
|
|
157
|
+
- Options & Flags: options.md
|
|
158
|
+
- Subcommands: subcommands.md
|
|
159
|
+
- Shell Helpers: shell.md
|
|
160
|
+
- Environment: environment.md
|
|
161
|
+
- Task Files: task-files.md
|
|
162
|
+
- API Reference: api.md
|
|
163
|
+
- Examples: examples.md
|
|
164
|
+
- Changelog: changelog.md
|