asgard 0.1.1 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7920fc28de5f8644ccc7934f576142d433de3fb5af1ac21b28cbd350d930af66
4
- data.tar.gz: e651f2956eb2f9fc076548fc04cae92e6835fb508b5960eeda952929d9197253
3
+ metadata.gz: 2d904144543326c15257e4e61584c46fa9b0ab4f1ba522680daa930950361e6a
4
+ data.tar.gz: fea08260a5db1fdde268eaf4ca930b1e52a07a39f12eaf2ce20e31736319cdff
5
5
  SHA512:
6
- metadata.gz: 19e473102e5850db42c777b9dd824765b9e4a187f1de6f009d80ae85fa1d6992984587a7ca4e9dc40cb301aadc11093826f913d3c5c7dac8e17a47c47f7cdefe
7
- data.tar.gz: '09c0f72476c8690f055e6aeb2b052449a4eb9fd415bff06a82e4cd5dc4255cfb8709721a76ae347226b31b8dafcbd3e63a439fcd6f249975f134156f578edb1f'
6
+ metadata.gz: 9f75f50ba53970998d047e21078b120090ba6c232ac698b0ba705b84a3de542bfef313690e11d2e09989101045323c7cd6549bf51826b316ccf1beeb96ada50d
7
+ data.tar.gz: 497bee99076f476b8fe680962f59b369898381470b27fd59e0f7e4dc7636e620004660cbcf3ceb10a2678e65ddf78a2a7e24e6571362fc7ad3e7929eeb61a614
data/CHANGELOG.md CHANGED
@@ -39,8 +39,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
39
39
  ### Added
40
40
 
41
41
  - `Asgard::Base` — Thor subclass providing the task DSL
42
- - `depends_on` — declare recipe dependencies; dependencies run at most once per invocation
43
- - `var` — declare static or lazy-evaluated variables available to all recipes
42
+ - `depends_on` — declare task dependencies; dependencies run at most once per invocation
43
+ - `var` — declare static or lazy-evaluated variables available to all tasks
44
44
  - `import` — flat-merge a task module into the current class
45
45
  - `dotenv` — load a `.env` file into the environment
46
46
  - `sh` — run a shell command or multiline heredoc script; exits with the command's status on failure
data/CLAUDE.md ADDED
@@ -0,0 +1,117 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## What Asgard Is
6
+
7
+ Asgard is a Ruby task runner. Users define tasks in `.loki` files by reopening the pre-defined `Tasks` class. The name is intentional: Thor handles the CLI, Asgard is where tasks live, and Loki (the `.loki` file) holds all the tricks.
8
+
9
+ ## Commands
10
+
11
+ ```bash
12
+ bundle install
13
+ bundle exec rake test # run tests (enforces 95% SimpleCov coverage)
14
+ bundle exec rake quality # test + flog complexity check
15
+ bundle exec rake build # build .gem into pkg/
16
+ bundle exec rake install # install locally
17
+
18
+ # or use the gem's own .loki file:
19
+ asgard test
20
+ asgard quality
21
+ asgard release
22
+ ```
23
+
24
+ Single test: `ruby -Ilib:test test/test_asgard.rb`
25
+
26
+ ## Architecture
27
+
28
+ ### Entry Point Flow
29
+
30
+ `bin/asgard` → `Asgard.run!(ARGV)` (`lib/asgard.rb`):
31
+ 1. Walk CWD + ancestors for `.loki` (marker only, not a task file)
32
+ 2. Glob + sort `*.loki` files from that dir, `load` each alphabetically
33
+ 3. Load `.loki` itself last
34
+ 4. `Tasks.validate_deps!` — build full dep graph, raise `CircularDependencyError` if cyclic
35
+ 5. `Tasks._reset_ran!` — clear execution tracking
36
+ 6. `Tasks.start(argv)` — Thor dispatches the command
37
+
38
+ ### Core Classes
39
+
40
+ | File | Role |
41
+ |------|------|
42
+ | `lib/asgard/base.rb` | DSL engine; inherits Thor, includes Shell |
43
+ | `lib/asgard/shell.rb` | `sh` / `shebang` helpers |
44
+ | `lib/asgard/tasks.rb` | `class Tasks < Asgard::Base` — the convention class users reopen; also holds gem-owned built-in tasks |
45
+
46
+ ### Naming Convention for Gem-Owned Methods
47
+
48
+ Any task or method defined by Asgard itself inside `Tasks` (i.e. not by the user's `.loki` files) must be prefixed with `_`. This distinguishes built-in gem behavior from user-defined tasks and prevents naming collisions.
49
+
50
+ ```ruby
51
+ # lib/asgard/tasks.rb — gem-owned built-ins use _ prefix
52
+ class Tasks < Asgard::Base
53
+ desc "--version", "Show version"
54
+ map "--version" => :_version
55
+ def _version
56
+ puts Asgard::VERSION
57
+ exit
58
+ end
59
+ end
60
+ ```
61
+
62
+ `method_added` in `Base` already skips `_`-prefixed methods when attaching dependency metadata, so built-ins are naturally excluded from the dependency graph.
63
+
64
+ Do not define `_`-prefixed methods in user `.loki` files — that namespace is reserved for the gem.
65
+
66
+ ### DSL Mechanics (`lib/asgard/base.rb`)
67
+
68
+ **`depends_on`** stores stages in `@_pending_deps`. On `method_added`, those stages are popped and stored in `@_deps[method_name]`. Bare symbols are sequential stages; arrays within a `depends_on` call are parallel stages:
69
+
70
+ ```ruby
71
+ depends_on :a, [:b, :c], :d # stages: [[:a], [:b, :c], [:d]]
72
+ ```
73
+
74
+ **`var`** stores values or lambdas in `@_vars` and creates an instance method that evaluates the lambda once on first access.
75
+
76
+ **`invoke_command`** (Thor dispatch hook):
77
+ 1. Atomically check `@_ran_tasks` Set (with `@_ran_mutex`); return early if already run
78
+ 2. Resolve `@_deps` stages → `_build_dep_graph` → `Dagwood::DependencyGraph#parallel_order`
79
+ 3. For each parallel group: spawn one thread per task, join; single-task groups run inline
80
+ 4. Execute the target task
81
+
82
+ **`_build_dep_graph(stages)`** converts stages to a DAG hash:
83
+ - `[[:a], [:b, :c], [:d]]` → `{ a: [], b: [:a], c: [:a], d: [:b, :c] }`
84
+
85
+ ### Dependency Resolution
86
+
87
+ Dagwood topologically sorts the DAG and returns parallel groups. The thread-safe deduplication (`_ran_tasks` Set + Mutex) ensures each task runs exactly once even when multiple tasks share a common dependency.
88
+
89
+ ### Shell Helpers
90
+
91
+ - `sh(script, silent: false)` — single-line strings use `system(script)`; multi-line strings pipe through `bash -c`; exits with the command's status on failure
92
+ - `shebang(interpreter, script)` — writes script to a tempfile and executes with the named interpreter (`:python3`, `:node`, `:ruby`, `:perl`, `:bash`, etc.)
93
+
94
+ ## Testing
95
+
96
+ All tests are in `test/test_asgard.rb` (one file, ~11 named classes). SimpleCov minimum is 95%; the Rakefile configures this with a prelude that loads coverage before the library.
97
+
98
+ Key test patterns: tests frequently subclass `Asgard::Base` directly (not `Tasks`) to test the engine in isolation, and use `capture_io` for output assertions.
99
+
100
+ ## The `.loki` Format
101
+
102
+ A `.loki` file is plain Ruby that reopens `Tasks`:
103
+
104
+ ```ruby
105
+ class Tasks
106
+ var :gem_name, "asgard"
107
+
108
+ desc "test", "Run tests"
109
+ def test = sh "bundle exec rake test"
110
+
111
+ depends_on :test
112
+ desc "release", "Build and release"
113
+ def release = sh "bundle exec rake release"
114
+ end
115
+ ```
116
+
117
+ Multiple `*.loki` files in the same directory are all loaded (alphabetically). The bare `.loki` file serves only as the project root marker — its content is loaded last.
data/README.md CHANGED
@@ -1,9 +1,11 @@
1
1
  # Asgard
2
2
 
3
- A [just](https://just.systems)-like task runner for Ruby. Built on [Thor](https://github.com/rails/thor) for argument handling and [Dagwood](https://github.com/rewindio/dagwood) for dependency ordering.
3
+ A Ruby task runner built on [Thor](https://github.com/rails/thor) for argument handling and [Dagwood](https://github.com/rewindio/dagwood) for dependency ordering.
4
4
 
5
5
  The name comes from Norse mythology: **Thor** is the CLI framework, **Asgard** is the realm where tasks live, and the task file is named **loki** — because Loki holds all the tricks.
6
6
 
7
+ > **Asgard is a wrapper around [Thor](https://github.com/rails/thor).** Anything Thor can do — subcommands, typed options, argument validation, shell completion — is available inside a `.loki` file. Familiarity with Thor's DSL will make you immediately productive with Asgard.
8
+
7
9
  ## Installation
8
10
 
9
11
  ```bash
@@ -213,6 +215,61 @@ end
213
215
 
214
216
  ---
215
217
 
218
+ ## Helper methods
219
+
220
+ Private methods are callable from any task in the same class but are never registered as commands — they won't appear in `--help` output and can't be invoked from the CLI.
221
+
222
+ ```ruby
223
+ class Tasks
224
+ desc "build", "Compile and package"
225
+ def build
226
+ compile("src")
227
+ package(version)
228
+ end
229
+
230
+ desc "release", "Build and publish"
231
+ def release
232
+ build
233
+ sh "gem push pkg/myapp-#{version}.gem"
234
+ end
235
+
236
+ private
237
+
238
+ def compile(dir)
239
+ sh "gcc -O2 -o bin/myapp #{dir}/*.c"
240
+ end
241
+
242
+ def package(ver)
243
+ sh "tar czf pkg/myapp-#{ver}.tar.gz bin/"
244
+ end
245
+ end
246
+ ```
247
+
248
+ Helpers can also be shared across multiple `.loki` files by extracting them into a plain Ruby file and loading it explicitly:
249
+
250
+ ```ruby
251
+ # shared/helpers.rb
252
+ module BuildHelpers
253
+ private
254
+
255
+ def compile(dir)
256
+ sh "gcc -O2 -o bin/myapp #{dir}/*.c"
257
+ end
258
+ end
259
+
260
+ # .loki
261
+ require_relative "shared/helpers"
262
+
263
+ class Tasks
264
+ include BuildHelpers
265
+
266
+ desc "build", "Compile the project"
267
+ def build = compile("src")
268
+ end
269
+ ```
270
+
271
+ ---
272
+
216
273
  ## Options shared across all tasks
217
274
 
218
275
  `class_option` defines an option available to every task in the class:
@@ -312,6 +369,64 @@ end
312
369
 
313
370
  ---
314
371
 
372
+ ## Subcommands
373
+
374
+ Group related tasks under a common name using Thor's `subcommand` method. Define a subcommand class that inherits from `Tasks`, then register it with a name and description.
375
+
376
+ ```ruby
377
+ class DeployCommands < Tasks
378
+ desc "staging", "Deploy to staging"
379
+ def staging = sh "cap staging deploy"
380
+
381
+ desc "production", "Deploy to production"
382
+ def production = sh "cap production deploy"
383
+ end
384
+
385
+ class Tasks
386
+ desc "deploy SUBCOMMAND", "Deploy the application"
387
+ subcommand "deploy", DeployCommands
388
+ end
389
+ ```
390
+
391
+ ```bash
392
+ asgard deploy # shows deploy subcommand help
393
+ asgard deploy staging
394
+ asgard deploy production
395
+ ```
396
+
397
+ Subcommand tasks have all the same access to helper methods like `sh`, `shebang`, `depends_on`, `var`, and the built-in `--debug`/`--verbose` class options as normal tasks.
398
+
399
+ `depends_on` only works within a subcommand group exactly as it does at the top level:
400
+
401
+ ```ruby
402
+ class DBCommands < Tasks
403
+ desc "migrate", "Run pending migrations"
404
+ def migrate = sh "rails db:migrate"
405
+
406
+ desc "seed", "Load seed data"
407
+ def seed = sh "rails db:seed"
408
+
409
+ depends_on :migrate, :seed
410
+ desc "reset", "Migrate then seed"
411
+ def reset = puts "Done."
412
+ end
413
+
414
+ class Tasks
415
+ desc "db SUBCOMMAND", "Manage the database"
416
+ subcommand "db", DBCommands
417
+ end
418
+ ```
419
+
420
+ ```bash
421
+ asgard db reset # migrate → seed → reset
422
+ ```
423
+
424
+ Each subcommand group can have its own `desc`, `long_desc`, `option`, `class_option`, and `map` declarations, all scoped to that group.
425
+
426
+ See [`examples/server_subcommands.loki`](examples/server_subcommands.loki) and [`examples/db_subcommands.loki`](examples/db_subcommands.loki) for full working examples.
427
+
428
+ ---
429
+
315
430
  ## `method_option` types reference
316
431
 
317
432
  | Type | CLI example | Ruby value |
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
@@ -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)
@@ -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
data/lib/asgard/tasks.rb CHANGED
@@ -3,4 +3,26 @@
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 "--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.1"
4
+ VERSION = "0.1.2"
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,14 @@ 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)
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}"
36
37
  load_loki(File.dirname(task_file))
37
38
  load task_file
38
39
  Tasks.validate_deps!
39
40
  Tasks._reset_ran!
40
41
  Tasks.start(argv)
41
42
  rescue CircularDependencyError => e
42
- warn "asgard: circular dependency — #{e.message}"
43
- exit 1
43
+ abort "asgard: circular dependency — #{e.message}"
44
44
  end
45
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.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dewayne VanHoozer
@@ -51,9 +51,9 @@ dependencies:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
53
  version: '3.0'
54
- description: 'Asgard brings just-style recipes to Ruby: Thor-powered CLI, dep ordering
55
- via Dagwood::DependencyGraph, var declarations, dotenv, sh/shebang helpers, and
56
- 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.
57
57
  email:
58
58
  - dewayne@vanhoozer.me
59
59
  executables:
@@ -64,6 +64,7 @@ files:
64
64
  - ".envrc"
65
65
  - ".loki"
66
66
  - CHANGELOG.md
67
+ - CLAUDE.md
67
68
  - COMMITS.md
68
69
  - LICENSE.txt
69
70
  - README.md
@@ -71,6 +72,10 @@ files:
71
72
  - bin/asgard
72
73
  - bin/console
73
74
  - bin/setup
75
+ - examples/.loki
76
+ - examples/db_subcommands.loki
77
+ - examples/kitchen_sink.loki
78
+ - examples/server_subcommands.loki
74
79
  - lib/asgard.rb
75
80
  - lib/asgard/base.rb
76
81
  - lib/asgard/shell.rb
@@ -100,5 +105,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
100
105
  requirements: []
101
106
  rubygems_version: 4.0.12
102
107
  specification_version: 4
103
- summary: A just-like task runner built on Thor, with recipe dependencies via Dagwood.
108
+ summary: A powerful Ruby-based task runner
104
109
  test_files: []