asgard 0.2.0 → 0.3.1

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,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+
5
+ module Kernel
6
+ def debug? = $DEBUG
7
+ def verbose? = $VERBOSE
8
+ module_function :debug?, :verbose?
9
+
10
+ # Fetch an environment variable by symbol or string name.
11
+ # The name is converted to an uppercase string automatically.
12
+ # Raises KeyError when the variable is missing and no default is given.
13
+ def env(name, default = nil)
14
+ key = name.to_s.upcase
15
+ default.nil? ? ENV.fetch(key) : ENV.fetch(key, default)
16
+ end
17
+ module_function :env
18
+
19
+ def loki_up(name = ".loki")
20
+ dir = Pathname.new(Dir.pwd)
21
+ loop do
22
+ candidate = dir + name
23
+ return candidate if candidate.exist?
24
+ parent = dir.parent
25
+ break if parent == dir
26
+ dir = parent
27
+ end
28
+ nil
29
+ end
30
+ module_function :loki_up
31
+
32
+ def import(path)
33
+ path = path.to_s
34
+ raise ArgumentError, "import: path must end with .loki (got #{path.inspect})" unless path.end_with?(".loki")
35
+ unless File.absolute_path?(path)
36
+ caller_dir = File.dirname(caller_locations(1, 1).first.absolute_path)
37
+ path = File.expand_path(path, caller_dir)
38
+ end
39
+ paths = path =~ /[*?\[{]/ ? Dir.glob(path) : [path]
40
+ loaded = paths.map do |p|
41
+ if $LOADED_FEATURES.include?(p)
42
+ warn "import: skip #{p} (already loaded)" if debug?
43
+ next false
44
+ end
45
+ warn "import: #{p}" if verbose? || debug?
46
+ load p
47
+ $LOADED_FEATURES << p
48
+ true
49
+ end
50
+ loaded.any?
51
+ end
52
+ module_function :import
53
+
54
+ def import_up(name = ".loki")
55
+ if name =~ /[*?\[{]/
56
+ dir = Dir.pwd
57
+ loop do
58
+ matches = Dir.glob(File.join(dir, name))
59
+ unless matches.empty?
60
+ warn "import_up: #{name} → #{dir}" if verbose? || debug?
61
+ return matches.map { |p| import(p) }.any?
62
+ end
63
+ parent = File.dirname(dir)
64
+ break if parent == dir
65
+ dir = parent
66
+ end
67
+ warn "import_up: #{name} not found" if debug?
68
+ return false
69
+ end
70
+ path = loki_up(name)
71
+ unless path
72
+ warn "import_up: #{name} not found" if debug?
73
+ return false
74
+ end
75
+ warn "import_up: #{name} → #{path}" if verbose? || debug?
76
+ import path
77
+ end
78
+ module_function :import_up
79
+ 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
- system("bash", "-c", script)
16
- else
17
- system(script)
18
- end
16
+ system("bash", "-c", script)
17
+ else
18
+ system(script)
19
+ end
19
20
 
20
- exit($?.exitstatus) unless success
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
- Tempfile.create(["asgard_", ext]) do |f|
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($?.exitstatus) unless $?.success?
42
+ exit($CHILD_STATUS.exitstatus) unless $CHILD_STATUS.success?
42
43
  end
43
44
  end
44
45
  end
data/lib/asgard/tasks.rb CHANGED
@@ -4,6 +4,13 @@
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
6
  class Tasks < Asgard::Base
7
+ header "\nasgard v#{Asgard::VERSION} The Mighty Thor and Loki working for you"
8
+
9
+ footer <<~FOOT
10
+ \nDocumentation ... https://madbomber.github.io/asgard
11
+ Github Repo ..... https://github.com/MadBomber/asgard\n
12
+ FOOT
13
+
7
14
  class_option :debug,
8
15
  type: :boolean,
9
16
  default: false,
@@ -14,21 +21,9 @@ class Tasks < Asgard::Base
14
21
  default: false,
15
22
  desc: "Enable verbose output ($VERBOSE = true)"
16
23
 
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
24
+ class_option :version,
25
+ type: :boolean,
26
+ default: false,
27
+ desc: "Show asgard version and exit"
28
+ no_negate :version
34
29
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Asgard
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.1"
5
5
  end
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,18 @@ 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
- dir = Dir.pwd
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?("_")
22
+ if argv.include?("--version")
23
+ puts Asgard::VERSION
24
+ exit
25
+ end
37
26
  task_file = find_task_file or abort "asgard: no .loki file found in #{Dir.pwd}"
38
27
  before = Asgard::Base.subclasses.dup
39
- load_loki(File.dirname(task_file)) if auto_load
40
28
  load task_file
41
29
  newly_defined = Asgard::Base.subclasses - before
42
30
  (newly_defined + [Tasks]).uniq.each(&:validate_deps!)
data/quality.loki ADDED
@@ -0,0 +1,75 @@
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
7
+ output = `bundle exec ruby -Ilib:test test/test_asgard.rb 2>&1`
8
+ @test_result = $?.success? ? :pass : :fail
9
+ if @test_result == :fail
10
+ lines = output.lines
11
+ start = lines.index { |l| l =~ /^\s+1\) / } || 0
12
+ finish = lines.rindex { |l| l =~ /\d+ runs,/ } || -1
13
+ print lines[start..finish].join
14
+ end
15
+ end
16
+
17
+ desc "Run all quality gates (tests, RuboCop, Flog) in parallel"
18
+ depends_on [:test, :rubocop, :flog_check]
19
+ def quality
20
+ results = { tests: @test_result, rubocop: @rubocop_result, flog: @flog_result }
21
+
22
+ if @flog_failures&.any?
23
+ puts "\nFlog failures (must be refactored):"
24
+ @flog_failures.each { |v| puts " #{v}" }
25
+ end
26
+
27
+ puts "\n#{"=" * 60}"
28
+ puts "Quality Summary"
29
+ puts "=" * 60
30
+ results.each { |gate, status| puts " [#{status == :pass ? "PASS" : "FAIL"}] #{gate}" }
31
+ puts "=" * 60
32
+
33
+ abort "\nQuality gate failed" if results.values.any?(:fail)
34
+ puts "\nAll quality gates passed."
35
+ end
36
+
37
+ desc "Check code style with RuboCop"
38
+ def rubocop
39
+ output = `RUBOCOP_CACHE_ROOT=tmp/rubocop_cache bundle exec rubocop 2>&1`
40
+ @rubocop_result = $?.success? ? :pass : :fail
41
+ if @rubocop_result == :fail
42
+ output.each_line { |l| print l if l =~ /:\d+:\d+: [CWEF]: / }
43
+ end
44
+ end
45
+
46
+ desc "Auto-correct RuboCop offenses"
47
+ def rubocop_fix
48
+ sh "RUBOCOP_CACHE_ROOT=tmp/rubocop_cache bundle exec rubocop -a"
49
+ end
50
+
51
+ desc "Check code complexity with Flog (warn >=20, fail >=50)"
52
+ def flog_check
53
+ require "flog"
54
+
55
+ method_warn = 20.0
56
+ method_fail = 50.0
57
+
58
+ flogger = Flog.new(all: true)
59
+ flogger.flog(*Dir.glob("lib/**/*.rb"))
60
+
61
+ warnings = []
62
+ @flog_failures = []
63
+
64
+ flogger.each_by_score do |method_name, score|
65
+ next if method_name.end_with?("#none")
66
+ if score > method_fail
67
+ @flog_failures << "#{"%.1f" % score}: #{method_name}"
68
+ elsif score > method_warn
69
+ warnings << "#{"%.1f" % score}: #{method_name}"
70
+ end
71
+ end
72
+
73
+ @flog_result = @flog_failures.empty? ? :pass : :fail
74
+ end
75
+ 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.2.0
4
+ version: 0.3.1
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: A powerful Ruby-based task runner for any kind of project with task dependency
55
- tracking and concurrent execution of designated tasks. Uses Thor for its rich CLI
56
- options, var declarations, dotenv, sh/shebang helpers, and importable task files.
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,12 +65,12 @@ 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
70
72
  - LICENSE.txt
71
73
  - README.md
72
- - Rakefile
73
74
  - bin/asgard
74
75
  - bin/console
75
76
  - bin/setup
@@ -89,17 +90,25 @@ files:
89
90
  - docs/task-files.md
90
91
  - docs/tasks.md
91
92
  - docs/variables.md
93
+ - examples/.env
92
94
  - examples/.loki
93
95
  - examples/concurrent.loki
94
96
  - examples/db_subcommands.loki
97
+ - examples/env_usage.loki
95
98
  - examples/kitchen_sink.loki
96
99
  - examples/server_subcommands.loki
100
+ - examples/subdir/.loki
101
+ - examples/subdir/import_demo.loki
102
+ - examples/subdir/import_up_demo.loki
103
+ - gem_tasks.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
102
110
  - mkdocs.yml
111
+ - quality.loki
103
112
  - sig/asgard.rbs
104
113
  homepage: https://github.com/madbomber/asgard
105
114
  licenses:
@@ -108,6 +117,7 @@ metadata:
108
117
  homepage_uri: https://github.com/madbomber/asgard
109
118
  source_code_uri: https://github.com/madbomber/asgard
110
119
  changelog_uri: https://github.com/madbomber/asgard/blob/master/CHANGELOG.md
120
+ rubygems_mfa_required: 'true'
111
121
  rdoc_options: []
112
122
  require_paths:
113
123
  - lib
data/Rakefile DELETED
@@ -1,22 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "bundler/gem_tasks"
4
- require "minitest/test_task"
5
-
6
- SIMPLECOV_PRELUDE = <<~RUBY.freeze
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 quality: :test do
19
- sh "flog lib/"
20
- end
21
-
22
- task default: :test