asgard 0.3.0 → 0.3.2

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/lib/asgard.rb CHANGED
@@ -5,6 +5,7 @@ require_relative "asgard/kernel_methods"
5
5
  require_relative "asgard/shell"
6
6
  require_relative "asgard/base"
7
7
  require_relative "asgard/tasks"
8
+ require_relative "asgard/doctor"
8
9
 
9
10
  module Asgard
10
11
  class Error < StandardError; end
@@ -18,7 +19,16 @@ module Asgard
18
19
 
19
20
  # Main entry point invoked by the asgard executable.
20
21
  def self.run!(argv)
21
- abort "asgard: unknown command '#{argv.first}'" if argv.first&.start_with?("_")
22
+ first = argv.first
23
+ abort "asgard: unknown command '#{first}'" if first&.start_with?("_")
24
+ if argv.include?("--version")
25
+ puts Asgard::VERSION
26
+ exit
27
+ end
28
+ if argv.include?("--doctor")
29
+ Asgard::Doctor.new.run
30
+ exit
31
+ end
22
32
  task_file = find_task_file or abort "asgard: no .loki file found in #{Dir.pwd}"
23
33
  before = Asgard::Base.subclasses.dup
24
34
  load task_file
data/quality.loki ADDED
@@ -0,0 +1,261 @@
1
+ # frozen_string_literal: true
2
+ # Quality gate tasks — imported by .loki
3
+
4
+ class Tasks
5
+ desc "Run the test suite"
6
+ def test_check
7
+ output = `bundle exec ruby -Ilib:test test/test_asgard.rb 2>&1`
8
+ result = $?.success? ? :pass : :fail
9
+ File.write("test_output.txt", output)
10
+ summary = output.lines.reverse.find { |l| l =~ /\d+ runs,/ }&.strip || result.to_s
11
+ puts "Tests: #{summary} (see test_output.txt)"
12
+ result
13
+ end
14
+
15
+ desc "Run the test suite with verbose output"
16
+ def test_verbose
17
+ sh "bundle exec ruby -Ilib:test test/test_asgard.rb -v"
18
+ end
19
+
20
+ desc "Run every *_check quality gate task in parallel"
21
+ # A Proc defers resolution to validate_deps!, after every .loki file has
22
+ # loaded — so a *_check task from another file (e.g. quality_rails.loki's
23
+ # brakeman_check) is picked up too, with no need to redeclare anything here.
24
+ depends_on -> { [all_commands.keys.grep(/_check\z/).sort.map(&:to_sym)] }
25
+ def quality
26
+ check_tasks = self.class._deps.fetch(:quality, []).flatten.map(&:to_s)
27
+
28
+ results = check_tasks.to_h { |name| [quality_gate_label(name), dep_result(name)] }
29
+
30
+ print_quality_summary(results)
31
+ end
32
+
33
+ desc "Check code style with RuboCop"
34
+ def rubocop_check
35
+ output = `RUBOCOP_CACHE_ROOT=tmp/rubocop_cache bundle exec rubocop 2>&1`
36
+ result = $?.success? ? :pass : :fail
37
+ File.write("rubocop_output.txt", output)
38
+ summary = output.lines.find { |l| l =~ /files inspected/ }&.strip || result.to_s
39
+ puts "RuboCop: #{summary} (see rubocop_output.txt)"
40
+ result
41
+ end
42
+
43
+ desc "Auto-correct RuboCop offenses"
44
+ def rubocop_fix
45
+ sh "RUBOCOP_CACHE_ROOT=tmp/rubocop_cache bundle exec rubocop -a"
46
+ end
47
+
48
+ desc "Check code complexity with Flog (warn >=20, fail >=50)"
49
+ def flog_check
50
+ require "flog"
51
+
52
+ method_warn = 20.0
53
+ method_fail = 50.0
54
+
55
+ flogger = Flog.new(all: true)
56
+ flogger.flog(*Dir.glob("lib/**/*.rb"))
57
+
58
+ warnings = []
59
+ failures = []
60
+
61
+ flogger.each_by_score do |method_name, score|
62
+ next if method_name.end_with?("#none")
63
+ if score > method_fail
64
+ failures << "#{"%.1f" % score}: #{method_name}"
65
+ elsif score > method_warn
66
+ warnings << "#{"%.1f" % score}: #{method_name}"
67
+ end
68
+ end
69
+
70
+ result = failures.empty? ? :pass : :fail
71
+
72
+ lines = ["Flog warnings (#{method_warn}–#{method_fail}):"]
73
+ lines.concat(warnings.empty? ? [" (none)"] : warnings.map { |w| " #{w}" })
74
+ lines << ""
75
+ lines << "Flog failures (>= #{method_fail}):"
76
+ lines.concat(failures.empty? ? [" (none)"] : failures.map { |f| " #{f}" })
77
+ File.write("flog_output.txt", "#{lines.join("\n")}\n")
78
+
79
+ puts "Flog: #{failures.size} failure(s), #{warnings.size} warning(s) (see flog_output.txt)"
80
+ result
81
+ end
82
+
83
+ desc "Check for structural code duplication with Flay (mass >= 150)"
84
+ def flay_check
85
+ require "flay"
86
+
87
+ mass_threshold = 150
88
+
89
+ flay = Flay.new(mass: mass_threshold, diff: false, verbose: false, summary: false, timeout: 60)
90
+ flay.process(*Dir.glob("lib/**/*.rb"))
91
+ flay.analyze
92
+
93
+ result = flay.hashes.empty? ? :pass : :fail
94
+
95
+ File.open("flay_output.txt", "w") do |f|
96
+ if result == :pass
97
+ f.puts "Flay: no structural duplication detected (mass >= #{mass_threshold})"
98
+ else
99
+ flay.report(f)
100
+ end
101
+ end
102
+ puts "Flay: #{flay.hashes.size} duplication pattern(s) found (mass >= #{mass_threshold}) (see flay_output.txt)"
103
+ result
104
+ end
105
+
106
+ desc "Check code smells with Reek"
107
+ def reek_check
108
+ output = `bundle exec reek lib 2>&1`
109
+ result = $?.success? ? :pass : :fail
110
+ File.write("reek_output.txt", output)
111
+ summary = output.lines.reverse.find { |l| l =~ /total warnings?/ }&.strip || result.to_s
112
+ puts "Reek: #{summary} (see reek_output.txt)"
113
+ result
114
+ end
115
+
116
+ desc "Check spelling with typos"
117
+ def typos_check
118
+ return skip_typos unless command_available?("typos")
119
+
120
+ output = `typos 2>&1`
121
+ result = $?.success? ? :pass : :fail
122
+ File.write("typos_output.txt", output)
123
+ count = output.lines.count { |l| l.start_with?("error:") }
124
+ summary = result == :pass ? "no typos found" : "#{count} typo(s) found"
125
+ puts "Typos: #{summary} (see typos_output.txt)"
126
+ result
127
+ end
128
+
129
+ desc "Auto-correct typos"
130
+ def typos_fix
131
+ return skip_typos unless command_available?("typos")
132
+
133
+ sh "typos -w"
134
+ end
135
+
136
+ desc "Check ERB templates for style and safety issues with ERBLint"
137
+ def erb_lint_check
138
+ return skip_erb_lint if Dir.glob("**/*.erb").empty?
139
+
140
+ output = `bundle exec erb_lint --lint-all --enable-all-linters 2>&1`
141
+ result = $?.success? ? :pass : :fail
142
+ File.write("erb_lint_output.txt", output)
143
+ summary = output.lines.reverse.find { |l| l =~ /error\(s\)|no errors were found/i }&.strip || result.to_s
144
+ puts "ERBLint: #{summary} (see erb_lint_output.txt)"
145
+ result
146
+ end
147
+
148
+ desc "Check for performance suggestions with Fasterer"
149
+ def fasterer_check
150
+ output = `bundle exec fasterer lib 2>&1`.gsub(/\e\[\d+m/, "")
151
+ result = $?.success? ? :pass : :warn
152
+ File.write("fasterer_output.txt", output)
153
+ summary = output.lines.find { |l| l =~ /files? inspected/ }&.strip || result.to_s
154
+ puts "Fasterer: #{summary} (see fasterer_output.txt)"
155
+ result
156
+ end
157
+
158
+ desc "Check Gemfile.lock for known vulnerabilities with bundler-audit"
159
+ def bundler_audit_check
160
+ output = `bundle exec bundle-audit check --update 2>&1`
161
+ result = $?.success? ? :pass : :fail
162
+ File.write("bundler_audit_output.txt", output)
163
+ summary = output.lines.reverse.find { |l| l =~ /vulnerabilit/i }&.strip || result.to_s
164
+ puts "Bundler Audit: #{summary} (see bundler_audit_output.txt)"
165
+ result
166
+ end
167
+
168
+ desc "Check architecture boundaries with ArchSpec"
169
+ def archspec_check
170
+ return skip_archspec unless command_available?("archspec") && File.exist?("Archspec.rb")
171
+
172
+ output = `archspec check 2>&1`
173
+ result = $?.success? ? :pass : :fail
174
+ File.write("archspec_output.txt", output)
175
+ summary = output.lines.reverse.find { |l| l =~ /architecture violations? found|ArchSpec passed/i }&.strip ||
176
+ result.to_s
177
+ puts "ArchSpec: #{summary} (see archspec_output.txt)"
178
+ result
179
+ end
180
+
181
+ # Curated display labels for the *_check tasks defined in this file. A
182
+ # *_check task from elsewhere (e.g. quality_rails.loki's brakeman_check,
183
+ # loaded automatically once Rails is defined) isn't listed here — it just
184
+ # falls back to a titleized version of its own name in quality_gate_label.
185
+ QUALITY_GATE_LABELS = {
186
+ "test_check" => "Tests + Coverage",
187
+ "rubocop_check" => "RuboCop",
188
+ "flog_check" => "Flog Complexity",
189
+ "flay_check" => "Flay Duplication",
190
+ "reek_check" => "Reek Smells",
191
+ "erb_lint_check" => "ERBLint",
192
+ "archspec_check" => "ArchSpec",
193
+ }.freeze
194
+
195
+ no_commands do
196
+ # true if +name+ resolves to an executable file somewhere on PATH.
197
+ def command_available?(name)
198
+ ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).any? do |dir|
199
+ File.executable?(File.join(dir, name))
200
+ end
201
+ end
202
+
203
+ def quality_gate_label(check_name)
204
+ QUALITY_GATE_LABELS.fetch(check_name) do
205
+ check_name.delete_suffix("_check").split("_").map(&:capitalize).join(" ")
206
+ end
207
+ end
208
+
209
+ def skip_typos
210
+ puts "Typos: SKIPPED — `typos` command not found. Install it with `brew install typos-cli`."
211
+ :skip
212
+ end
213
+
214
+ def skip_erb_lint
215
+ puts "ERBLint: SKIPPED — no .erb files found."
216
+ :skip
217
+ end
218
+
219
+ def skip_archspec
220
+ reason = command_available?("archspec") ? "no Archspec.rb found" : "`archspec` command not found"
221
+ puts "ArchSpec: SKIPPED — #{reason}. Install it with `gem install archspec` " \
222
+ "and run `archspec init` to generate one."
223
+ :skip
224
+ end
225
+
226
+ # Status badges. Only :fail is blocking (aborts the quality task);
227
+ # :skip (a required tool isn't installed) and :warn (the check ran but
228
+ # has non-blocking suggestions, e.g. a performance-hint gate) are not.
229
+ STATUS_BADGES = {
230
+ pass: ["PASS", 32], # green
231
+ fail: ["FAIL", 31], # red
232
+ warn: ["WARN", 33], # yellow
233
+ skip: ["SKIP", 36], # cyan
234
+ }.freeze
235
+
236
+ # Colorized status badges plus a pass/fail/warn/skip tally; aborts only
237
+ # if any gate failed — skip and warn are informational, not blocking.
238
+ def print_quality_summary(results)
239
+ colorize_badge = ->(code, text) { "\e[#{code}m#{text}\e[0m" }
240
+ width = results.keys.map(&:length).max
241
+
242
+ puts "\n#{"=" * 60}"
243
+ puts "Quality Gate Summary"
244
+ puts "=" * 60
245
+ results.each do |label, status|
246
+ text, code = STATUS_BADGES.fetch(status)
247
+ puts " [#{colorize_badge.call(code, text)}] #{label.ljust(width)}"
248
+ end
249
+ puts "-" * 60
250
+
251
+ counts = results.values.tally
252
+ failed = counts[:fail] || 0
253
+ tally = STATUS_BADGES.keys.filter_map { |s| "#{counts[s]} #{s}" if counts[s]&.positive? }.join(", ")
254
+ puts " #{colorize_badge.call(failed.zero? ? 32 : 31, tally)}"
255
+ puts "=" * 60
256
+
257
+ abort "\n#{colorize_badge.call(31, "Quality gate failed.")}" unless failed.zero?
258
+ puts "\n#{colorize_badge.call(32, "All quality gates passed.")}"
259
+ end
260
+ end
261
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+ # Rails-specific quality gate tasks — imported by .loki only when the Rails
3
+ # constant is defined.
4
+ #
5
+ # `quality` (in quality.loki) discovers every *_check task at run time by
6
+ # introspecting Tasks.all_commands, rather than a fixed depends_on list — so
7
+ # any *_check task defined here is automatically picked up by `asgard
8
+ # quality` too, with no need to redeclare or extend anything.
9
+ #
10
+ # Note on rubocop-rails: it's a RuboCop plugin, not a standalone CLI, so it
11
+ # has no *_check task of its own here. Add the gem to the app's Gemfile and
12
+ # `require: rubocop-rails` in its .rubocop.yml — the existing rubocop_check
13
+ # (quality.loki) picks up the added Rails cops automatically.
14
+
15
+ class Tasks
16
+ desc "Check for Rails security vulnerabilities with Brakeman"
17
+ def brakeman_check
18
+ output = `bundle exec brakeman -q 2>&1`
19
+ result = $?.success? ? :pass : :fail
20
+ File.write("brakeman_output.txt", output)
21
+ summary = output.lines.reverse.find { |l| l =~ /warnings? found/i }&.strip || result.to_s
22
+ puts "Brakeman: #{summary} (see brakeman_output.txt)"
23
+ result
24
+ end
25
+
26
+ desc "Check for Rails anti-patterns with RailsBestPractices"
27
+ def rails_best_practices_check
28
+ output = `bundle exec rails_best_practices --silent --without-color . 2>&1`
29
+ result = $?.success? ? :pass : :fail
30
+ File.write("rails_best_practices_output.txt", output)
31
+ summary = output.lines.reverse.find { |l| l =~ /warning/i }&.strip || result.to_s
32
+ puts "RailsBestPractices: #{summary} (see rails_best_practices_output.txt)"
33
+ result
34
+ end
35
+
36
+ desc "Check for database schema issues with ActiveRecordDoctor"
37
+ def active_record_doctor_check
38
+ output = `bundle exec rake active_record_doctor 2>&1`
39
+ result = $?.success? ? :pass : :fail
40
+ File.write("active_record_doctor_output.txt", output)
41
+ issue_count = output.lines.count { |l| l.strip != "" }
42
+ summary = result == :pass ? "no issues found" : "#{issue_count} issue line(s) found"
43
+ puts "ActiveRecordDoctor: #{summary} (see active_record_doctor_output.txt)"
44
+ result
45
+ end
46
+ end
data/xyzzy.loki ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+ # example of an overwritten task detected by (--doctor) — imported by .loki
3
+
4
+ class Tasks
5
+ desc "Push the current branch to its remote"
6
+ def xyzzy = puts <<~MAGIC
7
+ You are at a fork in the yellow brick road to AI.
8
+ To the left is python, the favorite programming language of snake charmers.
9
+ To the right is Ruby, the best computer programming language of all time.
10
+ Which way do you go?
11
+ MAGIC
12
+ end
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.3.0
4
+ version: 0.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dewayne VanHoozer
@@ -23,20 +23,6 @@ dependencies:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
25
  version: '1.0'
26
- - !ruby/object:Gem::Dependency
27
- name: dagwood
28
- requirement: !ruby/object:Gem::Requirement
29
- requirements:
30
- - - "~>"
31
- - !ruby/object:Gem::Version
32
- version: '1.0'
33
- type: :runtime
34
- prerelease: false
35
- version_requirements: !ruby/object:Gem::Requirement
36
- requirements:
37
- - - "~>"
38
- - !ruby/object:Gem::Version
39
- version: '1.0'
40
26
  - !ruby/object:Gem::Dependency
41
27
  name: dotenv
42
28
  requirement: !ruby/object:Gem::Requirement
@@ -65,13 +51,14 @@ files:
65
51
  - ".envrc"
66
52
  - ".github/workflows/deploy-github-pages.yml"
67
53
  - ".loki"
54
+ - ".reek.yml"
68
55
  - ".rubocop.yml"
56
+ - Archspec.rb
69
57
  - CHANGELOG.md
70
58
  - CLAUDE.md
71
59
  - COMMITS.md
72
60
  - LICENSE.txt
73
61
  - README.md
74
- - Rakefile
75
62
  - bin/asgard
76
63
  - bin/console
77
64
  - bin/setup
@@ -93,6 +80,7 @@ files:
93
80
  - docs/variables.md
94
81
  - examples/.env
95
82
  - examples/.loki
83
+ - examples/bad.loki
96
84
  - examples/concurrent.loki
97
85
  - examples/db_subcommands.loki
98
86
  - examples/env_usage.loki
@@ -101,14 +89,26 @@ files:
101
89
  - examples/subdir/.loki
102
90
  - examples/subdir/import_demo.loki
103
91
  - examples/subdir/import_up_demo.loki
92
+ - gem_tasks.loki
93
+ - git.loki
104
94
  - lib/asgard.rb
105
95
  - lib/asgard/base.rb
96
+ - lib/asgard/base/dependency_graph.rb
97
+ - lib/asgard/base/dispatch.rb
98
+ - lib/asgard/base/registry.rb
99
+ - lib/asgard/base/task_dsl.rb
100
+ - lib/asgard/doctor.rb
101
+ - lib/asgard/doctor/report.rb
102
+ - lib/asgard/doctor/task_sections.rb
106
103
  - lib/asgard/kernel_methods.rb
107
104
  - lib/asgard/shell.rb
108
105
  - lib/asgard/tasks.rb
109
106
  - lib/asgard/version.rb
110
107
  - mkdocs.yml
108
+ - quality.loki
109
+ - quality_rails.loki
111
110
  - sig/asgard.rbs
111
+ - xyzzy.loki
112
112
  homepage: https://github.com/madbomber/asgard
113
113
  licenses:
114
114
  - MIT
@@ -131,7 +131,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
131
131
  - !ruby/object:Gem::Version
132
132
  version: '0'
133
133
  requirements: []
134
- rubygems_version: 4.0.12
134
+ rubygems_version: 4.0.19
135
135
  specification_version: 4
136
136
  summary: A powerful Ruby-based task runner
137
137
  test_files: []
data/Rakefile DELETED
@@ -1,101 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "bundler/gem_tasks"
4
- require "minitest/test_task"
5
-
6
- SIMPLECOV_PRELUDE = <<~RUBY
7
- require "simplecov"
8
- SimpleCov.start do
9
- add_filter "/test/"
10
- minimum_coverage 95
11
- end
12
- RUBY
13
-
14
- Minitest::TestTask.create do |t|
15
- t.test_prelude = SIMPLECOV_PRELUDE
16
- end
17
-
18
- task default: :test
19
-
20
- RUBOCOP_ENV = { "RUBOCOP_CACHE_ROOT" => "tmp/rubocop_cache" }.freeze
21
-
22
- desc "Check code style with RuboCop"
23
- task :rubocop do
24
- sh RUBOCOP_ENV, "bundle exec rubocop"
25
- end
26
-
27
- desc "Auto-correct RuboCop offenses"
28
- task :rubocop_fix do
29
- sh RUBOCOP_ENV, "bundle exec rubocop -a"
30
- end
31
-
32
- desc "Check code complexity with Flog (warn >=20, fail >=50)"
33
- task :flog_check do
34
- require "flog"
35
-
36
- # Target to work toward; methods above this are warned but don't fail the gate.
37
- METHOD_WARN = 20.0
38
- # Current baseline floor — established from first run. Reduce incrementally.
39
- METHOD_FAIL = 50.0
40
-
41
- flogger = Flog.new(all: true)
42
- flogger.flog(*Dir.glob("lib/**/*.rb"))
43
-
44
- warnings = []
45
- failures = []
46
-
47
- flogger.each_by_score do |method, score|
48
- next if method.end_with?("#none")
49
- if score > METHOD_FAIL
50
- failures << "#{"%.1f" % score}: #{method}"
51
- elsif score > METHOD_WARN
52
- warnings << "#{"%.1f" % score}: #{method}"
53
- end
54
- end
55
-
56
- unless warnings.empty?
57
- puts "\nFlog warnings (#{METHOD_WARN}–#{METHOD_FAIL}) — target for future refactoring:"
58
- warnings.each { |v| puts " #{v}" }
59
- end
60
-
61
- if failures.empty?
62
- puts "\nFlog: no methods exceed the failure threshold (>=#{METHOD_FAIL})"
63
- else
64
- puts "\nFlog failures (>=#{METHOD_FAIL}) — must be refactored:"
65
- failures.each { |v| puts " #{v}" }
66
- $stdout.flush
67
- abort "\nFlog quality gate failed: #{failures.size} method(s) exceed #{METHOD_FAIL}"
68
- end
69
- end
70
-
71
- desc "Run all quality checks: tests (with coverage), RuboCop, and Flog"
72
- task :quality do
73
- results = {}
74
-
75
- puts "\n#{"=" * 60}"
76
- puts "Quality Gate: Tests + Coverage"
77
- puts "=" * 60
78
- results[:tests] = system("bundle exec rake test") ? :pass : :fail
79
-
80
- puts "\n#{"=" * 60}"
81
- puts "Quality Gate: RuboCop"
82
- puts "=" * 60
83
- results[:rubocop] = system(RUBOCOP_ENV, "bundle exec rubocop") ? :pass : :fail
84
-
85
- puts "\n#{"=" * 60}"
86
- puts "Quality Gate: Flog Complexity"
87
- puts "=" * 60
88
- results[:flog] = system("bundle exec rake flog_check") ? :pass : :fail
89
-
90
- puts "\n#{"=" * 60}"
91
- puts "Quality Summary"
92
- puts "=" * 60
93
- results.each do |gate, status|
94
- icon = status == :pass ? "PASS" : "FAIL"
95
- puts " [#{icon}] #{gate}"
96
- end
97
- puts "=" * 60
98
-
99
- abort "\nQuality gate failed" if results.values.any?(:fail)
100
- puts "\nAll quality gates passed."
101
- end