asgard 0.5.1 → 0.6.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.
Files changed (102) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +179 -99
  3. data/README.md +141 -46
  4. data/bin/asgard +10 -0
  5. data/lib/asgard/base/dispatch.rb +257 -31
  6. data/lib/asgard/base/registry.rb +7 -3
  7. data/lib/asgard/base.rb +140 -9
  8. data/lib/asgard/declarations.rb +21 -0
  9. data/lib/asgard/doctor/task_sections.rb +11 -3
  10. data/lib/asgard/doctor.rb +12 -8
  11. data/lib/asgard/jobs/commands.rb +152 -0
  12. data/lib/asgard/jobs/declaration.rb +149 -0
  13. data/lib/asgard/jobs/launcher.rb +109 -0
  14. data/lib/asgard/jobs/state.rb +190 -0
  15. data/lib/asgard/jobs/stopper.rb +141 -0
  16. data/lib/asgard/jobs.rb +25 -0
  17. data/lib/asgard/kernel_methods.rb +19 -14
  18. data/lib/asgard/leading_flag.rb +10 -0
  19. data/lib/asgard/pid.rb +130 -0
  20. data/lib/asgard/schedule/commands.rb +39 -11
  21. data/lib/asgard/schedule/declaration.rb +139 -12
  22. data/lib/asgard/schedule/job_files.rb +24 -0
  23. data/lib/asgard/schedule/launchd.rb +55 -18
  24. data/lib/asgard/schedule/systemd.rb +69 -27
  25. data/lib/asgard/schedule/table.rb +34 -16
  26. data/lib/asgard/schedule.rb +49 -9
  27. data/lib/asgard/shell.rb +372 -44
  28. data/lib/asgard/state_dir.rb +84 -0
  29. data/lib/asgard/tasks.rb +8 -0
  30. data/lib/asgard/version.rb +1 -1
  31. data/lib/asgard/watch/changes.rb +26 -1
  32. data/lib/asgard/watch/commands.rb +52 -12
  33. data/lib/asgard/watch/daemon.rb +334 -43
  34. data/lib/asgard/watch/declaration.rb +135 -40
  35. data/lib/asgard/watch/dispatcher.rb +61 -10
  36. data/lib/asgard/watch/mailbox.rb +50 -0
  37. data/lib/asgard/watch/reporter.rb +50 -34
  38. data/lib/asgard/watch/spawn.rb +41 -16
  39. data/lib/asgard/watch/state.rb +93 -27
  40. data/lib/asgard/watch/worker.rb +44 -9
  41. data/lib/asgard/watch.rb +4 -11
  42. data/lib/asgard.rb +80 -14
  43. metadata +167 -186
  44. data/.envrc +0 -6
  45. data/.github/workflows/deploy-github-pages.yml +0 -52
  46. data/.loki +0 -39
  47. data/.reek.yml +0 -166
  48. data/.rubocop.yml +0 -163
  49. data/Archspec.rb +0 -24
  50. data/CLAUDE.md +0 -139
  51. data/COMMITS.md +0 -196
  52. data/bin/console +0 -11
  53. data/bin/setup +0 -8
  54. data/doc_tasks.loki +0 -21
  55. data/docs/api.md +0 -368
  56. data/docs/assets/css/custom.css +0 -93
  57. data/docs/assets/images/asgard.jpg +0 -0
  58. data/docs/changelog.md +0 -1
  59. data/docs/dependencies.md +0 -313
  60. data/docs/environment.md +0 -129
  61. data/docs/examples.md +0 -194
  62. data/docs/getting-started.md +0 -181
  63. data/docs/helpers.md +0 -262
  64. data/docs/index.md +0 -87
  65. data/docs/options.md +0 -224
  66. data/docs/schedule.md +0 -106
  67. data/docs/shell.md +0 -237
  68. data/docs/subcommands.md +0 -181
  69. data/docs/task-files.md +0 -407
  70. data/docs/tasks.md +0 -326
  71. data/docs/variables.md +0 -338
  72. data/docs/watch.md +0 -123
  73. data/examples/.env +0 -4
  74. data/examples/.envrc +0 -3
  75. data/examples/.loki +0 -24
  76. data/examples/bad.loki +0 -63
  77. data/examples/concurrent.loki +0 -58
  78. data/examples/confirm.loki +0 -48
  79. data/examples/db_subcommands.loki +0 -74
  80. data/examples/depends_on_block/bad/.loki +0 -51
  81. data/examples/depends_on_block/good/.loki +0 -83
  82. data/examples/do.loki +0 -45
  83. data/examples/env_usage.loki +0 -27
  84. data/examples/kitchen_sink.loki +0 -210
  85. data/examples/server_subcommands.loki +0 -65
  86. data/examples/subdir/.loki +0 -12
  87. data/examples/subdir/import_demo.loki +0 -14
  88. data/examples/subdir/import_up_demo.loki +0 -18
  89. data/examples/watcher_examples/.envrc +0 -3
  90. data/examples/watcher_examples/.loki +0 -54
  91. data/examples/watcher_examples/README.md +0 -112
  92. data/examples/watcher_examples/bin/hello +0 -3
  93. data/examples/watcher_examples/data/records.json +0 -1
  94. data/examples/watcher_examples/docs/drafts/ideas.md +0 -5
  95. data/examples/watcher_examples/docs/index.md +0 -4
  96. data/examples/watcher_examples/lib/greeter.rb +0 -7
  97. data/gem_tasks.loki +0 -51
  98. data/git.loki +0 -13
  99. data/mkdocs.yml +0 -166
  100. data/quality.loki +0 -266
  101. data/quality_rails.loki +0 -47
  102. data/xyzzy.loki +0 -12
data/docs/options.md DELETED
@@ -1,224 +0,0 @@
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 four built-in `class_option` declarations on `Tasks`: `--debug`, `--verbose`, `--version`, and `--doctor`.
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
- ### Boolean class options and `no_negate`
75
-
76
- Thor automatically generates `[--no-name]` and `[--skip-name]` help entries alongside every boolean `class_option`. When negation is meaningful — `--dry-run` paired with `--no-dry-run` — this is useful. When negation is meaningless, call `no_negate` immediately after the declaration to remove the extra variants from the help output:
77
-
78
- ```ruby
79
- class Tasks
80
- class_option :color,
81
- type: :boolean,
82
- default: true,
83
- desc: "Colorize output"
84
- no_negate :color
85
- end
86
- ```
87
-
88
- Help output before `no_negate`:
89
-
90
- ```
91
- [--color], [--no-color], [--skip-color] # Colorize output
92
- ```
93
-
94
- Help output after `no_negate`:
95
-
96
- ```
97
- [--color] # Colorize output
98
- ```
99
-
100
- `no_negate` accepts multiple option names in a single call:
101
-
102
- ```ruby
103
- no_negate :color, :version, :emoji
104
- ```
105
-
106
- It has no effect on runtime behaviour — `--no-color` still works on the CLI; only the help display is affected.
107
-
108
- ---
109
-
110
- ## Built-in Flags
111
-
112
- `Tasks` ships with four built-in `class_option` declarations — `--debug`, `--verbose`, `--version`, and `--doctor` — all visible in the Options section of `asgard help`.
113
-
114
- ### `--version`
115
-
116
- A `class_option :version` of type `:boolean`. Prints `Asgard::VERSION` and exits. Handled by `Asgard.run!` before the `.loki` file is loaded, so it works even in a directory without a `.loki` file. `no_negate :version` suppresses the `[--no-version]` / `[--skip-version]` variants (see [Boolean class options and `no_negate`](#boolean-class-options-and-no_negate) above):
117
-
118
- ```bash
119
- asgard --version
120
- # 0.3.0
121
- ```
122
-
123
- ### `--doctor`
124
-
125
- A `class_option :doctor` of type `:boolean`. Diagnoses `.loki` resolution, import chains, and task definitions for the current directory, then exits. Handled by `Asgard.run!` before the `.loki` file is loaded (same pattern as `--version`) — deliberately so, since it needs to keep working in the exact situations that would otherwise abort `run!`: a broken `.loki` file, a circular or undefined dependency, or a task silently redefined by a later `def`. The report includes a "Tasks by file" listing — every command grouped by the file it's defined in, as `file:line` — with any silently-overridden task called out inline, right where it's defined. `no_negate :doctor` suppresses the `[--no-doctor]` / `[--skip-doctor]` variants:
126
-
127
- ```bash
128
- asgard --doctor
129
- ```
130
-
131
- See [`Asgard::Doctor` in the API Reference](api.md#asgarddoctor) for the full breakdown of what it checks.
132
-
133
- ### `--debug`
134
-
135
- 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`):
136
-
137
- ```bash
138
- asgard build --debug
139
- ```
140
-
141
- Inside the task, use the `debug?` predicate:
142
-
143
- ```ruby
144
- def build
145
- sh "rake build"
146
- sh "rake build --trace" if debug?
147
- end
148
- ```
149
-
150
- ### `--verbose`
151
-
152
- A `class_option :verbose` of type `:boolean`. When passed, sets `$VERBOSE = true` before the task body runs:
153
-
154
- ```bash
155
- asgard test --verbose
156
- ```
157
-
158
- Inside the task, use the `verbose?` predicate:
159
-
160
- ```ruby
161
- def test
162
- flags = verbose? ? "--verbose" : ""
163
- sh "bundle exec rake test #{flags}"
164
- end
165
- ```
166
-
167
- ---
168
-
169
- ## `debug?` and `verbose?` Predicates
170
-
171
- Both are private methods on `Tasks`, thin wrappers around the global variables:
172
-
173
- ```ruby
174
- private
175
-
176
- def debug? = $DEBUG
177
- def verbose? = $VERBOSE
178
- ```
179
-
180
- 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:
181
-
182
- ```bash
183
- asgard build --debug --verbose
184
- asgard deploy production --verbose
185
- ```
186
-
187
- ---
188
-
189
- ## Option Types Reference
190
-
191
- | Type | CLI Example | Ruby Value |
192
- |---|---|---|
193
- | `:string` | `--branch main` | `"main"` |
194
- | `:boolean` | `--force` / `--no-force` | `true` / `false` |
195
- | `:numeric` | `--count 3` | `3` |
196
- | `:array` | `--tags foo bar baz` | `["foo", "bar", "baz"]` |
197
- | `:hash` | `--vars KEY:val FOO:bar` | `{"KEY"=>"val", "FOO"=>"bar"}` |
198
-
199
- ---
200
-
201
- ## Option Keys Reference
202
-
203
- | Key | Applies to | Description |
204
- |---|---|---|
205
- | `aliases` | `method_option`, `class_option` | Short-form flag string, e.g. `"-b"` |
206
- | `type` | `method_option`, `class_option` | One of the five types above |
207
- | `default` | `method_option`, `class_option` | Value used when the flag is omitted |
208
- | `required` | `method_option` | Raises an error if the flag is missing |
209
- | `desc` | `method_option`, `class_option` | One-line description shown in help |
210
- | `enum` | `method_option`, `class_option` | Allowed values; validated by Thor |
211
- | `banner` | `method_option` | Placeholder shown in help for the value slot |
212
-
213
- ---
214
-
215
- ## `_` Prefix Convention
216
-
217
- Methods whose names start with `_` are considered gem-owned in Asgard's naming convention. `run!` guards against invoking them directly from the CLI:
218
-
219
- ```bash
220
- asgard _something
221
- # asgard: unknown command '_something'
222
- ```
223
-
224
- If you define your own methods on `Tasks`, avoid the `_` prefix to prevent them from being blocked. Built-in `class_option` declarations (like `--version`, `--debug`, `--verbose`, `--doctor`) do not use the `_` prefix because they are options, not commands.
data/docs/schedule.md DELETED
@@ -1,106 +0,0 @@
1
- # Scheduled Tasks
2
-
3
- Asgard can run any task on a schedule. You declare schedules in the project's own `.loki`, and `asgard schedule install` hands them to the platform's own scheduler: **launchd** on macOS, **systemd user timers** on Linux. Both schedulers run a calendar job that was missed while the machine slept as soon as it wakes.
4
-
5
- Scheduling is built into the gem, so there is nothing to import or require.
6
-
7
- ---
8
-
9
- ## Declaring Schedules
10
-
11
- Call `schedule` at class level inside `Tasks`:
12
-
13
- ```ruby
14
- # .loki
15
- class Tasks
16
- desc "daily_summary", "Summarize the day's work"
17
- def daily_summary = sh "bin/summary"
18
-
19
- schedule :daily_summary, at: "17:30", on: :weekdays
20
- schedule :weekly_report, at: "16:00", on: :friday
21
- schedule :sync, every: 3600 # seconds
22
- schedule :backup, at: %w[02:00 14:00] # on: defaults to :daily
23
-
24
- # The task's own options go in options:, split the way a shell would
25
- # (quotes respected).
26
- schedule :report, options: "--format md -v", at: "08:00", on: :weekdays
27
- schedule :report, options: "--period week", at: "16:00", on: :friday
28
- schedule :notify, options: "--msg 'backup done'", every: 86_400, as: "notify_daily"
29
- end
30
- ```
31
-
32
- A declaration needs exactly one of `at:` or `every:`.
33
-
34
- | Keyword | Meaning |
35
- |---------|---------|
36
- | `at:` | `"HH:MM"` (24-hour), or an Array of times |
37
- | `on:` | `:daily` (default), `:weekdays`, `:weekends`, a day (`:friday`), or an Array of days |
38
- | `every:` | Interval in seconds, or anything that responds to `in_seconds` (an ActiveSupport `Duration`) |
39
- | `options:` | The task's own arguments, as a String (split shell-style) or an Array of words |
40
- | `env:` | `{ "KEY" => "value" }`: literal environment variables added to the job |
41
- | `as:` | The entry's name (letters, digits, `_ . -`) |
42
-
43
- ### Entry Names
44
-
45
- Every entry has a name, and the subcommands below take it as `NAME`. The name defaults to the task name. When the entry has `options:`, the default is a slug of the task plus its options (`:report, options: "--format md -v"` becomes `report-format-md-v`), so one task can be scheduled several times with different flags. `as:` overrides the default. Declaring two *different* entries with the same name raises an error.
46
-
47
- ### Durations
48
-
49
- Asgard doesn't depend on ActiveSupport. If you want `every: 3.minutes`, require it yourself at the top of your `.loki`:
50
-
51
- ```ruby
52
- require "active_support/core_ext/integer/time"
53
-
54
- class Tasks
55
- schedule :sync, every: 15.minutes
56
- end
57
- ```
58
-
59
- ---
60
-
61
- ## Managing Schedules
62
-
63
- `schedule` is also a command with subcommands. They work the same way on both platforms:
64
-
65
- ```bash
66
- asgard schedule preview # print the job files install would write
67
- asgard schedule install # load declared entries; drop entries no longer declared
68
- asgard schedule list # this project's entries, state, last exit (--all: every project's)
69
- asgard schedule stop NAME # stop one entry (stays stopped across reboots and installs)
70
- asgard schedule start NAME # start a stopped entry, or install just this one
71
- asgard schedule trigger NAME # run an installed entry now, under the scheduler
72
- asgard schedule log NAME [-f] # print the entry's log (-f keeps following it)
73
- asgard schedule remove # unload and delete all of this project's entries
74
- ```
75
-
76
- `stop`, `start`, `trigger` and `log` also accept an entry of another project, so
77
- they work from any directory. Use the name shown by `list --all`; if two projects
78
- have an entry with the same name, write it as `project/name` (this project's own
79
- entry wins a bare name).
80
-
81
- Re-run `asgard schedule install` after changing declarations or your `PATH`.
82
-
83
- ---
84
-
85
- ## How Jobs Run
86
-
87
- Each entry runs `asgard <task> [options]` from the directory that holds `.loki`, so installing from any subdirectory gives the same result. The job gets the `PATH` that was current when you ran `install`, plus any `env:` variables.
88
-
89
- If the project has a `.envrc`, the job runs under `direnv exec`, so API keys and other secrets load from `.envrc` at run time and are never copied into the job files. If `direnv` isn't on your `PATH`, `install` warns that `.envrc` will not be loaded.
90
-
91
- Entries are scoped to the project (the name of the directory holding `.loki`), so `list`, `install` and `remove` only touch this project's jobs.
92
-
93
- | | macOS (launchd) | Linux (systemd) |
94
- |---|---|---|
95
- | Job files | `~/Library/LaunchAgents/com.madbomber.asgard.<project>.<name>.plist` | `~/.config/systemd/user/asgard.<project>.<name>.{service,timer}` |
96
- | Logs | `~/Library/Logs/asgard/` | `~/.local/state/asgard/` (honors `XDG_STATE_HOME`) |
97
- | Stop | `launchctl disable` | `systemctl --user disable --now` |
98
- | Caveats | runs only while you're logged in | runs only while you're logged in unless `loginctl enable-linger`; needs systemd 240+ |
99
-
100
- Other platforms aren't supported. The `schedule` subcommands there exit with an error.
101
-
102
- ---
103
-
104
- ## Name Collisions
105
-
106
- The built-in command is registered as `_schedule` and mapped to `schedule`, following Asgard's `_` convention for gem-owned tasks. If your `.loki` defines its own `schedule` task, `asgard schedule` still dispatches to the built-in command.
data/docs/shell.md DELETED
@@ -1,237 +0,0 @@
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. `sh` also accepts `exec: true` to replace the asgard process outright instead of forking — see [Handing Off with `exec`](#handing-off-with-exec).
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
- ### Handing Off with `exec`
84
-
85
- Pass `exec: true` to hand the command the asgard process itself instead of forking a child. Under the hood this calls `Kernel.exec` rather than `system`, which replaces the running ruby process image with the command — asgard doesn't stick around waiting on it.
86
-
87
- Use this for a task's final, long-running command — a dev server, a REPL, anything meant to take over the terminal — so there's no idle ruby process sitting in memory alongside it, and Ctrl-C is handled directly by the command instead of unwinding back through asgard:
88
-
89
- ```ruby
90
- class Tasks
91
- desc "Documentation server startup"
92
- depends_on :doc_builder
93
- def doc_server = sh "mkdocs serve", exec: true
94
- end
95
- ```
96
-
97
- Since the process is replaced, nothing after the `sh` call ever runs, and `depends_on` chains upstream of it must already have finished (they have — dependencies run before the task body).
98
-
99
- ```ruby
100
- class Tasks
101
- desc "Start a REPL — never returns to asgard"
102
- def console
103
- sh "bundle exec pry", exec: true
104
- puts "unreachable"
105
- end
106
- end
107
- ```
108
-
109
- !!! note
110
- `exec: true` only makes sense for a command meant to run for the lifetime of the process. Don't use it for a step with more work queued after it in the same task.
111
-
112
- ---
113
-
114
- ## `shebang` — Polyglot Scripts
115
-
116
- `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:
117
-
118
- ### Python
119
-
120
- ```ruby
121
- class Tasks
122
- desc "Run Python data analysis"
123
- def analyze
124
- shebang :python3, <<~PYTHON
125
- import json
126
- data = json.load(open("results.json"))
127
- print(f"Total: {sum(data.values())}")
128
- PYTHON
129
- end
130
- end
131
- ```
132
-
133
- ### Node.js
134
-
135
- ```ruby
136
- class Tasks
137
- desc "Build frontend assets with esbuild"
138
- def bundle_assets
139
- shebang :node, <<~JS
140
- const esbuild = require("esbuild")
141
- esbuild.buildSync({
142
- entryPoints: ["src/app.js"],
143
- bundle: true,
144
- outfile: "dist/app.js"
145
- })
146
- JS
147
- end
148
- end
149
- ```
150
-
151
- ### Ruby
152
-
153
- ```ruby
154
- class Tasks
155
- desc "Transform data with Ruby"
156
- def transform
157
- shebang :ruby, <<~RUBY
158
- require "json"
159
- data = JSON.parse(File.read("input.json"))
160
- File.write("output.json", JSON.pretty_generate(data.transform_values(&:upcase)))
161
- RUBY
162
- end
163
- end
164
- ```
165
-
166
- ### Bash
167
-
168
- ```ruby
169
- class Tasks
170
- desc "Run a bash provisioning script"
171
- def provision
172
- shebang :bash, <<~BASH
173
- set -euo pipefail
174
- apt-get update
175
- apt-get install -y curl wget git
176
- echo "Provisioned at $(date)"
177
- BASH
178
- end
179
- end
180
- ```
181
-
182
- ---
183
-
184
- ## Supported Interpreters
185
-
186
- | Symbol | File Extension | Interpreter |
187
- |---|---|---|
188
- | `:python3` | `.py` | `python3` |
189
- | `:python` | `.py` | `python` |
190
- | `:node` | `.js` | `node` |
191
- | `:ruby` | `.rb` | `ruby` |
192
- | `:perl` | `.pl` | `perl` |
193
- | `:bash` | `.sh` | `bash` |
194
- | `:sh` | `.sh` | `sh` |
195
- | Any other symbol | `.tmp` | Passed directly to `system` |
196
-
197
- !!! note
198
- 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:
199
-
200
- ```ruby
201
- shebang :lua, <<~LUA
202
- print("Hello from Lua!")
203
- LUA
204
- ```
205
-
206
- ### Silent Mode
207
-
208
- `shebang` also accepts `silent: true`, though in practice the interpreter itself controls what is printed:
209
-
210
- ```ruby
211
- def analyze
212
- shebang :python3, script_body, silent: true
213
- end
214
- ```
215
-
216
- ---
217
-
218
- ## Combining `sh` and `shebang`
219
-
220
- You can mix both in the same task:
221
-
222
- ```ruby
223
- class Tasks
224
- desc "Run a mixed shell + Python pipeline"
225
- def pipeline
226
- sh "bundle exec rake build"
227
-
228
- shebang :python3, <<~PYTHON
229
- import subprocess
230
- result = subprocess.run(["./bin/validate"], capture_output=True, text=True)
231
- print(result.stdout)
232
- PYTHON
233
-
234
- sh "rake deploy"
235
- end
236
- end
237
- ```