dr_talon 0.1.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 (67) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +60 -0
  3. data/CODE_OF_CONDUCT.md +10 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +270 -0
  6. data/Rakefile +10 -0
  7. data/exe/talon +6 -0
  8. data/lib/dr_talon/cli/engine.rb +274 -0
  9. data/lib/dr_talon/cli/generate.rb +29 -0
  10. data/lib/dr_talon/cli.rb +343 -0
  11. data/lib/dr_talon/config.rb +29 -0
  12. data/lib/dr_talon/credentials.rb +94 -0
  13. data/lib/dr_talon/deps/dependency.rb +238 -0
  14. data/lib/dr_talon/deps/git.rb +69 -0
  15. data/lib/dr_talon/deps/installer.rb +207 -0
  16. data/lib/dr_talon/deps/manifest.rb +220 -0
  17. data/lib/dr_talon/deps/tree.rb +97 -0
  18. data/lib/dr_talon/deps.rb +62 -0
  19. data/lib/dr_talon/doctor.rb +161 -0
  20. data/lib/dr_talon/engine/fetchers.rb +251 -0
  21. data/lib/dr_talon/engine/git_ignore.rb +63 -0
  22. data/lib/dr_talon/engine/importer.rb +103 -0
  23. data/lib/dr_talon/engine/inspector.rb +113 -0
  24. data/lib/dr_talon/engine/installation.rb +77 -0
  25. data/lib/dr_talon/engine/materializer.rb +172 -0
  26. data/lib/dr_talon/engine/spec.rb +38 -0
  27. data/lib/dr_talon/engine/store.rb +119 -0
  28. data/lib/dr_talon/engine.rb +50 -0
  29. data/lib/dr_talon/errors.rb +5 -0
  30. data/lib/dr_talon/file_copy.rb +49 -0
  31. data/lib/dr_talon/generators/new_project.rb +289 -0
  32. data/lib/dr_talon/generators/talon_code.rb +92 -0
  33. data/lib/dr_talon/http.rb +120 -0
  34. data/lib/dr_talon/paths.rb +115 -0
  35. data/lib/dr_talon/project.rb +101 -0
  36. data/lib/dr_talon/runner.rb +188 -0
  37. data/lib/dr_talon/talon_framework.rb +103 -0
  38. data/lib/dr_talon/templates/bare/main_tests.rb.tt +7 -0
  39. data/lib/dr_talon/templates/bare/rubocop.yml.tt +8 -0
  40. data/lib/dr_talon/templates/bare/solargraph.yml.tt +24 -0
  41. data/lib/dr_talon/templates/lib/app/main.rb.tt +13 -0
  42. data/lib/dr_talon/templates/lib/lib/library.rb.tt +4 -0
  43. data/lib/dr_talon/templates/lib/lib/version.rb.tt +3 -0
  44. data/lib/dr_talon/templates/lib/rubocop.yml.tt +9 -0
  45. data/lib/dr_talon/templates/lib/solargraph.yml.tt +23 -0
  46. data/lib/dr_talon/templates/lib/test/test_helper.rb.tt +1 -0
  47. data/lib/dr_talon/templates/lib/test/tests.rb.tt +8 -0
  48. data/lib/dr_talon/templates/shared/Gemfile.tt +10 -0
  49. data/lib/dr_talon/templates/shared/README.md.tt +45 -0
  50. data/lib/dr_talon/templates/shared/editorconfig.tt +9 -0
  51. data/lib/dr_talon/templates/shared/gitignore.tt +19 -0
  52. data/lib/dr_talon/templates/talon/game/dependencies.rb.tt +3 -0
  53. data/lib/dr_talon/templates/talon/game/main.rb.tt +9 -0
  54. data/lib/dr_talon/templates/talon/game/spin.rb.tt +7 -0
  55. data/lib/dr_talon/templates/talon/game/title.rb.tt +8 -0
  56. data/lib/dr_talon/templates/talon/game/title_tests.rb.tt +16 -0
  57. data/lib/dr_talon/templates/talon/generate/component.rb.tt +19 -0
  58. data/lib/dr_talon/templates/talon/generate/component_tests.rb.tt +15 -0
  59. data/lib/dr_talon/templates/talon/generate/prefab.rb.tt +11 -0
  60. data/lib/dr_talon/templates/talon/generate/prefab_tests.rb.tt +15 -0
  61. data/lib/dr_talon/templates/talon/generate/scene.rb.tt +14 -0
  62. data/lib/dr_talon/templates/talon/generate/scene_tests.rb.tt +13 -0
  63. data/lib/dr_talon/test_report.rb +76 -0
  64. data/lib/dr_talon/version.rb +5 -0
  65. data/lib/dr_talon.rb +17 -0
  66. data/sig/dr_talon.rbs +524 -0
  67. metadata +142 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b0f3e2b0a809454db387d0d2782ea7374d93b8ecab4f68088b43775ea000ec04
4
+ data.tar.gz: 6505483d2a0afc819e039687ad651cf09a18eb4956ae8b0110546ba3ec9e5ae7
5
+ SHA512:
6
+ metadata.gz: 5f3aae9f5c691833c3d43e76708855baf876358e2bf890b5bd1ee8563d565d48f0844110164ba672991799cba460971af7b5f1e57086f985333c3cf769c7c36e
7
+ data.tar.gz: f64e5e291bbae8b123015cc1e4eb504f8dc63ed053221f772ab241c56e5b336cc6c7fd2330d404daa3ed169ab8b04d31ef63224016886569a884484690460611
data/CHANGELOG.md ADDED
@@ -0,0 +1,60 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2026-10-11
4
+
5
+ First release: `talon`, a Rails-style CLI for DragonRuby Game Toolkit.
6
+
7
+ ### Engines
8
+
9
+ - `talon engine import <zip|dir>` caches a DragonRuby engine as `VERSION-TIER-PLATFORM`, reading the version from
10
+ `CHANGELOG-CURR.txt`, the platform from the engine directory, and the tier from the zip name (or `--tier`).
11
+ Executable bits and the hidden `.dragonruby/` directory are kept; re-importing the same zip is a no-op.
12
+ - `talon engine fetch` downloads the latest engine for this machine: Pro and Indie from dragonruby.org with your
13
+ account login, the free Jam license from DragonRuby's itch.io page with no account, and Standard with itch.io's
14
+ butler. The tier defaults to your newest cached engine's.
15
+ - `talon engine login` / `logout` check and save, or delete, a dragonruby.org login in `credentials.yml` (mode 0600;
16
+ refused if others can read it). `DRAGONRUBY_USERNAME` / `DRAGONRUBY_PASSWORD` take precedence.
17
+ - `talon engine list`, `which`, `uninstall` (asks first, since the cache may be the only copy of an old version).
18
+ - `talon engine use [ENGINE]` pins an engine in `.dragonruby-version` (version and tier only, so it works on every
19
+ OS) and copies it into the project, cloning files on btrfs/XFS. Installed files are tracked in
20
+ `.talon/engine.yml`, so switching engines removes exactly those; `mygame/` and your own files are never touched,
21
+ and differing files stop the install unless `--force`. `talon engine current` shows pinned vs installed.
22
+
23
+ ### Projects
24
+
25
+ - `talon new NAME` creates a game in DragonRuby's layout from the cached engine's own `mygame/`, with a starter
26
+ test, RuboCop and Solargraph config, `game_metadata.txt` filled in (`--title`, `--dev-id`, `--dev-title`,
27
+ `config.yml`, or asked for in a terminal, since `dragonruby-publish` needs them), and `git init` /
28
+ `bundle install`. It asks whether to commit the engine; if not, a managed `.gitignore` block
29
+ ignores it and `talon engine use` keeps the block current.
30
+ - `talon new NAME --lib` creates a library (the repository root is the game directory) with `lib/`, a demo
31
+ `app/main.rb` and a DragonRuby test.
32
+ - `talon new NAME --template talon` creates a game on the [Talon](https://github.com/asimmons91/talon) framework,
33
+ with Talon vendored, a `Title` scene, a `Spin` component and a test.
34
+ - `talon generate scene|component|prefab NAME` (alias `talon g`) writes Talon code and a DragonRuby test, and adds
35
+ the require to `app/main.rb`.
36
+ - `talon doctor` checks the pin, the cached and installed engine, dependencies, and `game_metadata.txt`.
37
+ - `config.yml` settings: `default_template`, `dev_id`, `dev_title`.
38
+
39
+ ### Running, testing and building
40
+
41
+ - `talon run [ARGS...]` runs the game; arguments such as `--record` pass through to `dragonruby`.
42
+ - `talon test [FILES...]` runs each `*_tests.rb` with `dragonruby --test`, headless by default, and reads
43
+ DragonRuby's output (it always exits 0) to report failures, files that don't load, files with no tests and
44
+ timeouts, exiting non-zero on any of them.
45
+ - `talon build` packages every platform with `dragonruby-publish --only-package`; `talon serve` serves the newest
46
+ HTML5 build with `dragonruby-httpd`; `talon clean` deletes `builds/`, `logs/`, `tmp/` and `errors/`.
47
+
48
+ ### Dependencies
49
+
50
+ - Declared in the game directory's `dependencies.rb` with a Foodchain-compatible DSL (`github`, `git`, `url`,
51
+ `path`), vendored into `vendor/`, and locked below `__END__` to commit SHAs (or SHA-256s for URLs) with a checksum
52
+ of the vendored files. Foodchain's ETag locks are replaced on the first install.
53
+ - `talon add SOURCE` (`github:owner/repo`, `git:URL`, `url:URL`, `path:DIR`; `--path`, `--ref`, `--as`) installs a
54
+ dependency and prints the line to require it; `talon remove`, `talon update`, `talon outdated`.
55
+ - `talon install` restores the pinned engine if it's missing, then the locked dependencies. Locally edited vendored
56
+ files stop it unless `--force`.
57
+ - GitHub dependencies resolve refs with `git ls-remote` and download one tarball per dependency.
58
+
59
+ [Unreleased]: https://github.com/asimmons91/dr_talon/compare/v0.1.0...HEAD
60
+ [0.1.0]: https://github.com/asimmons91/dr_talon/releases/tag/v0.1.0
@@ -0,0 +1,10 @@
1
+ # Code of Conduct
2
+
3
+ "dr_talon" follows [The Ruby Community Conduct Guideline](https://www.ruby-lang.org/en/conduct) in all "collaborative space", which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.):
4
+
5
+ * Participants will be tolerant of opposing views.
6
+ * Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
7
+ * When interpreting the words and actions of others, participants should always assume good intentions.
8
+ * Behaviour which can be reasonably considered harassment will not be tolerated.
9
+
10
+ If you have any concerns about behaviour within this project, please contact us at ["austin_simmons@fastmail.com"](mailto:"austin_simmons@fastmail.com").
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Austin Simmons
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,270 @@
1
+ # dr_talon
2
+
3
+ `talon` is a Rails-style command line tool for [DragonRuby Game Toolkit](https://dragonruby.org):
4
+
5
+ - **Engines.** It keeps a cache of DragonRuby versions, downloads new ones, and pins one per project, so every
6
+ clone runs the same engine.
7
+ - **Projects.** `talon new` scaffolds games and libraries; `talon run`, `test`, `build` and `serve` replace the
8
+ usual shell scripts. `talon test` reports failures properly, even though DragonRuby itself always exits 0.
9
+ - **Dependencies.** It vendors DragonRuby libraries from GitHub, git, URLs or local paths, locked to exact commits.
10
+ - **Talon.** Optionally, it scaffolds games on the [Talon](https://github.com/asimmons91/talon) framework and
11
+ generates scenes, components and prefabs.
12
+
13
+ ## Requirements
14
+
15
+ - Ruby 3.2 or newer
16
+ - git, for GitHub and git dependencies
17
+ - A DragonRuby engine. Any license works, including the free [Jam license](#downloading-engines), which `talon`
18
+ can download for you.
19
+
20
+ `talon` is developed and tested on Linux. It handles macOS and Windows paths and engine names, but has seen less
21
+ use there.
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ gem install dr_talon
27
+ ```
28
+
29
+ The gem installs a `talon` executable. `talon help` lists every command, and `talon help COMMAND` describes one.
30
+
31
+ ## Quick start
32
+
33
+ ```bash
34
+ talon engine fetch --tier jam # or: talon engine import ~/Downloads/dragonruby-pro-linux-amd64.zip
35
+ talon new my_game # asks whether to commit the engine, and your itch.io name
36
+ cd my_game
37
+ talon run # ./dragonruby mygame
38
+ talon test # *_tests.rb in mygame/test/ and mygame/tests/ (one starter test)
39
+ talon build # package every platform your license allows into builds/
40
+ talon doctor # check the engine, dependencies and game metadata
41
+ ```
42
+
43
+ `dragonruby-publish` won't package a game until `game_metadata.txt` names its developer, so `talon new` asks for
44
+ your itch.io username and display name, unless you pass `--dev-id` and `--dev-title` or set them in your
45
+ [config](#configuration). It only asks in a terminal; scripts aren't prompted.
46
+
47
+ ## Engines
48
+
49
+ DragonRuby only offers its latest release for download, so `talon` keeps a cache of every engine you fetch or
50
+ import. That cache is how you get back to an older version.
51
+
52
+ ```bash
53
+ talon engine import ~/Downloads/dragonruby-pro-linux-amd64.zip # the zip as downloaded, or an extracted directory
54
+ talon engine list
55
+ talon engine which 7.23 # 7.23, 7.23-pro or 7.23-pro-linux-amd64
56
+ talon engine uninstall 7.23 # asks first; --yes to skip
57
+ ```
58
+
59
+ Engines are identified as `VERSION-TIER-PLATFORM`, for example `7.23-pro-linux-amd64`. The version comes from
60
+ `CHANGELOG-CURR.txt` and the platform from the engine directory name. The tier comes from the zip or directory
61
+ name; pass `--tier pro|indie|standard|jam` if the name doesn't include it.
62
+
63
+ ### Downloading engines
64
+
65
+ ```bash
66
+ talon engine fetch --tier jam # the free Jam license; no account needed
67
+ talon engine login # once: saves your dragonruby.org login (mode 600)
68
+ talon engine fetch # latest release, same tier as your newest cached engine
69
+ talon engine fetch --tier indie
70
+ talon engine fetch --tier standard # via itch.io's butler, after `butler login`
71
+ talon engine logout
72
+ ```
73
+
74
+ - **Jam.** The [Jam license](https://dragonruby.itch.io/dragonruby-gtk/devlog/1620970/dragonruby-jam-license-free-to-all)
75
+ is free and can be used commercially. It has Standard's full API but only exports to the web. `talon` downloads
76
+ it the same way the Download button on [DragonRuby's itch.io page](https://dragonruby.itch.io/dragonruby-gtk#demo)
77
+ does, for Linux (x64 and arm64), macOS and Windows. If itch.io changes that page, download the zip by hand and
78
+ run `talon engine import`.
79
+ - **Pro and Indie.** These come from dragonruby.org's subscription API with your account's login. `talon` looks
80
+ for it in `DRAGONRUBY_USERNAME` and `DRAGONRUBY_PASSWORD`, then in the file saved by `talon engine login`, and
81
+ otherwise asks for it (offering to save it). The saved file holds the password in plain text, so it must be
82
+ readable only by you; `talon` refuses to use it otherwise.
83
+ - **Standard.** This comes from itch.io through [butler](https://itch.io/docs/butler/).
84
+
85
+ ### Pinning an engine to a project
86
+
87
+ ```bash
88
+ cd my_game
89
+ talon engine use 7.23-pro # pin in .dragonruby-version and install the engine here
90
+ talon engine use # after cloning: install whatever is pinned
91
+ talon engine current # pinned vs installed
92
+ ```
93
+
94
+ DragonRuby expects its engine files next to the game, so `talon engine use` copies the cached engine into the
95
+ project. On btrfs and XFS the files are cloned (reflinked), so each copy takes almost no extra disk space.
96
+
97
+ - **The pin.** `.dragonruby-version` holds only the version and tier (`7.23-pro`), so it works on every OS.
98
+ - **What gets copied.** Games in DragonRuby's layout (`mygame/app/main.rb`) get every engine file except
99
+ `mygame/`. Libraries, whose root is the game directory (`app/main.rb`), get just the `dragonruby` binary and
100
+ `font.ttf`.
101
+ - **Switching versions.** The installed files are listed in `.talon/engine.yml`. Switching engines removes exactly
102
+ those files and never touches `mygame/` or your other files. If an existing file would be overwritten and
103
+ differs from the engine's copy, the install stops until you pass `--force`.
104
+
105
+ ## Projects
106
+
107
+ ```bash
108
+ talon new my_game # a game, in DragonRuby's layout (mygame/)
109
+ talon new my-lib --lib # a library: lib/, app/main.rb, test/
110
+ talon new my_game --template talon # a game on the Talon framework (see below)
111
+ talon help new # all options
112
+ ```
113
+
114
+ `talon new` pins the newest cached engine (or `--engine 7.23-pro`) and installs it. It also:
115
+
116
+ - copies the engine's own sample game into `mygame/`
117
+ - adds a starter test, `mygame/tests/main_tests.rb`
118
+ - fills in `game_metadata.txt`: `gameid` and `gametitle` from the name, and `devid`/`devtitle` from
119
+ `--dev-id`/`--dev-title`, your config, or by asking
120
+ - adds RuboCop and Solargraph config
121
+ - runs `git init` and `bundle install` (`--skip-git`, `--skip-bundle`)
122
+
123
+ It asks whether to commit the engine to git, which DragonRuby recommends for private and commercial games. Pass
124
+ `--commit-engine` or `--no-commit-engine` to skip the question. If you don't commit it, the engine's files go in a
125
+ marked section of `.gitignore`, and anyone who clones the project runs `talon install` to restore it. Libraries
126
+ never commit the engine.
127
+
128
+ `talon doctor` checks that the pinned engine is cached and installed, that dependencies are installed and
129
+ unmodified, and that `game_metadata.txt` is ready to publish.
130
+
131
+ ### Running, testing and building
132
+
133
+ ```bash
134
+ talon run # ./dragonruby mygame (or . for libraries); extra args such as --record pass through
135
+ talon test # every *_tests.rb in test/ and tests/ under the game directory
136
+ talon test tests/player_tests.rb --no-headless --timeout 30 --verbose
137
+ talon build # dragonruby-publish --only-package: into builds/, never uploaded
138
+ talon serve # the newest HTML5 build at http://localhost:8080/ (--port)
139
+ talon clean # delete builds/, logs/, tmp/ and errors/
140
+ ```
141
+
142
+ DragonRuby's test runner always exits 0, so `talon test` reads its output instead. It prints each file's result
143
+ and the details of any failure, and it exits non-zero if a test fails, a file doesn't load, a file has no tests,
144
+ or a run passes `--timeout` (default 120 seconds).
145
+
146
+ Tests run headless by default, using SDL's dummy video and audio drivers, so no window opens and they work in CI.
147
+ Pass `--no-headless` for tests that need real rendering.
148
+
149
+ ## Dependencies
150
+
151
+ DragonRuby doesn't load gems, so libraries are vendored into the game: their files are copied into `vendor/` inside
152
+ the game directory and committed with it.
153
+
154
+ ```bash
155
+ talon add github:asimmons91/talon --path lib # vendor/talon, then prints the require line
156
+ talon add github:someone/tweens --ref v1.2.0 --as tweens
157
+ talon add url:https://example.com/easing.rb
158
+ talon add path:../my_lib/lib # a local library, copied on every install
159
+ talon install # after cloning: the engine, then locked dependencies
160
+ talon update [NAME...] # move to the newest commit of each ref
161
+ talon outdated
162
+ talon remove tweens
163
+ ```
164
+
165
+ Dependencies are declared in `dependencies.rb` in the game directory (`mygame/` for games). The format is
166
+ compatible with [Foodchain](https://github.com/pvande/foodchain), so existing files keep working:
167
+
168
+ ```ruby
169
+ github "asimmons91/talon", path: "lib", ref: "main", as: "talon"
170
+ github :asimmons91, :talon, "lib", destination: "vendor/talon" # Foodchain style
171
+ git "https://codeberg.org/someone/lib.git", path: "src", ref: "v2"
172
+ url "https://example.com/easing.rb"
173
+ path "../my_lib/lib"
174
+
175
+ __END__
176
+
177
+ # The lines below pin the versions of your installed dependencies.
178
+ ...
179
+ ```
180
+
181
+ `talon` owns everything below `__END__`. Each dependency gets one tab-separated line with its key, ref,
182
+ destination, locked commit (or a SHA-256 for `url`), and a checksum of the vendored files.
183
+
184
+ - `talon install` reproduces exactly those versions.
185
+ - If you edit a vendored file, `talon` notices and won't overwrite it unless you pass `--force`.
186
+ - GitHub dependencies resolve refs with `git ls-remote` and download one tarball each, so they don't use up the
187
+ GitHub API rate limit. Private repositories need `git:` with an SSH URL instead.
188
+
189
+ ## Talon games
190
+
191
+ [Talon](https://github.com/asimmons91/talon) is a scene, game object and component framework for DragonRuby.
192
+ `talon` works with any DragonRuby game; Talon support is opt-in.
193
+
194
+ ```bash
195
+ talon new my_game --template talon # Talon vendored, a Title scene, a Spin component and a test
196
+ talon generate scene MainMenu # app/scenes/main_menu.rb + tests/scenes/main_menu_tests.rb
197
+ talon generate component Health # app/components/health.rb + test
198
+ talon g prefab EnemyShip # app/prefabs/enemy_ship.rb + test (spawn(:ship, EnemyShip))
199
+ ```
200
+
201
+ DragonRuby only loads files that are explicitly required, so generators add each new file's `require` to a
202
+ marked section of `app/main.rb`, ordered components, then prefabs, then scenes. If that section is missing, they
203
+ print the line for you to paste.
204
+
205
+ Generators only run in projects whose `dependencies.rb` includes Talon. To use them in an existing game, run
206
+ `talon add github:asimmons91/talon --path lib` first.
207
+
208
+ ## Configuration
209
+
210
+ `~/.config/dr_talon/config.yml` (see below for other platforms):
211
+
212
+ ```yaml
213
+ default_template: bare # or talon, or lib
214
+ dev_id: my_itchio_username # devid in game_metadata.txt
215
+ dev_title: My Name # devtitle in game_metadata.txt
216
+ ```
217
+
218
+ ### Where talon keeps its files
219
+
220
+ | | Linux | macOS | Windows |
221
+ |---|---|---|---|
222
+ | Engines (data) | `$XDG_DATA_HOME/dr_talon` (`~/.local/share/dr_talon`) | `~/Library/Application Support/dr_talon` | `%LOCALAPPDATA%\dr_talon` |
223
+ | Downloads (cache) | `$XDG_CACHE_HOME/dr_talon` (`~/.cache/dr_talon`) | `~/Library/Caches/dr_talon` | `%LOCALAPPDATA%\dr_talon\cache` |
224
+ | Config and login | `$XDG_CONFIG_HOME/dr_talon` (`~/.config/dr_talon`) | `~/Library/Application Support/dr_talon` | `%APPDATA%\dr_talon` |
225
+
226
+ On macOS, explicitly set `XDG_*` variables take precedence. Setting `DR_TALON_HOME` puts everything under that one
227
+ directory (`data/`, `cache/`, `config/`).
228
+
229
+ ## Development
230
+
231
+ ```bash
232
+ bin/setup # install dependencies
233
+ bundle exec rake # tests + standardrb
234
+ bundle exec exe/talon # try the CLI from the checkout
235
+ ```
236
+
237
+ The tests never need a real DragonRuby engine or the network. They build small fake engines, use local git
238
+ repositories in place of GitHub, and run HTTP against a local server. Real DragonRuby output is kept as fixtures in
239
+ `test/fixtures/dragonruby/`. The design and its decisions are in [PLAN.md](PLAN.md).
240
+
241
+ ### Releasing
242
+
243
+ Add changes to the `## [Unreleased]` section of `CHANGELOG.md` as you go. To release:
244
+
245
+ ```bash
246
+ bundle exec rake release:prepare[minor] # or major, patch, or an explicit version like 1.2.3
247
+ git push origin main v0.2.0 # the command it prints
248
+ ```
249
+
250
+ `release:prepare` runs the tests, bumps `version.rb` and `Gemfile.lock`, moves the Unreleased notes under a dated
251
+ heading, and commits and tags the release. The first release is `rake release:prepare[0.1.0]`: the current
252
+ version, since it was never tagged.
253
+
254
+ Pushing the tag runs `.github/workflows/release.yml`. It checks that the tag, version and changelog agree, runs
255
+ the tests, publishes the gem to RubyGems.org with [Trusted Publishing](https://guides.rubygems.org/trusted-publishing/),
256
+ and creates the GitHub release from that version's changelog notes.
257
+
258
+ One-time setup: on RubyGems.org, add a trusted publisher for this repository with the workflow `release.yml` and the
259
+ environment `release`. Use a "pending" trusted publisher, since the gem has never been pushed.
260
+
261
+ ## Contributing
262
+
263
+ Bug reports and pull requests are welcome on GitHub at https://github.com/asimmons91/dr_talon. This project is
264
+ intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the
265
+ [code of conduct](https://github.com/asimmons91/dr_talon/blob/main/CODE_OF_CONDUCT.md).
266
+
267
+ ## License
268
+
269
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
270
+ DragonRuby Game Toolkit itself is commercial software under its own license; `talon` doesn't include any part of it.
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ require "standard/rake"
9
+
10
+ task default: %i[test standard]
data/exe/talon ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "dr_talon"
5
+
6
+ DrTalon::CLI.start(ARGV)
@@ -0,0 +1,274 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "thor"
5
+ require "tmpdir"
6
+
7
+ module DrTalon
8
+ class CLI < Thor
9
+ # +talon engine ...+: manage the cache of DragonRuby engines.
10
+ class Engine < Thor
11
+ def self.exit_on_failure?
12
+ true
13
+ end
14
+
15
+ desc "import PATH", "Add a DragonRuby zip (or an extracted engine directory) to the cache"
16
+ long_desc <<~DESC
17
+ Copies a DragonRuby distribution into talon's engine cache. PATH can be the
18
+ zip as downloaded or a directory it was already extracted to.
19
+
20
+ The version is read from CHANGELOG-CURR.txt and the platform from the
21
+ engine directory name. The tier is read from the zip or directory name
22
+ (e.g. dragonruby-pro-linux-amd64.zip); pass --tier when it isn't there.
23
+
24
+ DragonRuby only offers its latest release for download, so import every
25
+ version you may want to go back to.
26
+ DESC
27
+ method_option :tier, enum: DrTalon::Engine::TIERS, desc: "License tier, when the file name doesn't say"
28
+ method_option :platform, desc: "Engine platform, e.g. linux-amd64, when the directory name doesn't say"
29
+ method_option :force, type: :boolean, default: false, desc: "Replace a cached engine with the same id"
30
+ def import(path)
31
+ result = with_errors do
32
+ importer = DrTalon::Engine::Importer.new(store, tier: options[:tier], platform: options[:platform], force: options[:force])
33
+ importer.import(path)
34
+ end
35
+ installation = result.installation
36
+
37
+ case result.status
38
+ when :unchanged then say "#{installation.id} is already cached from the same zip."
39
+ when :replaced then say "Replaced #{installation.id} at #{installation.path}"
40
+ else say "Imported #{installation.id} to #{installation.path}"
41
+ end
42
+ end
43
+
44
+ desc "fetch", "Download the latest engine and add it to the cache"
45
+ long_desc <<~DESC
46
+ Downloads the newest DragonRuby release for this machine and imports it.
47
+
48
+ Jam (DragonRuby's free license: Standard's API, web export only) comes
49
+ from the free downloads on DragonRuby's itch.io page; no account needed.
50
+
51
+ Pro and Indie come from dragonruby.org with your account's username and
52
+ password, taken from DRAGONRUBY_USERNAME and DRAGONRUBY_PASSWORD, from
53
+ `talon engine login`, or asked for.
54
+
55
+ Standard comes from itch.io with butler (https://itch.io/docs/butler/),
56
+ after `butler login`.
57
+
58
+ The tier defaults to that of your newest cached engine. DragonRuby only
59
+ offers its latest release, so fetching regularly is how older versions
60
+ stay available in the cache.
61
+ DESC
62
+ method_option :tier, enum: DrTalon::Engine::TIERS, desc: "License tier (default: your newest cached engine's)"
63
+ method_option :keep_zip, type: :boolean, default: false, desc: "Keep the downloaded zip in talon's cache directory"
64
+ def fetch
65
+ tier = options[:tier] || store.latest_native&.tier ||
66
+ raise(Thor::Error, "Pass --tier #{DrTalon::Engine::TIERS.join("|")} (no cached engine to take it from). " \
67
+ "Jam is free and needs no account.")
68
+
69
+ result = with_errors { fetch_and_import(tier) }
70
+ installation = result.installation
71
+
72
+ if result.status == :unchanged
73
+ say "#{installation.id} is already cached; it's the latest #{tier} release."
74
+ else
75
+ say "Fetched #{installation.id} to #{installation.path}", :green
76
+ say "Use it in a project with `talon engine use #{installation.version}-#{installation.tier}`."
77
+ end
78
+ end
79
+
80
+ desc "login", "Save a dragonruby.org login for fetching Pro/Indie engines"
81
+ long_desc <<~DESC
82
+ Asks for your dragonruby.org username and password, checks them against
83
+ the download API, and saves them to credentials.yml in talon's config
84
+ directory, readable only by you (mode 0600). The password is stored in
85
+ plain text there; DRAGONRUBY_USERNAME and DRAGONRUBY_PASSWORD are an
86
+ alternative that stores nothing.
87
+ DESC
88
+ method_option :tier, enum: %w[pro indie], default: "pro", desc: "Tier to check the login against"
89
+ def login
90
+ credentials = ask_credentials
91
+ with_errors { DrTalon::Engine::Fetchers::DragonRubyAPI.new(options[:tier]).verify(credentials) }
92
+ credentials.save(Paths.new.credentials_file)
93
+ say "Saved the login for #{credentials.username} to #{Paths.new.credentials_file} (mode 600)."
94
+ end
95
+
96
+ desc "logout", "Delete the saved dragonruby.org login"
97
+ def logout
98
+ path = Paths.new.credentials_file
99
+ say DrTalon::Credentials.delete(path) ? "Deleted #{path}." : "No saved login at #{path}."
100
+ end
101
+
102
+ desc "list", "List cached engines"
103
+ def list
104
+ installations = with_errors { store.installations }
105
+
106
+ if installations.empty?
107
+ say "No engines cached yet. Add one with `talon engine import <zip>`."
108
+ return
109
+ end
110
+
111
+ pinned = pinned_installation
112
+ rows = installations.map do |installation|
113
+ marker = (installation == pinned) ? "*" : ""
114
+ [marker, installation.id, installation.version, installation.tier, installation.platform, installation.built_on.to_s]
115
+ end
116
+ print_table([["", "ID", "VERSION", "TIER", "PLATFORM", "BUILT"], *rows])
117
+ say "\n* pinned by this project" if pinned
118
+ end
119
+
120
+ desc "which [ENGINE]", "Print the path of a cached engine (default: this project's pin)"
121
+ def which(spec = nil)
122
+ say with_errors { spec ? store.find(spec) : store.find_native(required_pin) }.path
123
+ end
124
+
125
+ desc "use [ENGINE]", "Pin an engine for this project and install it here"
126
+ long_desc <<~DESC
127
+ Writes ENGINE (e.g. 7.23-pro) to .dragonruby-version and copies the cached
128
+ engine into the project, replacing the previously installed one. Without
129
+ ENGINE, installs the version already pinned, e.g. after cloning.
130
+
131
+ Only files talon installed before are removed. Files in mygame/ are never
132
+ touched, and other project files that differ from the engine's stop the
133
+ install unless you pass --force.
134
+
135
+ Run this in a DragonRuby project: a directory with .dragonruby-version,
136
+ mygame/app/main.rb (full layout) or app/main.rb (game-only layout).
137
+ DESC
138
+ method_option :force, type: :boolean, default: false, desc: "Overwrite project files that differ from the engine's"
139
+ def use(spec = nil)
140
+ project = with_errors { Project.locate }
141
+ installation, previous, result = with_errors do
142
+ requested = spec ? DrTalon::Engine::Spec.parse(spec) : required_pin(project)
143
+ installation = store.find_native(requested, default_tier: project.pin&.tier)
144
+ materializer = DrTalon::Engine::Materializer.new(project)
145
+ previous = materializer.manifest&.engine
146
+ result = materializer.install(installation, force: options[:force])
147
+ DrTalon::Engine::GitIgnore.refresh(project.path(".gitignore"), materializer.manifest.files)
148
+ [installation, previous, result]
149
+ end
150
+
151
+ repinned = project.pin&.then { |pin| "#{pin.version}-#{pin.tier}" } != "#{installation.version}-#{installation.tier}"
152
+ project.pin!(installation) if repinned
153
+
154
+ details = ["#{result.copied} files"]
155
+ details << "#{result.cloned} as reflinks" if result.cloned.positive?
156
+ if previous == installation.id
157
+ say "Reinstalled #{installation.id} into #{project.root} (#{details.join(", ")})"
158
+ else
159
+ details << "replaced #{previous}" if previous
160
+ say "Installed #{installation.id} into #{project.root} (#{details.join(", ")})"
161
+ end
162
+ say "Pinned #{installation.version}-#{installation.tier} in #{Project::PIN_FILE}" if repinned
163
+ end
164
+
165
+ desc "current", "Show the engine this project pins and the one installed"
166
+ def current
167
+ project = with_errors { Project.locate }
168
+ pin = with_errors { project.pin }
169
+ manifest = with_errors { DrTalon::Engine::Materializer.new(project).manifest }
170
+
171
+ say "Pinned: #{pin || "nothing (no #{Project::PIN_FILE})"}"
172
+ say "Installed: #{manifest&.engine || "nothing"}"
173
+ end
174
+
175
+ desc "uninstall ENGINE", "Remove an engine from the cache"
176
+ long_desc <<~DESC
177
+ Deletes a cached engine. DragonRuby only offers its latest release for
178
+ download, so the cache may be your only copy of older versions; you're
179
+ asked to confirm unless you pass --yes.
180
+ DESC
181
+ method_option :yes, type: :boolean, aliases: "-y", default: false, desc: "Don't ask for confirmation"
182
+ def uninstall(spec)
183
+ installation = with_errors { store.find(spec) }
184
+
185
+ unless options[:yes] || yes?("Remove #{installation.id}? The cache may be your only copy of this version. [y/N]")
186
+ say "Kept #{installation.id}."
187
+ return
188
+ end
189
+
190
+ store.remove(installation)
191
+ say "Removed #{installation.id}."
192
+ end
193
+
194
+ no_commands do
195
+ def store
196
+ @store ||= DrTalon::Engine::Store.new(Paths.new.engines_dir)
197
+ end
198
+
199
+ def required_pin(project = Project.find)
200
+ pin = project&.pin
201
+ return pin if pin
202
+
203
+ raise DrTalon::Error, "No engine given and no #{Project::PIN_FILE} found. Pass one, e.g. `7.23-pro`."
204
+ end
205
+
206
+ def pinned_installation
207
+ pin = Project.find&.pin
208
+ pin && store.find_native(pin)
209
+ rescue DrTalon::Error
210
+ nil
211
+ end
212
+
213
+ def fetch_and_import(tier)
214
+ fetcher = DrTalon::Engine::Fetchers.for(tier)
215
+ credentials = fetcher.credentials? ? DrTalon::Credentials.lookup || ask_credentials(offer_to_save: true) : nil
216
+ say "Downloading the latest #{tier} engine…" unless $stdout.tty?
217
+
218
+ path = begin
219
+ fetcher.fetch(credentials: credentials) { |received, total| show_progress(received, total) }
220
+ rescue DrTalon::HTTP::Unauthorized
221
+ source = {env: "DRAGONRUBY_USERNAME/DRAGONRUBY_PASSWORD", file: Paths.new.credentials_file, prompt: "what you typed"}
222
+ raise DrTalon::Error, "dragonruby.org rejected the login from #{source.fetch(credentials.source)}. " \
223
+ "Check it, or run `talon engine login`."
224
+ ensure
225
+ finish_progress
226
+ end
227
+
228
+ begin
229
+ DrTalon::Engine::Importer.new(store, tier: tier).import(path)
230
+ ensure
231
+ FileUtils.rm_rf(path) unless options[:keep_zip] && File.file?(path)
232
+ end
233
+ end
234
+
235
+ def ask_credentials(offer_to_save: false)
236
+ unless $stdin.tty?
237
+ raise Thor::Error, "No dragonruby.org login. Set #{DrTalon::Credentials::ENV_USERNAME} and " \
238
+ "#{DrTalon::Credentials::ENV_PASSWORD}, or run `talon engine login` in a terminal."
239
+ end
240
+
241
+ username = ask("dragonruby.org username:").to_s.strip
242
+ password = ask("dragonruby.org password:", echo: false).to_s
243
+ say ""
244
+ raise Thor::Error, "A username and password are needed." if username.empty? || password.empty?
245
+
246
+ credentials = DrTalon::Credentials.new(username: username, password: password, source: :prompt)
247
+ if offer_to_save && yes?("Save this login to #{Paths.new.credentials_file} (mode 600)? [y/N]")
248
+ credentials.save(Paths.new.credentials_file)
249
+ end
250
+ credentials
251
+ end
252
+
253
+ def show_progress(received, total)
254
+ return unless $stdout.tty?
255
+
256
+ @progress_shown = true
257
+ amount = total ? "#{received * 100 / total}% of #{total / 1_048_576} MB" : "#{received / 1_048_576} MB"
258
+ print "\rDownloading… #{amount} "
259
+ end
260
+
261
+ def finish_progress
262
+ puts if @progress_shown
263
+ @progress_shown = false
264
+ end
265
+
266
+ def with_errors
267
+ yield
268
+ rescue DrTalon::Error => e
269
+ raise Thor::Error, e.message
270
+ end
271
+ end
272
+ end
273
+ end
274
+ end