asgard 0.1.0 → 0.1.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/examples/.loki ADDED
@@ -0,0 +1,2 @@
1
+ # The .loki file can be empty if there are *.loki files define the tasks
2
+ # they will be auto loaded.
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+ # Demonstrates Thor subcommands with Asgard's depends_on chaining within
3
+ # the subcommand group. Inherits from Tasks for full Asgard DSL access.
4
+ #
5
+ # Usage:
6
+ # asgard db # shows subcommand help
7
+ # asgard db migrate
8
+ # asgard db migrate 20240101120000 --dry-run
9
+ # asgard db rollback
10
+ # asgard db rollback 3
11
+ # asgard db seed --file db/seeds/staging.rb
12
+ # asgard db reset # runs: rollback → migrate → seed
13
+ # asgard db console --env staging
14
+ # asgard db status
15
+
16
+ class DBCommands < Tasks
17
+ desc "migrate [VERSION]", "Run pending migrations up to VERSION"
18
+ option :dry_run, aliases: "-n", type: :boolean, default: false, desc: "Print SQL without executing"
19
+ option :verbose, aliases: "-v", type: :boolean, default: false, desc: "Show each migration as it runs"
20
+ def migrate(version = nil)
21
+ target = version ? "to version #{version}" : "to latest"
22
+ puts "Migrating #{target}#{options[:dry_run] ? " (dry run)" : ""}..."
23
+ end
24
+
25
+ desc "rollback [STEPS]", "Roll back the last STEPS migrations (default: 1)"
26
+ option :dry_run, aliases: "-n", type: :boolean, default: false, desc: "Print SQL without executing"
27
+ def rollback(steps = "1")
28
+ puts "Rolling back #{steps} migration(s)#{options[:dry_run] ? " (dry run)" : ""}..."
29
+ end
30
+
31
+ desc "seed [FILE]", "Load seed data into the database"
32
+ option :env, type: :string, default: "development",
33
+ enum: %w[development staging production],
34
+ desc: "Environment to seed"
35
+ def seed(file = "db/seeds.rb")
36
+ puts "Seeding #{options[:env]} from #{file}..."
37
+ end
38
+
39
+ # depends_on chains within the subcommand group:
40
+ # rollback → migrate → seed → reset
41
+ depends_on :rollback, :migrate, :seed
42
+ desc "reset", "Rollback all migrations, re-migrate, and reseed"
43
+ def reset
44
+ puts "Database reset complete."
45
+ end
46
+
47
+ long_desc <<~DESC
48
+ Opens an interactive SQL console connected to the configured database.
49
+
50
+ The console inherits credentials from the current environment's
51
+ database.yml (Rails) or DATABASE_URL.
52
+
53
+ Examples:\x5
54
+ asgard db console\x5
55
+ asgard db console --env staging
56
+ DESC
57
+ desc "console", "Open an interactive database console"
58
+ option :env, type: :string, default: "development",
59
+ enum: %w[development staging production],
60
+ desc: "Environment to connect to"
61
+ def console
62
+ puts "Opening #{options[:env]} database console..."
63
+ end
64
+
65
+ desc "status", "Show applied and pending migrations"
66
+ def status
67
+ puts "Checking migration status..."
68
+ end
69
+ end
70
+
71
+ class Tasks
72
+ desc "db SUBCOMMAND", "Manage the database"
73
+ subcommand "db", DBCommands
74
+ end
@@ -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
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "thor"
4
4
  require "set"
5
- require "simple_flow"
5
+ require "dagwood"
6
6
 
7
7
  module Asgard
8
8
  class Base < Thor
@@ -57,15 +57,15 @@ module Asgard
57
57
  graph
58
58
  end
59
59
 
60
- # Declare dependencies for the next recipe.
60
+ # Declare dependencies for the next task.
61
61
  # Bare symbols run sequentially; arrays within the splat run in parallel.
62
62
  #
63
63
  # depends_on :build # sequential
64
64
  # depends_on :build, :lint # both sequential
65
65
  # depends_on [:build, :lint] # build and lint in parallel
66
66
  # depends_on :setup, [:build, :lint], :test # setup, then build+lint, then test
67
- def depends_on(*recipes)
68
- @_pending_deps = recipes
67
+ def depends_on(*tasks)
68
+ @_pending_deps = tasks
69
69
  end
70
70
 
71
71
  def var(name, value = nil, &block)
@@ -88,7 +88,7 @@ module Asgard
88
88
  Dotenv.load(path) if File.exist?(path)
89
89
  end
90
90
 
91
- # Validate the full dep graph for cycles using SimpleFlow::DependencyGraph.
91
+ # Validate the full dep graph for cycles using Dagwood::DependencyGraph.
92
92
  def validate_deps!
93
93
  return if _deps.empty?
94
94
 
@@ -97,7 +97,7 @@ module Asgard
97
97
  hash[task] = _deps.fetch(task, []).flatten
98
98
  end
99
99
 
100
- SimpleFlow::DependencyGraph.new(full_graph).order
100
+ Dagwood::DependencyGraph.new(full_graph).order
101
101
  rescue TSort::Cyclic => e
102
102
  raise Asgard::CircularDependencyError, e.message
103
103
  end
@@ -118,8 +118,10 @@ module Asgard
118
118
  no_commands do
119
119
  # Dispatch hook: resolves and runs all deps (in parallel where declared)
120
120
  # before executing the target command. Thread-safe deduplication via
121
- # the class-level _ran_tasks set ensures each recipe runs at most once.
121
+ # the class-level _ran_tasks set ensures each task runs at most once.
122
122
  def invoke_command(command, *args)
123
+ $DEBUG = true if options[:debug]
124
+ $VERBOSE = true if options[:verbose]
123
125
  target = command.name.to_sym
124
126
 
125
127
  should_run = self.class._ran_mutex.synchronize do
@@ -132,7 +134,7 @@ module Asgard
132
134
  stages = self.class._deps[target]
133
135
  if stages&.any?
134
136
  graph = self.class._build_dep_graph(stages)
135
- groups = SimpleFlow::DependencyGraph.new(graph).parallel_order
137
+ groups = Dagwood::DependencyGraph.new(graph).parallel_order
136
138
 
137
139
  groups.each do |group|
138
140
  if group.size > 1
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Tasks is the single conventional entry point for all .loki files.
4
+ # It is pre-defined by the gem so .loki files never need to declare a class.
5
+ # Auxiliary *.loki files define modules which are imported into Tasks.
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 "--version", "Show asgard version"
18
+ map "--version" => :_version
19
+ def _version
20
+ puts Asgard::VERSION
21
+ exit
22
+ end
23
+
24
+ private
25
+
26
+ def debug? = $DEBUG
27
+ def verbose? = $VERBOSE
28
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Asgard
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.2"
5
5
  end
data/lib/asgard.rb CHANGED
@@ -3,30 +3,43 @@
3
3
  require_relative "asgard/version"
4
4
  require_relative "asgard/shell"
5
5
  require_relative "asgard/base"
6
+ require_relative "asgard/tasks"
6
7
 
7
8
  module Asgard
8
9
  class Error < StandardError; end
9
10
  class CircularDependencyError < Error; end
10
11
 
11
- # Search the current directory and its ancestors for task files.
12
- # Returns an array of paths, or nil if nothing is found.
13
- #
14
- # Priority:
15
- # 1. A single .loki file in the directory (default/hidden task file)
16
- # 2. All *.loki files in the directory, sorted alphabetically
17
- def self.find_task_files
12
+ # Search the current directory and its ancestors for a .loki task file.
13
+ # Returns the path string, or nil if not found.
14
+ def self.find_task_file
18
15
  dir = Dir.pwd
19
16
  loop do
20
- dot_loki = File.join(dir, ".loki")
21
- return [dot_loki] if File.exist?(dot_loki)
22
-
23
- matches = Dir.glob(File.join(dir, "*.loki")).sort
24
- return matches unless matches.empty?
25
-
17
+ candidate = File.join(dir, ".loki")
18
+ return candidate if File.exist?(candidate)
26
19
  parent = File.dirname(dir)
27
20
  break if parent == dir
28
21
  dir = parent
29
22
  end
30
23
  nil
31
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 }
31
+ end
32
+
33
+ # Main entry point invoked by the asgard executable.
34
+ def self.run!(argv)
35
+ abort "asgard: unknown command '#{argv.first}'" if argv.first&.start_with?("_")
36
+ task_file = find_task_file or abort "asgard: no .loki file found in #{Dir.pwd}"
37
+ load_loki(File.dirname(task_file))
38
+ load task_file
39
+ Tasks.validate_deps!
40
+ Tasks._reset_ran!
41
+ Tasks.start(argv)
42
+ rescue CircularDependencyError => e
43
+ abort "asgard: circular dependency — #{e.message}"
44
+ end
32
45
  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.1.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dewayne VanHoozer
@@ -9,20 +9,6 @@ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
- - !ruby/object:Gem::Dependency
13
- name: logger
14
- requirement: !ruby/object:Gem::Requirement
15
- requirements:
16
- - - ">="
17
- - !ruby/object:Gem::Version
18
- version: '0'
19
- type: :runtime
20
- prerelease: false
21
- version_requirements: !ruby/object:Gem::Requirement
22
- requirements:
23
- - - ">="
24
- - !ruby/object:Gem::Version
25
- version: '0'
26
12
  - !ruby/object:Gem::Dependency
27
13
  name: thor
28
14
  requirement: !ruby/object:Gem::Requirement
@@ -38,19 +24,19 @@ dependencies:
38
24
  - !ruby/object:Gem::Version
39
25
  version: '1.0'
40
26
  - !ruby/object:Gem::Dependency
41
- name: simple_flow
27
+ name: dagwood
42
28
  requirement: !ruby/object:Gem::Requirement
43
29
  requirements:
44
30
  - - "~>"
45
31
  - !ruby/object:Gem::Version
46
- version: '0.4'
32
+ version: '1.0'
47
33
  type: :runtime
48
34
  prerelease: false
49
35
  version_requirements: !ruby/object:Gem::Requirement
50
36
  requirements:
51
37
  - - "~>"
52
38
  - !ruby/object:Gem::Version
53
- version: '0.4'
39
+ version: '1.0'
54
40
  - !ruby/object:Gem::Dependency
55
41
  name: dotenv
56
42
  requirement: !ruby/object:Gem::Requirement
@@ -65,9 +51,9 @@ dependencies:
65
51
  - - "~>"
66
52
  - !ruby/object:Gem::Version
67
53
  version: '3.0'
68
- description: 'Asgard brings just-style recipes to Ruby: Thor-powered CLI, dep ordering
69
- via SimpleFlow::DependencyGraph, var declarations, dotenv, sh/shebang helpers, and
70
- importable task modules.'
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.
71
57
  email:
72
58
  - dewayne@vanhoozer.me
73
59
  executables:
@@ -78,6 +64,7 @@ files:
78
64
  - ".envrc"
79
65
  - ".loki"
80
66
  - CHANGELOG.md
67
+ - CLAUDE.md
81
68
  - COMMITS.md
82
69
  - LICENSE.txt
83
70
  - README.md
@@ -85,9 +72,14 @@ files:
85
72
  - bin/asgard
86
73
  - bin/console
87
74
  - bin/setup
75
+ - examples/.loki
76
+ - examples/db_subcommands.loki
77
+ - examples/kitchen_sink.loki
78
+ - examples/server_subcommands.loki
88
79
  - lib/asgard.rb
89
80
  - lib/asgard/base.rb
90
81
  - lib/asgard/shell.rb
82
+ - lib/asgard/tasks.rb
91
83
  - lib/asgard/version.rb
92
84
  - sig/asgard.rbs
93
85
  homepage: https://github.com/madbomber/asgard
@@ -113,5 +105,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
113
105
  requirements: []
114
106
  rubygems_version: 4.0.12
115
107
  specification_version: 4
116
- summary: A just-like task runner built on Thor, with recipe dependencies via SimpleFlow.
108
+ summary: A powerful Ruby-based task runner
117
109
  test_files: []