asgard 0.2.0 → 0.3.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/.loki +7 -9
- data/.rubocop.yml +157 -0
- data/CHANGELOG.md +49 -11
- data/CLAUDE.md +19 -9
- data/README.md +78 -53
- data/Rakefile +83 -4
- data/docs/api.md +86 -13
- data/docs/changelog.md +4 -0
- data/docs/dependencies.md +25 -25
- data/docs/environment.md +30 -14
- data/docs/examples.md +3 -3
- data/docs/getting-started.md +5 -6
- data/docs/helpers.md +34 -10
- data/docs/index.md +6 -6
- data/docs/options.md +2 -2
- data/docs/shell.md +11 -11
- data/docs/subcommands.md +9 -9
- data/docs/task-files.md +266 -113
- data/docs/tasks.md +17 -15
- data/docs/variables.md +267 -51
- data/examples/.env +4 -0
- data/examples/.loki +24 -2
- data/examples/concurrent.loki +5 -5
- data/examples/db_subcommands.loki +3 -3
- data/examples/env_usage.loki +27 -0
- data/examples/kitchen_sink.loki +48 -15
- data/examples/server_subcommands.loki +3 -3
- data/examples/subdir/.loki +12 -0
- data/examples/subdir/import_demo.loki +14 -0
- data/examples/subdir/import_up_demo.loki +18 -0
- data/lib/asgard/base.rb +125 -83
- data/lib/asgard/kernel_methods.rb +77 -0
- data/lib/asgard/shell.rb +8 -7
- data/lib/asgard/tasks.rb +0 -11
- data/lib/asgard/version.rb +1 -1
- data/lib/asgard.rb +2 -18
- metadata +13 -4
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# Demonstrates Thor subcommands registered on the top-level Tasks class.
|
|
3
3
|
#
|
|
4
4
|
# The subcommand class inherits from Tasks so it has access to sh, shebang,
|
|
5
|
-
#
|
|
5
|
+
# depends_on, and the built-in --debug/--verbose class options.
|
|
6
6
|
#
|
|
7
7
|
# Usage:
|
|
8
8
|
# asgard server # shows subcommand help
|
|
@@ -26,7 +26,7 @@ class ServerCommands < Tasks
|
|
|
26
26
|
]
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
desc "
|
|
29
|
+
desc "Stop the running server"
|
|
30
30
|
option :force, aliases: "-f", type: :boolean, default: false, desc: "Force-kill without draining"
|
|
31
31
|
option :wait, type: :numeric, default: 30, desc: "Seconds to wait for shutdown"
|
|
32
32
|
def stop
|
|
@@ -37,7 +37,7 @@ class ServerCommands < Tasks
|
|
|
37
37
|
end
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
-
desc "
|
|
40
|
+
desc "Show server status and process info"
|
|
41
41
|
def status
|
|
42
42
|
puts "Checking server status..."
|
|
43
43
|
end
|
|
@@ -0,0 +1,12 @@
|
|
|
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"
|
|
@@ -0,0 +1,14 @@
|
|
|
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
|
|
@@ -0,0 +1,18 @@
|
|
|
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"
|
data/lib/asgard/base.rb
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "thor"
|
|
4
|
-
require "set"
|
|
5
4
|
require "dagwood"
|
|
6
5
|
|
|
7
6
|
module Asgard
|
|
@@ -16,9 +15,10 @@ module Asgard
|
|
|
16
15
|
def inherited(subclass)
|
|
17
16
|
super
|
|
18
17
|
Asgard::Base.subclasses << subclass
|
|
19
|
-
subclass.instance_variable_set(:@_deps,
|
|
20
|
-
subclass.instance_variable_set(:@
|
|
21
|
-
subclass.instance_variable_set(:@
|
|
18
|
+
subclass.instance_variable_set(:@_deps, {})
|
|
19
|
+
subclass.instance_variable_set(:@_pending_deps, [])
|
|
20
|
+
subclass.instance_variable_set(:@_pending_single_desc, nil)
|
|
21
|
+
subclass.instance_variable_set(:@_pending_single_desc_opts, nil)
|
|
22
22
|
subclass.instance_variable_set(:@_running, Set.new)
|
|
23
23
|
subclass.instance_variable_set(:@_done, Set.new)
|
|
24
24
|
subclass.instance_variable_set(:@_cond, Hash.new { |h, k| h[k] = ConditionVariable.new })
|
|
@@ -29,10 +29,6 @@ module Asgard
|
|
|
29
29
|
@_deps ||= {}
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
-
def _vars
|
|
33
|
-
@_vars ||= {}
|
|
34
|
-
end
|
|
35
|
-
|
|
36
32
|
def _running
|
|
37
33
|
@_running ||= Set.new
|
|
38
34
|
end
|
|
@@ -65,7 +61,7 @@ module Asgard
|
|
|
65
61
|
def _build_dep_graph(stages)
|
|
66
62
|
graph = {}
|
|
67
63
|
stages.each_with_index do |stage, i|
|
|
68
|
-
prev_stage = i
|
|
64
|
+
prev_stage = i.positive? ? stages[i - 1] : []
|
|
69
65
|
stage.each { |task| graph[task] = prev_stage.dup }
|
|
70
66
|
end
|
|
71
67
|
graph
|
|
@@ -82,68 +78,98 @@ module Asgard
|
|
|
82
78
|
@_pending_deps = tasks
|
|
83
79
|
end
|
|
84
80
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
end
|
|
81
|
+
# Allow single-argument desc: desc "Run the tests"
|
|
82
|
+
# The usage string defaults to the method name when the description is the only arg.
|
|
83
|
+
def desc(usage_or_desc, description = nil, options = {})
|
|
84
|
+
if description.nil? || description.is_a?(Hash)
|
|
85
|
+
options = description if description.is_a?(Hash)
|
|
86
|
+
@_pending_single_desc = usage_or_desc
|
|
87
|
+
@_pending_single_desc_opts = options
|
|
88
|
+
else
|
|
89
|
+
@_pending_single_desc = nil
|
|
90
|
+
@_pending_single_desc_opts = nil
|
|
91
|
+
super
|
|
97
92
|
end
|
|
98
93
|
end
|
|
99
94
|
|
|
100
|
-
def import(mod)
|
|
101
|
-
include mod
|
|
102
|
-
end
|
|
103
|
-
|
|
104
95
|
def dotenv(path = ".env")
|
|
105
96
|
require "dotenv"
|
|
106
97
|
Dotenv.load(path) if File.exist?(path)
|
|
107
98
|
end
|
|
108
99
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
100
|
+
def default_task(meth = nil)
|
|
101
|
+
if meth && meth != :none && @_default_task_location
|
|
102
|
+
here = caller_locations(1, 1).first
|
|
103
|
+
warn "asgard: default_task :#{meth} at #{here.path}:#{here.lineno} " \
|
|
104
|
+
"overrides default_task :#{@_default_task_name} set at " \
|
|
105
|
+
"#{@_default_task_location.path}:#{@_default_task_location.lineno}"
|
|
106
|
+
end
|
|
107
|
+
if meth && meth != :none
|
|
108
|
+
@_default_task_location = caller_locations(1, 1).first
|
|
109
|
+
@_default_task_name = meth
|
|
115
110
|
end
|
|
111
|
+
super
|
|
112
|
+
end
|
|
116
113
|
|
|
114
|
+
# Validate the full dep graph for cycles using Dagwood::DependencyGraph.
|
|
115
|
+
def validate_deps!
|
|
116
|
+
_check_orphaned_deps!
|
|
117
117
|
return if _deps.empty?
|
|
118
118
|
|
|
119
119
|
all_task_names = all_commands.keys.map(&:to_sym)
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
120
|
+
_check_undefined_deps!(all_task_names)
|
|
121
|
+
_check_dep_arities!
|
|
122
|
+
_build_and_sort_graph(all_task_names)
|
|
123
|
+
rescue TSort::Cyclic => e
|
|
124
|
+
raise Asgard::CircularDependencyError, e.message
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
private
|
|
128
|
+
|
|
129
|
+
def _check_orphaned_deps!
|
|
130
|
+
pending = Array(@_pending_deps)
|
|
131
|
+
return unless pending.any?
|
|
123
132
|
|
|
133
|
+
raise Asgard::Error,
|
|
134
|
+
"depends_on(#{pending.join(', ')}) declared without a following task definition"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def _check_undefined_deps!(all_task_names)
|
|
124
138
|
undefined = _deps.values.flatten.uniq - all_task_names
|
|
125
|
-
|
|
126
|
-
raise Asgard::Error, "undefined task(s) in depends_on: #{undefined.sort.join(', ')}"
|
|
127
|
-
end
|
|
139
|
+
return unless undefined.any?
|
|
128
140
|
|
|
129
|
-
|
|
141
|
+
raise Asgard::Error, "undefined task(s) in depends_on: #{undefined.sort.join(', ')}"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def _check_dep_arities!
|
|
145
|
+
_deps.each_value do |stages|
|
|
130
146
|
stages.flatten.each do |dep|
|
|
131
|
-
meth = instance_method(dep.to_s)
|
|
132
|
-
next unless meth
|
|
147
|
+
meth = instance_method(dep.to_s)
|
|
133
148
|
required = meth.parameters.count { |type, _| type == :req }
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
149
|
+
next unless required.positive?
|
|
150
|
+
|
|
151
|
+
raise Asgard::Error,
|
|
152
|
+
"task '#{dep}' has #{required} required argument(s) and cannot be used as a dependency"
|
|
138
153
|
end
|
|
139
154
|
end
|
|
155
|
+
end
|
|
140
156
|
|
|
157
|
+
def _build_and_sort_graph(all_task_names)
|
|
158
|
+
full_graph = all_task_names.to_h { |task| [task, _deps.fetch(task, []).flatten] }
|
|
141
159
|
Dagwood::DependencyGraph.new(full_graph).order
|
|
142
|
-
rescue TSort::Cyclic => e
|
|
143
|
-
raise Asgard::CircularDependencyError, e.message
|
|
144
160
|
end
|
|
145
161
|
|
|
162
|
+
public
|
|
163
|
+
|
|
146
164
|
def method_added(method_name)
|
|
165
|
+
if @_pending_single_desc && !no_commands?
|
|
166
|
+
pending_desc = @_pending_single_desc
|
|
167
|
+
pending_opts = @_pending_single_desc_opts || {}
|
|
168
|
+
@_pending_single_desc = nil
|
|
169
|
+
@_pending_single_desc_opts = nil
|
|
170
|
+
desc(method_name.to_s, pending_desc, pending_opts)
|
|
171
|
+
end
|
|
172
|
+
|
|
147
173
|
return super unless @usage
|
|
148
174
|
|
|
149
175
|
pending = Array(@_pending_deps).dup
|
|
@@ -171,51 +197,67 @@ module Asgard
|
|
|
171
197
|
$DEBUG = true if options[:debug]
|
|
172
198
|
$VERBOSE = true if options[:verbose]
|
|
173
199
|
target = command.name.to_sym
|
|
174
|
-
|
|
175
|
-
should_run = self.class._ran_mutex.synchronize do
|
|
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
|
|
185
|
-
end
|
|
186
|
-
return unless should_run
|
|
200
|
+
return unless acquire_run_token(target)
|
|
187
201
|
|
|
188
202
|
begin
|
|
189
|
-
|
|
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
|
|
203
|
-
end
|
|
204
|
-
end
|
|
205
|
-
|
|
203
|
+
run_deps_for(target)
|
|
206
204
|
command.run(self, *args)
|
|
207
205
|
ensure
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
206
|
+
signal_done(target)
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
private
|
|
212
|
+
|
|
213
|
+
def acquire_run_token(target)
|
|
214
|
+
self.class._ran_mutex.synchronize do
|
|
215
|
+
if self.class._done.include?(target)
|
|
216
|
+
false
|
|
217
|
+
elsif self.class._running.include?(target)
|
|
218
|
+
self.class._cond[target].wait(self.class._ran_mutex) until self.class._done.include?(target)
|
|
219
|
+
false
|
|
220
|
+
else
|
|
221
|
+
self.class._running.add(target)
|
|
222
|
+
true
|
|
212
223
|
end
|
|
213
224
|
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def run_deps_for(target)
|
|
228
|
+
stages = self.class._deps[target]
|
|
229
|
+
return unless stages&.any?
|
|
230
|
+
|
|
231
|
+
groups = Dagwood::DependencyGraph.new(self.class._build_dep_graph(stages)).parallel_order
|
|
232
|
+
groups.each { |group| run_dep_group(group) }
|
|
233
|
+
end
|
|
214
234
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
235
|
+
def run_dep_group(group)
|
|
236
|
+
if group.size > 1
|
|
237
|
+
threads = group.map { |task| Thread.new { run_dep(task) } }
|
|
238
|
+
errors = []
|
|
239
|
+
threads.each { |t| begin; t.join; rescue => e; errors << e; end }
|
|
240
|
+
if errors.size == 1
|
|
241
|
+
raise errors.first
|
|
242
|
+
elsif errors.any?
|
|
243
|
+
errors.each { |e| warn "asgard: #{e.message}" }
|
|
244
|
+
raise Asgard::Error, "#{errors.size} parallel dependencies failed"
|
|
245
|
+
end
|
|
246
|
+
else
|
|
247
|
+
run_dep(group.first)
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def signal_done(target)
|
|
252
|
+
self.class._ran_mutex.synchronize do
|
|
253
|
+
self.class._done.add(target)
|
|
254
|
+
self.class._cond[target].broadcast
|
|
218
255
|
end
|
|
219
256
|
end
|
|
257
|
+
|
|
258
|
+
def run_dep(task)
|
|
259
|
+
command = self.class.all_commands[task.to_s]
|
|
260
|
+
invoke_command(command) if command
|
|
261
|
+
end
|
|
220
262
|
end
|
|
221
263
|
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kernel
|
|
4
|
+
def debug? = $DEBUG
|
|
5
|
+
def verbose? = $VERBOSE
|
|
6
|
+
module_function :debug?, :verbose?
|
|
7
|
+
|
|
8
|
+
# Fetch an environment variable by symbol or string name.
|
|
9
|
+
# The name is converted to an uppercase string automatically.
|
|
10
|
+
# Raises KeyError when the variable is missing and no default is given.
|
|
11
|
+
def env(name, default = nil)
|
|
12
|
+
key = name.to_s.upcase
|
|
13
|
+
default.nil? ? ENV.fetch(key) : ENV.fetch(key, default)
|
|
14
|
+
end
|
|
15
|
+
module_function :env
|
|
16
|
+
|
|
17
|
+
def loki_up(name = ".loki")
|
|
18
|
+
dir = Dir.pwd
|
|
19
|
+
loop do
|
|
20
|
+
candidate = File.join(dir, name)
|
|
21
|
+
return candidate if File.exist?(candidate)
|
|
22
|
+
parent = File.dirname(dir)
|
|
23
|
+
break if parent == dir
|
|
24
|
+
dir = parent
|
|
25
|
+
end
|
|
26
|
+
nil
|
|
27
|
+
end
|
|
28
|
+
module_function :loki_up
|
|
29
|
+
|
|
30
|
+
def import(path)
|
|
31
|
+
path = path.to_s
|
|
32
|
+
raise ArgumentError, "import: path must end with .loki (got #{path.inspect})" unless path.end_with?(".loki")
|
|
33
|
+
unless File.absolute_path?(path)
|
|
34
|
+
caller_dir = File.dirname(caller_locations(1, 1).first.absolute_path)
|
|
35
|
+
path = File.expand_path(path, caller_dir)
|
|
36
|
+
end
|
|
37
|
+
paths = path =~ /[*?\[{]/ ? Dir.glob(path) : [path]
|
|
38
|
+
loaded = paths.map do |p|
|
|
39
|
+
if $LOADED_FEATURES.include?(p)
|
|
40
|
+
warn "import: skip #{p} (already loaded)" if debug?
|
|
41
|
+
next false
|
|
42
|
+
end
|
|
43
|
+
warn "import: #{p}" if verbose? || debug?
|
|
44
|
+
load p
|
|
45
|
+
$LOADED_FEATURES << p
|
|
46
|
+
true
|
|
47
|
+
end
|
|
48
|
+
loaded.any?
|
|
49
|
+
end
|
|
50
|
+
module_function :import
|
|
51
|
+
|
|
52
|
+
def import_up(name = ".loki")
|
|
53
|
+
if name =~ /[*?\[{]/
|
|
54
|
+
dir = Dir.pwd
|
|
55
|
+
loop do
|
|
56
|
+
matches = Dir.glob(File.join(dir, name))
|
|
57
|
+
unless matches.empty?
|
|
58
|
+
warn "import_up: #{name} → #{dir}" if verbose? || debug?
|
|
59
|
+
return matches.map { |p| import(p) }.any?
|
|
60
|
+
end
|
|
61
|
+
parent = File.dirname(dir)
|
|
62
|
+
break if parent == dir
|
|
63
|
+
dir = parent
|
|
64
|
+
end
|
|
65
|
+
warn "import_up: #{name} not found" if debug?
|
|
66
|
+
return false
|
|
67
|
+
end
|
|
68
|
+
path = loki_up(name)
|
|
69
|
+
unless path
|
|
70
|
+
warn "import_up: #{name} not found" if debug?
|
|
71
|
+
return false
|
|
72
|
+
end
|
|
73
|
+
warn "import_up: #{name} → #{path}" if verbose? || debug?
|
|
74
|
+
import path
|
|
75
|
+
end
|
|
76
|
+
module_function :import_up
|
|
77
|
+
end
|
data/lib/asgard/shell.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'English'
|
|
3
4
|
require "tempfile"
|
|
4
5
|
|
|
5
6
|
module Asgard
|
|
@@ -12,12 +13,12 @@ module Asgard
|
|
|
12
13
|
$stdout.puts script unless silent
|
|
13
14
|
|
|
14
15
|
success = if script.include?("\n")
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
system("bash", "-c", script)
|
|
17
|
+
else
|
|
18
|
+
system(script)
|
|
19
|
+
end
|
|
19
20
|
|
|
20
|
-
exit(
|
|
21
|
+
exit($CHILD_STATUS.exitstatus) unless success
|
|
21
22
|
end
|
|
22
23
|
|
|
23
24
|
# Write +script+ to a tempfile and execute it with +interpreter+.
|
|
@@ -34,11 +35,11 @@ module Asgard
|
|
|
34
35
|
|
|
35
36
|
$stdout.puts script unless silent
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
Tempfile.create(["asgard_", ext]) do |f|
|
|
38
39
|
f.write(script)
|
|
39
40
|
f.flush
|
|
40
41
|
system(interpreter.to_s, f.path)
|
|
41
|
-
exit(
|
|
42
|
+
exit($CHILD_STATUS.exitstatus) unless $CHILD_STATUS.success?
|
|
42
43
|
end
|
|
43
44
|
end
|
|
44
45
|
end
|
data/lib/asgard/tasks.rb
CHANGED
|
@@ -14,21 +14,10 @@ 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
|
-
|
|
23
17
|
desc "--version", "Show asgard version"
|
|
24
18
|
map "--version" => :_version
|
|
25
19
|
def _version
|
|
26
20
|
puts Asgard::VERSION
|
|
27
21
|
exit
|
|
28
22
|
end
|
|
29
|
-
|
|
30
|
-
private
|
|
31
|
-
|
|
32
|
-
def debug? = $DEBUG
|
|
33
|
-
def verbose? = $VERBOSE
|
|
34
23
|
end
|
data/lib/asgard/version.rb
CHANGED
data/lib/asgard.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "asgard/version"
|
|
4
|
+
require_relative "asgard/kernel_methods"
|
|
4
5
|
require_relative "asgard/shell"
|
|
5
6
|
require_relative "asgard/base"
|
|
6
7
|
require_relative "asgard/tasks"
|
|
@@ -12,31 +13,14 @@ module Asgard
|
|
|
12
13
|
# Search the current directory and its ancestors for a .loki task file.
|
|
13
14
|
# Returns the path string, or nil if not found.
|
|
14
15
|
def self.find_task_file
|
|
15
|
-
|
|
16
|
-
loop do
|
|
17
|
-
candidate = File.join(dir, ".loki")
|
|
18
|
-
return candidate if File.exist?(candidate)
|
|
19
|
-
parent = File.dirname(dir)
|
|
20
|
-
break if parent == dir
|
|
21
|
-
dir = parent
|
|
22
|
-
end
|
|
23
|
-
nil
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
# Load all *.loki files from dir in alphabetical order.
|
|
27
|
-
# Each file typically reopens class Tasks to add tasks.
|
|
28
|
-
# The .loki entry point is excluded — it is loaded separately by run!.
|
|
29
|
-
def self.load_loki(dir)
|
|
30
|
-
Dir.glob(File.join(dir, "*.loki")).sort.each { |f| load f }
|
|
16
|
+
loki_up
|
|
31
17
|
end
|
|
32
18
|
|
|
33
19
|
# Main entry point invoked by the asgard executable.
|
|
34
20
|
def self.run!(argv)
|
|
35
|
-
auto_load = argv.delete("--auto-load")
|
|
36
21
|
abort "asgard: unknown command '#{argv.first}'" if argv.first&.start_with?("_")
|
|
37
22
|
task_file = find_task_file or abort "asgard: no .loki file found in #{Dir.pwd}"
|
|
38
23
|
before = Asgard::Base.subclasses.dup
|
|
39
|
-
load_loki(File.dirname(task_file)) if auto_load
|
|
40
24
|
load task_file
|
|
41
25
|
newly_defined = Asgard::Base.subclasses - before
|
|
42
26
|
(newly_defined + [Tasks]).uniq.each(&:validate_deps!)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: asgard
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dewayne VanHoozer
|
|
@@ -51,9 +51,10 @@ dependencies:
|
|
|
51
51
|
- - "~>"
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
53
|
version: '3.0'
|
|
54
|
-
description:
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
description: |
|
|
55
|
+
A powerful Ruby-based task runner for any kind of project with task dependency tracking
|
|
56
|
+
and concurrent execution of designated tasks. Uses Thor for its rich CLI options, var
|
|
57
|
+
declarations, dotenv, sh/shebang helpers, and importable task files.
|
|
57
58
|
email:
|
|
58
59
|
- dewayne@vanhoozer.me
|
|
59
60
|
executables:
|
|
@@ -64,6 +65,7 @@ files:
|
|
|
64
65
|
- ".envrc"
|
|
65
66
|
- ".github/workflows/deploy-github-pages.yml"
|
|
66
67
|
- ".loki"
|
|
68
|
+
- ".rubocop.yml"
|
|
67
69
|
- CHANGELOG.md
|
|
68
70
|
- CLAUDE.md
|
|
69
71
|
- COMMITS.md
|
|
@@ -89,13 +91,19 @@ files:
|
|
|
89
91
|
- docs/task-files.md
|
|
90
92
|
- docs/tasks.md
|
|
91
93
|
- docs/variables.md
|
|
94
|
+
- examples/.env
|
|
92
95
|
- examples/.loki
|
|
93
96
|
- examples/concurrent.loki
|
|
94
97
|
- examples/db_subcommands.loki
|
|
98
|
+
- examples/env_usage.loki
|
|
95
99
|
- examples/kitchen_sink.loki
|
|
96
100
|
- examples/server_subcommands.loki
|
|
101
|
+
- examples/subdir/.loki
|
|
102
|
+
- examples/subdir/import_demo.loki
|
|
103
|
+
- examples/subdir/import_up_demo.loki
|
|
97
104
|
- lib/asgard.rb
|
|
98
105
|
- lib/asgard/base.rb
|
|
106
|
+
- lib/asgard/kernel_methods.rb
|
|
99
107
|
- lib/asgard/shell.rb
|
|
100
108
|
- lib/asgard/tasks.rb
|
|
101
109
|
- lib/asgard/version.rb
|
|
@@ -108,6 +116,7 @@ metadata:
|
|
|
108
116
|
homepage_uri: https://github.com/madbomber/asgard
|
|
109
117
|
source_code_uri: https://github.com/madbomber/asgard
|
|
110
118
|
changelog_uri: https://github.com/madbomber/asgard/blob/master/CHANGELOG.md
|
|
119
|
+
rubygems_mfa_required: 'true'
|
|
111
120
|
rdoc_options: []
|
|
112
121
|
require_paths:
|
|
113
122
|
- lib
|