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 +4 -4
- data/CHANGELOG.md +2 -2
- data/CLAUDE.md +117 -0
- data/README.md +116 -1
- data/examples/.loki +2 -0
- data/examples/db_subcommands.loki +74 -0
- data/examples/kitchen_sink.loki +164 -0
- data/examples/server_subcommands.loki +56 -0
- data/lib/asgard/base.rb +6 -4
- data/lib/asgard/tasks.rb +23 -1
- data/lib/asgard/version.rb +1 -1
- data/lib/asgard.rb +4 -4
- metadata +10 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2d904144543326c15257e4e61584c46fa9b0ab4f1ba522680daa930950361e6a
|
|
4
|
+
data.tar.gz: fea08260a5db1fdde268eaf4ca930b1e52a07a39f12eaf2ce20e31736319cdff
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
43
|
-
- `var` — declare static or lazy-evaluated variables available to all
|
|
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
|
|
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,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
|
|
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(*
|
|
68
|
-
@_pending_deps =
|
|
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
|
|
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
|
|
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
|
data/lib/asgard/version.rb
CHANGED
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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:
|
|
55
|
-
|
|
56
|
-
importable task
|
|
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
|
|
108
|
+
summary: A powerful Ruby-based task runner
|
|
104
109
|
test_files: []
|