asgard 0.1.2 → 0.3.0

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/docs/options.md ADDED
@@ -0,0 +1,180 @@
1
+ # Options & Flags
2
+
3
+ Asgard tasks use the full Thor option system. Options declared with `method_option` (alias: `option`) apply to a single task. Options declared with `class_option` apply to every task in the class. Asgard ships with three built-in `class_option` declarations on `Tasks`: `--debug`, `--verbose`, and `--version`.
4
+
5
+ ---
6
+
7
+ ## Per-Task Options
8
+
9
+ `method_option` (or its alias `option`) declares an option for the immediately following task:
10
+
11
+ ```ruby
12
+ class Tasks
13
+ desc "deploy ENV", "Deploy to ENV"
14
+ method_option :branch,
15
+ aliases: "-b",
16
+ type: :string,
17
+ default: "main",
18
+ desc: "Git branch to deploy"
19
+ method_option :dry_run,
20
+ aliases: "-n",
21
+ type: :boolean,
22
+ default: false,
23
+ desc: "Print commands without running"
24
+ def deploy(env = "staging")
25
+ if options[:dry_run]
26
+ puts "Would deploy #{options[:branch]} to #{env}"
27
+ else
28
+ sh "cap #{env} deploy BRANCH=#{options[:branch]}"
29
+ end
30
+ end
31
+ end
32
+ ```
33
+
34
+ Access option values inside the task body via `options[:name]` (a hash keyed by symbol).
35
+
36
+ ---
37
+
38
+ ## Class Options (Shared Across All Tasks)
39
+
40
+ `class_option` defines an option available on every task in the class. Add your own to complement the built-in ones:
41
+
42
+ ```ruby
43
+ class Tasks
44
+ class_option :dry_run,
45
+ aliases: "-n",
46
+ type: :boolean,
47
+ default: false,
48
+ desc: "Print commands without running"
49
+
50
+ class_option :env,
51
+ type: :string,
52
+ default: "development",
53
+ enum: %w[development staging production],
54
+ desc: "Target environment"
55
+
56
+ desc "Deploy the application"
57
+ def deploy
58
+ if options[:dry_run]
59
+ puts "Would deploy to #{options[:env]}"
60
+ else
61
+ sh "cap #{options[:env]} deploy"
62
+ end
63
+ end
64
+
65
+ desc "Run database migrations"
66
+ def migrate
67
+ sh "rails db:migrate RAILS_ENV=#{options[:env]}"
68
+ end
69
+ end
70
+ ```
71
+
72
+ Both `deploy` and `migrate` automatically accept `--dry-run` and `--env`.
73
+
74
+ ---
75
+
76
+ ## Built-in Flags
77
+
78
+ `Tasks` ships with three built-in class options and a version flag:
79
+
80
+ ### `--version`
81
+
82
+ Prints `Asgard::VERSION` and exits. Implemented as the `_version` method with the `_` prefix convention (gem-owned, blocked from direct CLI invocation):
83
+
84
+ ```bash
85
+ asgard --version
86
+ # 0.1.2
87
+ ```
88
+
89
+ ### `--debug`
90
+
91
+ A `class_option :debug` of type `:boolean`. When passed, sets `$DEBUG = true` before the task body runs (via the `invoke_command` hook in `Asgard::Base`):
92
+
93
+ ```bash
94
+ asgard build --debug
95
+ ```
96
+
97
+ Inside the task, use the `debug?` predicate:
98
+
99
+ ```ruby
100
+ def build
101
+ sh "rake build"
102
+ sh "rake build --trace" if debug?
103
+ end
104
+ ```
105
+
106
+ ### `--verbose`
107
+
108
+ A `class_option :verbose` of type `:boolean`. When passed, sets `$VERBOSE = true` before the task body runs:
109
+
110
+ ```bash
111
+ asgard test --verbose
112
+ ```
113
+
114
+ Inside the task, use the `verbose?` predicate:
115
+
116
+ ```ruby
117
+ def test
118
+ flags = verbose? ? "--verbose" : ""
119
+ sh "bundle exec rake test #{flags}"
120
+ end
121
+ ```
122
+
123
+ ---
124
+
125
+ ## `debug?` and `verbose?` Predicates
126
+
127
+ Both are private methods on `Tasks`, thin wrappers around the global variables:
128
+
129
+ ```ruby
130
+ private
131
+
132
+ def debug? = $DEBUG
133
+ def verbose? = $VERBOSE
134
+ ```
135
+
136
+ They are available in every task body and in subcommand classes that inherit from `Tasks`. Because `--debug` and `--verbose` are `class_option` declarations (not standalone commands), they work as modifiers alongside any task:
137
+
138
+ ```bash
139
+ asgard build --debug --verbose
140
+ asgard deploy production --verbose
141
+ ```
142
+
143
+ ---
144
+
145
+ ## Option Types Reference
146
+
147
+ | Type | CLI Example | Ruby Value |
148
+ |---|---|---|
149
+ | `:string` | `--branch main` | `"main"` |
150
+ | `:boolean` | `--force` / `--no-force` | `true` / `false` |
151
+ | `:numeric` | `--count 3` | `3` |
152
+ | `:array` | `--tags foo bar baz` | `["foo", "bar", "baz"]` |
153
+ | `:hash` | `--vars KEY:val FOO:bar` | `{"KEY"=>"val", "FOO"=>"bar"}` |
154
+
155
+ ---
156
+
157
+ ## Option Keys Reference
158
+
159
+ | Key | Applies to | Description |
160
+ |---|---|---|
161
+ | `aliases` | `method_option`, `class_option` | Short-form flag string, e.g. `"-b"` |
162
+ | `type` | `method_option`, `class_option` | One of the five types above |
163
+ | `default` | `method_option`, `class_option` | Value used when the flag is omitted |
164
+ | `required` | `method_option` | Raises an error if the flag is missing |
165
+ | `desc` | `method_option`, `class_option` | One-line description shown in help |
166
+ | `enum` | `method_option`, `class_option` | Allowed values; validated by Thor |
167
+ | `banner` | `method_option` | Placeholder shown in help for the value slot |
168
+
169
+ ---
170
+
171
+ ## `_` Prefix Convention
172
+
173
+ Methods whose names start with `_` are considered gem-owned in Asgard's naming convention. `run!` guards against invoking them directly from the CLI:
174
+
175
+ ```bash
176
+ asgard _version
177
+ # asgard: unknown command '_version'
178
+ ```
179
+
180
+ If you define your own methods on `Tasks`, avoid the `_` prefix to prevent them from being silently blocked.
data/docs/shell.md ADDED
@@ -0,0 +1,208 @@
1
+ # Shell Helpers
2
+
3
+ Asgard provides two methods for running shell commands and scripts from within task bodies: `sh` for shell commands and heredocs, and `shebang` for polyglot scripts. Both are provided by `Asgard::Shell` and mixed into every `Tasks` instance.
4
+
5
+ Both methods exit with the command's status code on failure — they do not raise Ruby exceptions.
6
+
7
+ ---
8
+
9
+ ## `sh` — Run Shell Commands
10
+
11
+ ### Single-Line Command
12
+
13
+ Pass a single-line string to run it via `system`:
14
+
15
+ ```ruby
16
+ class Tasks
17
+ desc "Compile the project"
18
+ def build = sh "rake build"
19
+
20
+ desc "Remove build artifacts"
21
+ def clean = sh "rm -rf dist/ tmp/"
22
+ end
23
+ ```
24
+
25
+ By default, `sh` prints the command before running it:
26
+
27
+ ```
28
+ rake build
29
+ ```
30
+
31
+ ### Multi-Line Heredoc
32
+
33
+ Pass a multiline string (e.g., a heredoc) to run it as a single `bash -c` script. All lines execute in the same shell session, so variable assignments carry across lines:
34
+
35
+ ```ruby
36
+ class Tasks
37
+ desc "Bootstrap the development environment"
38
+ def setup
39
+ sh <<~SHELL
40
+ brew install redis postgresql
41
+ brew services start redis
42
+ bundle install
43
+ rails db:setup
44
+ SHELL
45
+ end
46
+ end
47
+ ```
48
+
49
+ Asgard detects the newline and automatically routes multiline scripts through `bash -c`.
50
+
51
+ ### Silent Mode
52
+
53
+ Pass `silent: true` to suppress the command echo. The command still runs and still exits on failure; it just doesn't print the command text first:
54
+
55
+ ```ruby
56
+ class Tasks
57
+ desc "Compile (quiet)"
58
+ def build = sh "rake build", silent: true
59
+
60
+ desc "Print environment info without noise"
61
+ def info
62
+ sh "printenv | grep APP_", silent: true
63
+ end
64
+ end
65
+ ```
66
+
67
+ ### Exit on Failure
68
+
69
+ `sh` always calls `exit($?.exitstatus)` if the command fails. There is no rescue path — a failing command terminates the `asgard` process. This is intentional: failed steps should stop the pipeline rather than silently continue.
70
+
71
+ ```ruby
72
+ class Tasks
73
+ depends_on :test
74
+ desc "Test then release"
75
+ def release
76
+ sh "bundle exec rake release"
77
+ # Never reached if rake release fails
78
+ puts "Released!"
79
+ end
80
+ end
81
+ ```
82
+
83
+ ---
84
+
85
+ ## `shebang` — Polyglot Scripts
86
+
87
+ `shebang` writes the script body to a tempfile with the appropriate extension and executes it with the specified interpreter. Use it to embed Python, Node.js, Ruby, Perl, or any other interpreter directly in a task:
88
+
89
+ ### Python
90
+
91
+ ```ruby
92
+ class Tasks
93
+ desc "Run Python data analysis"
94
+ def analyze
95
+ shebang :python3, <<~PYTHON
96
+ import json
97
+ data = json.load(open("results.json"))
98
+ print(f"Total: {sum(data.values())}")
99
+ PYTHON
100
+ end
101
+ end
102
+ ```
103
+
104
+ ### Node.js
105
+
106
+ ```ruby
107
+ class Tasks
108
+ desc "Build frontend assets with esbuild"
109
+ def bundle_assets
110
+ shebang :node, <<~JS
111
+ const esbuild = require("esbuild")
112
+ esbuild.buildSync({
113
+ entryPoints: ["src/app.js"],
114
+ bundle: true,
115
+ outfile: "dist/app.js"
116
+ })
117
+ JS
118
+ end
119
+ end
120
+ ```
121
+
122
+ ### Ruby
123
+
124
+ ```ruby
125
+ class Tasks
126
+ desc "Transform data with Ruby"
127
+ def transform
128
+ shebang :ruby, <<~RUBY
129
+ require "json"
130
+ data = JSON.parse(File.read("input.json"))
131
+ File.write("output.json", JSON.pretty_generate(data.transform_values(&:upcase)))
132
+ RUBY
133
+ end
134
+ end
135
+ ```
136
+
137
+ ### Bash
138
+
139
+ ```ruby
140
+ class Tasks
141
+ desc "Run a bash provisioning script"
142
+ def provision
143
+ shebang :bash, <<~BASH
144
+ set -euo pipefail
145
+ apt-get update
146
+ apt-get install -y curl wget git
147
+ echo "Provisioned at $(date)"
148
+ BASH
149
+ end
150
+ end
151
+ ```
152
+
153
+ ---
154
+
155
+ ## Supported Interpreters
156
+
157
+ | Symbol | File Extension | Interpreter |
158
+ |---|---|---|
159
+ | `:python3` | `.py` | `python3` |
160
+ | `:python` | `.py` | `python` |
161
+ | `:node` | `.js` | `node` |
162
+ | `:ruby` | `.rb` | `ruby` |
163
+ | `:perl` | `.pl` | `perl` |
164
+ | `:bash` | `.sh` | `bash` |
165
+ | `:sh` | `.sh` | `sh` |
166
+ | Any other symbol | `.tmp` | Passed directly to `system` |
167
+
168
+ !!! note
169
+ Unknown interpreter symbols get a `.tmp` extension and are passed to `system` directly. This makes it easy to use interpreters not in the table above:
170
+
171
+ ```ruby
172
+ shebang :lua, <<~LUA
173
+ print("Hello from Lua!")
174
+ LUA
175
+ ```
176
+
177
+ ### Silent Mode
178
+
179
+ `shebang` also accepts `silent: true`, though in practice the interpreter itself controls what is printed:
180
+
181
+ ```ruby
182
+ def analyze
183
+ shebang :python3, script_body, silent: true
184
+ end
185
+ ```
186
+
187
+ ---
188
+
189
+ ## Combining `sh` and `shebang`
190
+
191
+ You can mix both in the same task:
192
+
193
+ ```ruby
194
+ class Tasks
195
+ desc "Run a mixed shell + Python pipeline"
196
+ def pipeline
197
+ sh "bundle exec rake build"
198
+
199
+ shebang :python3, <<~PYTHON
200
+ import subprocess
201
+ result = subprocess.run(["./bin/validate"], capture_output=True, text=True)
202
+ print(result.stdout)
203
+ PYTHON
204
+
205
+ sh "rake deploy"
206
+ end
207
+ end
208
+ ```
@@ -0,0 +1,181 @@
1
+ # Subcommands
2
+
3
+ Subcommands group related tasks under a common namespace, giving you commands like `asgard server start` or `asgard db migrate`. Asgard uses Thor's `subcommand` method for this, with one important convention: subcommand classes inherit from `Tasks` rather than from `Asgard::Base` or `Thor` directly.
4
+
5
+ ---
6
+
7
+ ## Basic Pattern
8
+
9
+ Define a subcommand class that inherits from `Tasks`, then register it on the top-level `Tasks` class with `subcommand`:
10
+
11
+ ```ruby
12
+ class DeployCommands < Tasks
13
+ desc "Deploy to staging"
14
+ def staging = sh "cap staging deploy"
15
+
16
+ desc "Deploy to production"
17
+ def production = sh "cap production deploy"
18
+ end
19
+
20
+ class Tasks
21
+ desc "deploy SUBCOMMAND", "Deploy the application"
22
+ subcommand "deploy", DeployCommands
23
+ end
24
+ ```
25
+
26
+ ```bash
27
+ asgard deploy # shows deploy subcommand help
28
+ asgard deploy staging
29
+ asgard deploy production
30
+ ```
31
+
32
+ ---
33
+
34
+ ## Why Inherit from `Tasks`?
35
+
36
+ Inheriting from `Tasks` (rather than `Asgard::Base` or `Thor`) gives the subcommand class access to:
37
+
38
+ - `sh` and `shebang` shell helpers (from `Asgard::Shell`)
39
+ - `depends_on` for dependency declarations
40
+ - `dotenv` for environment loading
41
+ - `@@` class variables declared on `Tasks` (visible in all subclasses)
42
+ - The built-in `--debug` and `--verbose` class options
43
+ - The `debug?` and `verbose?` private predicates
44
+ - Any private helpers or `no_commands` methods defined on `Tasks`
45
+
46
+ !!! warning
47
+ Do **not** redeclare `class_option :debug` or `class_option :verbose` in your subcommand class — they are already inherited from `Tasks`. Redeclaring them causes duplicate option errors.
48
+
49
+ ---
50
+
51
+ ## depends_on Within a Subcommand
52
+
53
+ `depends_on` works exactly as at the top level, scoped to the subcommand's own dependency graph:
54
+
55
+ ```ruby
56
+ class DBCommands < Tasks
57
+ desc "Run pending migrations"
58
+ def migrate = sh "rails db:migrate"
59
+
60
+ desc "Load seed data"
61
+ def seed = sh "rails db:seed"
62
+
63
+ depends_on :migrate, :seed
64
+ desc "Migrate then seed"
65
+ def reset = puts "Done."
66
+ end
67
+
68
+ class Tasks
69
+ desc "db SUBCOMMAND", "Manage the database"
70
+ subcommand "db", DBCommands
71
+ end
72
+ ```
73
+
74
+ ```bash
75
+ asgard db reset # migrate → seed → reset
76
+ ```
77
+
78
+ ---
79
+
80
+ ## Server Subcommand Example
81
+
82
+ ```ruby
83
+ class ServerCommands < Tasks
84
+ desc "start [PORT]", "Start the server on PORT (default: 3000)"
85
+ option :daemon, aliases: "-d", type: :boolean, default: false, desc: "Run as a background daemon"
86
+ option :workers, aliases: "-w", type: :numeric, default: 2, desc: "Number of worker processes"
87
+ option :log, type: :string, default: "log/server.log",
88
+ banner: "FILE", desc: "Write logs to FILE"
89
+ def start(port = "3000")
90
+ flags = []
91
+ flags << "--daemon" if options[:daemon]
92
+ flags << "--workers #{options[:workers]}"
93
+ flags << "--log #{options[:log]}"
94
+ sh "puma -p #{port} #{flags.join(' ')}"
95
+ end
96
+
97
+ desc "Stop the running server"
98
+ option :force, aliases: "-f", type: :boolean, default: false, desc: "Force-kill without draining"
99
+ def stop
100
+ options[:force] ? sh "pkill -9 puma" : sh "pumactl stop"
101
+ end
102
+
103
+ desc "Show server status"
104
+ def status = sh "pumactl stats"
105
+
106
+ depends_on :stop, :start
107
+ desc "restart [PORT]", "Stop then start"
108
+ def restart(port = "3000") = puts "Server restarted on :#{port}."
109
+ end
110
+
111
+ class Tasks
112
+ desc "server SUBCOMMAND", "Manage the application server"
113
+ subcommand "server", ServerCommands
114
+ end
115
+ ```
116
+
117
+ ```bash
118
+ asgard server start
119
+ asgard server start 4000 --workers 4 --daemon
120
+ asgard server stop --force
121
+ asgard server restart
122
+ asgard server status
123
+ ```
124
+
125
+ ---
126
+
127
+ ## Scoped DSL
128
+
129
+ Each subcommand class has its own independent scope for:
130
+
131
+ - `desc` / `long_desc` — documentation strings
132
+ - `method_option` / `option` — per-command options
133
+ - `class_option` — options shared across the subcommand's tasks (in addition to inherited ones)
134
+ - `map` — aliases within the subcommand group
135
+ - `default_task` — which command runs when the subcommand is invoked with no further arguments
136
+ - `depends_on` — dependency declarations scoped to this class
137
+
138
+ These do not bleed into the parent `Tasks` class or other subcommand classes.
139
+
140
+ ---
141
+
142
+ ## Multiple Subcommands
143
+
144
+ You can register as many subcommand groups as needed:
145
+
146
+ ```ruby
147
+ class ServerCommands < Tasks
148
+ # ... server tasks ...
149
+ end
150
+
151
+ class DBCommands < Tasks
152
+ # ... database tasks ...
153
+ end
154
+
155
+ class DeployCommands < Tasks
156
+ # ... deploy tasks ...
157
+ end
158
+
159
+ class Tasks
160
+ desc "server SUBCOMMAND", "Manage the server"; subcommand "server", ServerCommands
161
+ desc "db SUBCOMMAND", "Manage the database"; subcommand "db", DBCommands
162
+ desc "deploy SUBCOMMAND", "Deploy"; subcommand "deploy", DeployCommands
163
+ end
164
+ ```
165
+
166
+ ---
167
+
168
+ ## Subcommands Across Files
169
+
170
+ Define each subcommand class in its own `.loki` file. Because all files reopen the same Ruby classes, the classes are available when `.loki` registers them:
171
+
172
+ ```
173
+ myproject/
174
+ .loki ← registers all subcommands
175
+ server_subcommands.loki ← defines ServerCommands
176
+ db_subcommands.loki ← defines DBCommands
177
+ ```
178
+
179
+ Because siblings loaded via `import "*.loki"` execute before `.loki`'s own class body, both `DBCommands` and `ServerCommands` are defined by the time `.loki` runs its `subcommand` calls.
180
+
181
+ See [`examples/server_subcommands.loki`](examples.md#server-subcommands) and [`examples/db_subcommands.loki`](examples.md#db-subcommands) for complete working examples.