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.
@@ -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(:@_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.
@@ -57,15 +71,15 @@ module Asgard
57
71
  graph
58
72
  end
59
73
 
60
- # Declare dependencies for the next recipe.
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(*recipes)
68
- @_pending_deps = recipes
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
- 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,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. Thread-safe deduplication via
121
- # the class-level _ran_tasks set ensures each recipe 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)
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
- next false if self.class._ran_tasks.include?(target)
127
- self.class._ran_tasks.add(target)
128
- 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
129
185
  end
130
186
  return unless should_run
131
187
 
132
- stages = self.class._deps[target]
133
- if stages&.any?
134
- graph = self.class._build_dep_graph(stages)
135
- groups = Dagwood::DependencyGraph.new(graph).parallel_order
136
-
137
- groups.each do |group|
138
- if group.size > 1
139
- threads = group.map { |task| Thread.new { _run_dep(task) } }
140
- threads.each(&:join)
141
- else
142
- _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
143
203
  end
144
204
  end
145
- end
146
205
 
147
- 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
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
- 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
@@ -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; end
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Asgard
4
- VERSION = "0.1.1"
4
+ VERSION = "0.2.0"
5
5
  end
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 recipes.
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
- task_file = find_task_file or (warn "asgard: no .loki file found in #{Dir.pwd}"; exit 1)
36
- load_loki(File.dirname(task_file))
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
- Tasks.validate_deps!
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
- warn "asgard: circular dependency — #{e.message}"
43
- exit 1
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 &copy; 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