asgard 0.1.2 → 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.
data/docs/tasks.md ADDED
@@ -0,0 +1,284 @@
1
+ # Defining Tasks
2
+
3
+ Every task is a public method inside `class Tasks`. Asgard pre-defines `Tasks` as a subclass of `Asgard::Base` (which is itself a Thor subclass), so your `.loki` files just reopen the class and add methods. The full Thor DSL is available everywhere.
4
+
5
+ ---
6
+
7
+ ## Basic Task
8
+
9
+ A task with no parameters and no options:
10
+
11
+ ```ruby
12
+ class Tasks
13
+ desc "hello", "Say hello"
14
+ def hello = sh 'echo "Hello, World!"'
15
+ end
16
+ ```
17
+
18
+ `desc` takes two arguments: the usage string and the one-line description shown in `asgard help`.
19
+
20
+ ```bash
21
+ asgard hello
22
+ ```
23
+
24
+ ---
25
+
26
+ ## Positional Parameter with Default
27
+
28
+ Positional parameters are declared directly in the method signature. Document them in the `desc` usage string (uppercase by convention):
29
+
30
+ ```ruby
31
+ class Tasks
32
+ desc "greet NAME", "Greet NAME; omit NAME to greet the world"
33
+ def greet(name = "World")
34
+ sh "echo 'Hello, #{name}!'"
35
+ end
36
+ end
37
+ ```
38
+
39
+ ```bash
40
+ asgard greet # Hello, World!
41
+ asgard greet Alice # Hello, Alice!
42
+ ```
43
+
44
+ ---
45
+
46
+ ## Named Options
47
+
48
+ Use `method_option` (alias: `option`) for named flags. Access them inside the method via `options[:name]`.
49
+
50
+ ### All Five Option Types
51
+
52
+ ```ruby
53
+ class Tasks
54
+ desc "compile", "Compile the project"
55
+ option :output, aliases: "-o", type: :string, default: "dist/", desc: "Output directory"
56
+ option :verbose, aliases: "-v", type: :boolean, default: false, desc: "Enable verbose output"
57
+ option :jobs, aliases: "-j", type: :numeric, default: 1, desc: "Number of parallel jobs"
58
+ option :tags, type: :array, desc: "Build tags to apply"
59
+ option :defines, type: :hash, desc: "Preprocessor defines (KEY:VALUE)"
60
+ def compile
61
+ puts "Compiling → #{options[:output]} with #{options[:jobs]} job(s)"
62
+ puts "Tags: #{options[:tags].join(', ')}" if options[:tags]
63
+ puts "Defines: #{options[:defines]}" if options[:defines]
64
+ end
65
+ end
66
+ ```
67
+
68
+ ### Option Types Reference
69
+
70
+ | Type | CLI Example | Ruby Value |
71
+ |---|---|---|
72
+ | `:string` | `--output dist/` | `"dist/"` |
73
+ | `:boolean` | `--verbose` / `--no-verbose` | `true` / `false` |
74
+ | `:numeric` | `--jobs 4` | `4` |
75
+ | `:array` | `--tags foo bar baz` | `["foo", "bar", "baz"]` |
76
+ | `:hash` | `--defines KEY:val FOO:bar` | `{"KEY"=>"val", "FOO"=>"bar"}` |
77
+
78
+ ### Common Option Keys
79
+
80
+ | Key | Description |
81
+ |---|---|
82
+ | `aliases` | Short-form flag, e.g. `"-o"` |
83
+ | `type` | `:string`, `:boolean`, `:numeric`, `:array`, or `:hash` |
84
+ | `default` | Value used when the flag is omitted |
85
+ | `required` | If `true`, Thor raises an error when the flag is missing |
86
+ | `desc` | One-line description shown in help |
87
+ | `enum` | Array of allowed values; Thor validates automatically |
88
+ | `banner` | Placeholder shown in help for the value slot, e.g. `"SECONDS"` |
89
+
90
+ ---
91
+
92
+ ## Required Option
93
+
94
+ ```ruby
95
+ class Tasks
96
+ desc "deploy ENV", "Deploy to ENV"
97
+ option :strategy,
98
+ type: :string,
99
+ required: true,
100
+ enum: %w[blue-green rolling canary],
101
+ desc: "Deployment strategy"
102
+ def deploy(env = "staging")
103
+ sh "cap #{env} deploy --strategy #{options[:strategy]}"
104
+ end
105
+ end
106
+ ```
107
+
108
+ ```bash
109
+ asgard deploy # Error: required option '--strategy' is missing
110
+ asgard deploy --strategy rolling
111
+ asgard deploy production --strategy blue-green
112
+ ```
113
+
114
+ ---
115
+
116
+ ## Enum Validation
117
+
118
+ ```ruby
119
+ class Tasks
120
+ desc "build", "Build the project"
121
+ option :env,
122
+ type: :string,
123
+ default: "development",
124
+ enum: %w[development staging production],
125
+ desc: "Target environment"
126
+ def build
127
+ sh "rake build ENV=#{options[:env]}"
128
+ end
129
+ end
130
+ ```
131
+
132
+ Thor validates the value against the enum and shows a helpful error if it doesn't match.
133
+
134
+ ---
135
+
136
+ ## Banner
137
+
138
+ `banner` replaces the default `VALUE` placeholder in help output with a more descriptive name:
139
+
140
+ ```ruby
141
+ class Tasks
142
+ desc "wait", "Wait for a service to become available"
143
+ option :timeout, type: :numeric, default: 30, banner: "SECONDS", desc: "Give up after SECONDS"
144
+ def wait
145
+ sh "wait-for-it --timeout #{options[:timeout]}"
146
+ end
147
+ end
148
+ ```
149
+
150
+ Help output shows: `[--timeout=SECONDS]` instead of `[--timeout=VALUE]`.
151
+
152
+ ---
153
+
154
+ ## Extended Description
155
+
156
+ `long_desc` provides detailed help shown by `asgard help <task>`. Use `\x5` at the start of a line to force a line break within the wrapped text (a Thor convention):
157
+
158
+ ```ruby
159
+ class Tasks
160
+ long_desc <<~DESC
161
+ Generates a project report covering test coverage, lint results,
162
+ and a dependency audit.
163
+
164
+ Pass --format to control output style. Use --since to scope the
165
+ report to changes after a given date.
166
+
167
+ Examples:\x5
168
+ asgard report --format html --since 2024-01-01\x5
169
+ asgard report --format json --output report.json\x5
170
+ asgard report --format text
171
+ DESC
172
+ desc "report", "Generate a project report"
173
+ option :format, type: :string, default: "text", enum: %w[text html json], desc: "Output format"
174
+ option :since, type: :string, banner: "DATE", desc: "Limit to changes after DATE"
175
+ def report
176
+ sh "generate-report --format #{options[:format]}"
177
+ end
178
+ end
179
+ ```
180
+
181
+ !!! tip
182
+ `desc` and `depends_on` are independent of each other — either can come first, but both must appear before the `def`.
183
+
184
+ ---
185
+
186
+ ## Default Task
187
+
188
+ `default_task` declares which command runs when `asgard` is invoked with no arguments:
189
+
190
+ ```ruby
191
+ class Tasks
192
+ default_task :greet
193
+
194
+ desc "greet", "Say hello (runs by default)"
195
+ def greet
196
+ puts "Hello from Asgard!"
197
+ end
198
+ end
199
+ ```
200
+
201
+ ```bash
202
+ asgard # same as: asgard greet
203
+ ```
204
+
205
+ ---
206
+
207
+ ## Command Aliases
208
+
209
+ `map` creates short aliases for existing tasks:
210
+
211
+ ```ruby
212
+ class Tasks
213
+ map "-v" => "version"
214
+ map "--v" => "version"
215
+ map "t" => "test"
216
+ map "b" => "build"
217
+
218
+ desc "version", "Print the version"
219
+ def version = puts Asgard::VERSION
220
+
221
+ desc "test", "Run tests"
222
+ def test = sh "bundle exec rake test"
223
+
224
+ desc "build", "Build the gem"
225
+ def build = sh "bundle exec rake build"
226
+ end
227
+ ```
228
+
229
+ ```bash
230
+ asgard t # same as: asgard test
231
+ asgard b # same as: asgard build
232
+ asgard -v # same as: asgard version (note: --version is the built-in flag)
233
+ ```
234
+
235
+ ---
236
+
237
+ ## Formal Argument Declaration
238
+
239
+ `argument` provides rich positional-parameter metadata including type checking, enums, and help text.
240
+
241
+ !!! warning "Class-level scope"
242
+ `argument` is a **class-level declaration** that applies to **every task in the class**, not just the one that follows it. It is best suited for single-command CLIs or when every task in the file genuinely shares the same positional input. In multi-task files, prefer method signature parameters instead.
243
+
244
+ ```ruby
245
+ class Tasks
246
+ argument :name,
247
+ type: :string,
248
+ default: "World",
249
+ desc: "Name to greet"
250
+
251
+ desc "hello NAME", "Say hello to NAME"
252
+ def hello = sh "echo 'Hello, #{name}!'"
253
+ end
254
+ ```
255
+
256
+ For most multi-task `.loki` files, the simpler positional default pattern is safer:
257
+
258
+ ```ruby
259
+ def hello(name = "World") = sh "echo 'Hello, #{name}!'"
260
+ ```
261
+
262
+ ---
263
+
264
+ ## No Commands Block
265
+
266
+ `no_commands` marks a block of methods as public helpers that are excluded from the CLI and `--help` output. They are callable from any task in the same class:
267
+
268
+ ```ruby
269
+ class Tasks
270
+ desc "build", "Compile the project"
271
+ def build
272
+ puts "Revision: #{current_sha}"
273
+ sh "rake build"
274
+ end
275
+
276
+ no_commands do
277
+ def current_sha
278
+ `git rev-parse --short HEAD`.strip
279
+ end
280
+ end
281
+ end
282
+ ```
283
+
284
+ See [Helper Methods](helpers.md) for the full guide on helpers, `private`, and cross-file sharing.
data/docs/variables.md ADDED
@@ -0,0 +1,122 @@
1
+ # Variables
2
+
3
+ `var` declares a named value that is available to all tasks in the class as a method call. Values can be static or lazily evaluated.
4
+
5
+ ---
6
+
7
+ ## Static Value
8
+
9
+ Pass the value directly as the second argument:
10
+
11
+ ```ruby
12
+ class Tasks
13
+ var :app, "myapp"
14
+ var :env, "production"
15
+ var :port, 3000
16
+
17
+ desc "info", "Print app info"
18
+ def info
19
+ puts "#{app} running on port #{port} in #{env}"
20
+ end
21
+ end
22
+ ```
23
+
24
+ ---
25
+
26
+ ## Lazy Lambda
27
+
28
+ Pass a lambda (or proc) to defer evaluation until the variable is first accessed. The lambda is called once and its return value is used for all subsequent accesses:
29
+
30
+ ```ruby
31
+ class Tasks
32
+ var :version, -> { `git describe --tags`.strip }
33
+ var :sha, -> { `git rev-parse --short HEAD`.strip }
34
+
35
+ desc "tag", "Create a release tag"
36
+ def tag = sh "git tag v#{version}"
37
+
38
+ desc "info", "Show version info"
39
+ def info = puts "#{version} (#{sha})"
40
+ end
41
+ ```
42
+
43
+ !!! tip
44
+ Lazy lambdas are ideal for values that require a shell call or file read — they only pay the cost if the variable is actually used in the task being run.
45
+
46
+ ---
47
+
48
+ ## Block Syntax
49
+
50
+ You can also use a block instead of a lambda:
51
+
52
+ ```ruby
53
+ class Tasks
54
+ var(:build_dir) { "builds/#{app}" }
55
+ var(:app) { "myapp" }
56
+
57
+ desc "build", "Compile into build_dir"
58
+ def build = sh "rake build OUTDIR=#{build_dir}"
59
+ end
60
+ ```
61
+
62
+ !!! note
63
+ The block form and the lambda form behave identically — both are stored as callables and invoked on first access.
64
+
65
+ ---
66
+
67
+ ## Accessing Variables from Tasks
68
+
69
+ Variables are available as method calls from within any task body (or other method) in the same class. They are defined using `no_commands`, so they appear neither in `--help` output nor as CLI commands:
70
+
71
+ ```ruby
72
+ class Tasks
73
+ var :app, "myapp"
74
+ var :version, -> { `git describe --tags`.strip }
75
+ var :pkg, -> { "pkg/#{app}-#{version}.gem" }
76
+
77
+ desc "build", "Build the gem"
78
+ def build = sh "gem build #{app}.gemspec"
79
+
80
+ desc "push", "Push the gem to RubyGems"
81
+ def push = sh "gem push #{pkg}"
82
+ end
83
+ ```
84
+
85
+ Variables can reference other variables in their lambdas as long as the referenced variable is also defined with `var` on the same class.
86
+
87
+ ---
88
+
89
+ ## Sharing Variables Across Files
90
+
91
+ Because all `.loki` files reopen the same `class Tasks`, variables declared in one file are available in all other files loaded in the same session:
92
+
93
+ ```ruby
94
+ # config.loki
95
+ class Tasks
96
+ var :app, "myapp"
97
+ var :port, 8080
98
+ end
99
+
100
+ # deploy.loki
101
+ class Tasks
102
+ desc "deploy", "Deploy the app"
103
+ def deploy = sh "cap deploy APP=#{app} PORT=#{port}"
104
+ end
105
+ ```
106
+
107
+ ---
108
+
109
+ ## Naming Caution
110
+
111
+ !!! warning
112
+ Do not use `var` names that conflict with built-in Ruby method names, Thor DSL method names, or Asgard's own built-in methods. In particular, avoid naming a variable `version` — `Tasks` already defines `_version` (the `--version` flag handler), and a `var :version` would collide with that namespace and produce confusing behavior. Use a more specific name like `app_version` or `gem_version` instead.
113
+
114
+ ```ruby
115
+ # Avoid this — conflicts with the built-in version infrastructure:
116
+ # var :version, -> { "1.0.0" }
117
+
118
+ # Use this instead:
119
+ var :app_version, -> { `git describe --tags`.strip }
120
+ ```
121
+
122
+ Other names to avoid: `options`, `class_options`, `shell`, `invoke`, `invoke_command`.
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+ # Demonstrates concurrent task execution via parallel depends_on groups.
3
+ #
4
+ # Each worker prints its letter repeatedly with random delays. Because
5
+ # worker_a, worker_b, and worker_c run in separate threads their output
6
+ # interleaves on stdout, proving real concurrency.
7
+ #
8
+ # Execution order:
9
+ # start → worker_a + worker_b + worker_c (all three concurrent) → finish
10
+ #
11
+ # Run with:
12
+ # asgard finish
13
+ #
14
+ # Sample output (character order varies every run):
15
+ # start
16
+ # ABCBACBACBABCBACBACB
17
+ # end
18
+
19
+ $stdout.sync = true # flush every print immediately across all threads
20
+
21
+ CONCURRENT_REPS = 10 # how many times each worker prints its character
22
+
23
+ class Tasks
24
+ desc "start", "Print start marker"
25
+ def start
26
+ puts "starting demo of concurrent task execution ..."
27
+ end
28
+
29
+ desc "worker_a", "Print 'A' repeatedly with random delays"
30
+ def worker_a
31
+ CONCURRENT_REPS.times do
32
+ print "A"
33
+ sleep rand(0.05..0.3)
34
+ end
35
+ end
36
+
37
+ desc "worker_b", "Print 'B' repeatedly with random delays"
38
+ def worker_b
39
+ CONCURRENT_REPS.times do
40
+ print "B"
41
+ sleep rand(0.05..0.3)
42
+ end
43
+ end
44
+
45
+ desc "worker_c", "Print 'C' repeatedly with random delays"
46
+ def worker_c
47
+ CONCURRENT_REPS.times do
48
+ print "C"
49
+ sleep rand(0.05..0.3)
50
+ end
51
+ end
52
+
53
+ depends_on :start, [:worker_a, :worker_b, :worker_c]
54
+ desc "finish", "Print end marker after all workers complete"
55
+ def finish
56
+ puts "\nfini - the end of concurrent task demo"
57
+ end
58
+ 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(:@_ran_tasks, Set.new)
23
- subclass.instance_variable_set(:@_ran_mutex, Mutex.new)
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 _ran_tasks
35
- @_ran_tasks ||= Set.new
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 { @_ran_tasks = Set.new }
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.
@@ -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
- v = self.class._vars[name.to_sym]
77
- v.respond_to?(:call) ? v.call : v
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
- all_tasks = all_commands.keys.map(&:to_sym)
96
- full_graph = all_tasks.each_with_object({}) do |task, hash|
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,36 +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. Thread-safe deduplication via
121
- # the class-level _ran_tasks set ensures each task runs at most once.
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)
123
171
  $DEBUG = true if options[:debug]
124
172
  $VERBOSE = true if options[:verbose]
125
173
  target = command.name.to_sym
126
174
 
127
175
  should_run = self.class._ran_mutex.synchronize do
128
- next false if self.class._ran_tasks.include?(target)
129
- self.class._ran_tasks.add(target)
130
- true
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
131
185
  end
132
186
  return unless should_run
133
187
 
134
- stages = self.class._deps[target]
135
- if stages&.any?
136
- graph = self.class._build_dep_graph(stages)
137
- groups = Dagwood::DependencyGraph.new(graph).parallel_order
138
-
139
- groups.each do |group|
140
- if group.size > 1
141
- threads = group.map { |task| Thread.new { _run_dep(task) } }
142
- threads.each(&:join)
143
- else
144
- _run_dep(group.first)
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
145
203
  end
146
204
  end
147
- end
148
205
 
149
- command.run(self, *args)
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
150
213
  end
151
214
 
152
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
- Tempfile.create(["asgard_", ext]) do |f|
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
@@ -14,6 +14,12 @@ class Tasks < Asgard::Base
14
14
  default: false,
15
15
  desc: "Enable verbose output ($VERBOSE = true)"
16
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
+
17
23
  desc "--version", "Show asgard version"
18
24
  map "--version" => :_version
19
25
  def _version
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Asgard
4
- VERSION = "0.1.2"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/asgard.rb CHANGED
@@ -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
+ auto_load = argv.delete("--auto-load")
35
36
  abort "asgard: unknown command '#{argv.first}'" if argv.first&.start_with?("_")
36
37
  task_file = find_task_file or abort "asgard: no .loki file found in #{Dir.pwd}"
37
- load_loki(File.dirname(task_file))
38
+ before = Asgard::Base.subclasses.dup
39
+ load_loki(File.dirname(task_file)) if auto_load
38
40
  load task_file
39
- Tasks.validate_deps!
41
+ newly_defined = Asgard::Base.subclasses - before
42
+ (newly_defined + [Tasks]).uniq.each(&:validate_deps!)
40
43
  Tasks._reset_ran!
41
44
  Tasks.start(argv)
42
45
  rescue CircularDependencyError => e
43
46
  abort "asgard: circular dependency — #{e.message}"
47
+ rescue Error => e
48
+ abort "asgard: #{e.message}"
44
49
  end
45
50
  end