Skip to content

Releases: unabated-games/godot-cli

v0.26.0

Choose a tag to compare

@github-actions github-actions released this 25 Sep 10:25

Added

  • The installer works on Windows. install.sh refused on anything but macOS and Linux, so a Windows user unpacked the archive and wired up the env file, the skill and the docs by hand. It now detects Git Bash, MSYS2 and Cygwin, fetches the .zip rather than the tarball, unpacks it with whichever of unzip, tar or PowerShell is present, and installs godot-cli.exe. Verified end to end by faking the platform on macOS — real Windows archive, real checksum, real unpack — and the godot-cli.exe it installed was then run under wine.
  • install.ps1, for Windows without a POSIX shell: irm https://raw.githubusercontent.com/unabated-games/godot-cli/main/install.ps1 | iex, with -InstallSkill and -AddToPath. Same checksum verification, same layout. It has not been run on Windows — there is no PowerShell on any machine its author can reach — so the Git Bash path is the recommended one and the docs say so. Its assumptions about the archive were checked against the real artifact, which caught two mistakes before it shipped.
  • project resave <file>: Godot's own save of a scene or resource, as the reference for scene compare-godot. It imports first, unless --no-import, then has Godot headless load the file and save a copy. The copy goes to .godot/godot-cli/resave/<its path> by default, never over the file itself, which it refuses as resave_over_source. Trial 35 wrote that GDScript by hand and ran Godot from the shell to get the same thing. A copy saved to a new path has its ext_resource ids renumbered and its uids left out. That is how Godot writes a save to a new path, and compare-godot allows for both.
  • camera_3d and place_3d recipes place and aim a 3D node. camera_3d adds a Camera3D at position facing look_at, with optional current and fov. place_3d sets an existing 3D node's transform the same way. Each point is [x, y, z] or "Vector3(x, y, z)". The basis is Godot's looking_at (-Z toward the point, +Y up), written row by row. Trial 35's camera, worked out by hand, comes out of camera_3d digit for digit, and a Godot re-save of the recipe's output matches it.
  • uid read <file>: the UID a file records, read from the file itself, so it needs no uid cache and works on a fresh clone. It reads a scene or resource header, binary (.res, .scn) or text, a script's .uid sidecar, or an imported asset's .import file, and source says which. For a binary resource it also reports the class, whether the file is compressed, and the Godot version that saved it. A file that records no UID fails with no_uid_recorded and says why. Until now the only route to a binary resource's UID was uid cache lookup, which needs the project opened in Godot since the file last changed. Asked for by a user who needed a UID out of a binary resource and had no way to get one.
  • project run says when it ran a 3D scene with no camera: no_camera_3d: true, and a message saying that unless a script adds a Camera3D, the frame shows only the clear colour, so the run proves the scene loads and not how it looks. It checks the scene and every scene it instances, and says nothing for a headless run or a scene it cannot read. Trials 31 to 34 each had to work out why their frame was grey. Adding a camera to trial 34's level made the note disappear and the meshes appear.
  • scene compare-godot compares UIDs, and says what differs. A scene whose mesh uid had been swapped still reported matches_godot_save: true. The scene's uid and each ext_resource's uid are now compared wherever both files carry one. A reference saved from a script leaves them out, so a missing uid is not a difference. On a mismatch, difference names the first one. The descriptions now say what is compared and what is not (ext_resource ids, load_steps, sub_resources), and that the second positional and --reference are the same thing. A node's instance is now compared by the scene it names, not by its ext_resource id. Compared literally, every instanced node was a mismatch against a save to a new path, which renumbers those ids; project resave's own test caught it.
  • scene normalize reports changed: whether the save changes the file. So --dry-run answers "is this already written the way a save writes it?". A dry run also returns preview_sections, and says when a save would change only blank lines or section order. scene apply --dry-run now also runs the uid refresh a write does, so both previews show a write's exact text.
  • scene validate warns about a 3D node placed with position, rotation, scale, quaternion, basis or a global_* property (property_not_stored). Godot loads such a line but never writes one, because a 3D node stores its placement as transform (node_3d.cpp gives those properties editor-only or no storage). So the editor's next save turns the line into transform = Transform3D(...), a diff nobody asked for. The warning gives the transform form to use. Before shipping it was swept against Godot 4.8's own output: every instantiable Node3D, Node2D and Control class (218 of them), moved, rotated and scaled, then saved. Godot wrote transform for all 111 3D nodes and position for the 2D ones, and the check flagged nothing. No Node3D subclass in the class table declares its own property by any of those names.
  • scene diff reports resources: ext_resources added, removed, or changed in type or uid, keyed by path because their local ids are renumbered freely; and sub_resources, keyed by id. With --properties, a sub_resource's changed properties are listed too, addressed as SubResource("id"). Added and removed nodes carry their unique_id, and a node whose unique_id changed at the same path is reported as unique_id_changed. Trial 32 had to find an added ext_resource through scene describe.
  • Every scene and resource write takes --snapshot <path>: a copy of the file as it was before the write, for scene diff or scene restore afterwards. It is taken once per command, before anything is written, whether the command comes from the shell, a batch step, or MCP. A dry run takes none, and on scene restore, where --snapshot names the file to restore from, nothing is copied over it. scene apply --auto-snapshot now defaults to .godot/godot-cli/snapshots/<the scene's path> under the project root, where Godot imports nothing, instead of beside the scene. Trials 31 to 33 each kept a "before" copy by hand, and trial 32 left MCP to do it.
  • scene apply --dry-run returns preview_sections: the exact text of every section a write would add or change, after the same uid refresh and save preparation a write does. Trial 33 needed a patch's text before writing it, and only scene node add could show one. On a patch that set a property and added a node, both previewed sections appear verbatim in the file the real write produced.
  • scene node add --dry-run and scene instance add --dry-run return section_text: the section a write would put in the file, after the same preparation a write does, unique_id included. Trial 32 dry-ran an add to see how a property would be written and got no text back. The descriptions say the preview's unique_id is the one a write assigns, being seeded from the scene's path. Trial 33 could not tell that, and worked around it with a patch op that pins the id.

Fixed

  • Saving a scene rewrote correct UIDs on its references to binary resources (.res, .scn) whenever the project had no .godot/uid_cache.bin, which is every fresh clone, since .godot/ is gitignored. Any write with --project-root did it. In the reproduction, a set-property on an unrelated node replaced uid://bc628hhe4x5yp with uid://byggqned6p7ih and exited 0. Godot then warned invalid UID … using text path instead on every load, and the reference no longer survived the file being moved, which is what the UID is for. A binary resource keeps its UID inside its own header, and godot-cli read headers only from .tscn and .tres files. For anything else it computed a UID from the file's bytes, and those bytes contain the UID, so the hash can never equal it. scene ext add of a .res with no cache wrote a wrong UID the same way. godot-cli reads the binary header now, including the compressed form the editor saves by default, and a save repairs references the old behaviour damaged. Found while looking into the request above.
  • Rewriting a scene put a blank line between consecutive [ext_resource] lines, so even a no-op scene normalize of a file Godot saved was not byte-identical. Godot writes that block as adjacent lines and puts one blank line after it (ResourceFormatSaverTextInstance::save), so every scene with two or more ext_resources got a diff on lines nobody had touched, the first time godot-cli wrote it. No Godot-saved fixture had more than one ext_resource, which is how this went unnoticed. A new one, multi_ext_godot_saved.tscn, saved by Godot 4.8, has three. Files godot-cli wrote before lose those blank lines on their next edit, as they would on Godot's next save.
  • An added reference put uid after id, and an instanced node given a unique_id put it after instance=. Godot writes type, uid, path, id on an ext_resource and ends a node line with instance. Both now land where the editor writes them.
  • scene diff --properties said nothing about an added node's properties, and gave an instanced node an empty type. Trial 31 added a mesh with a transform and a mesh, got property_diff_count: 0, and fell back to diff -u for its answer. An added node now lists the properties it arrived with, and a removed node the properties it took with it. An instanced node reports its scene's root class when there is a project root, as scene describe does, and PackedScene when there isn't; instance_path_a or instance_path_b names the scene. `scene...
Read more

v0.25.1

Choose a tag to compare

@github-actions github-actions released this 11 Sep 09:51

Fixed

  • scene validate --project-root segfaulted on Windows whenever the project had a .godot/uid_cache.bin. Not scene-specific: every scene in every such project. The validation context held a pointer to the loaded uid cache, and that pointer was taken inside a function that then returned the struct by value — so the caller's copy kept the address of a stack frame that had been left. macOS and Linux survive it because the return slot happens to land where the local was; Windows places the copy elsewhere, and the dereference is a crash with no message. The context is filled in the caller's own storage now, so the pointer is inside the struct it belongs to by construction. Reported by a Windows user who bisected it to the uid cache themselves.

Changed

  • CI runs the test suite on Windows. It was built for and never run on, which is how a pointer bug that only manifests there reached a user. Reported rather than blocking for now, until the shell-driven smoke tests are known to behave on that runner.
  • Release binaries are built ReleaseSafe rather than ReleaseFast. Undefined behaviour now stops with a panic naming a line instead of a segfault naming nothing — the bug above cost a session of bisection to place, and a message would have placed it in one run. Measured on this workload: 3.0 ms per scene validate either way, with the binary 1% larger.

Install

curl -fsSL https://raw.githubusercontent.com/unabated-games/godot-cli/main/install.sh | bash

Installs to ~/.godot-cli (binary, scene templates, agent docs, examples,
shell completions, man page). Then:

source "$HOME/.godot-cli/env.sh"
godot-cli --version

Or download an archive below and run bin/godot-cli from it.

Verify

shasum -a 256 -c SHA256SUMS --ignore-missing
gh attestation verify godot-cli-0.25.1-<target>.tar.gz --repo unabated-games/godot-cli

v0.25.0

Choose a tag to compare

@github-actions github-actions released this 10 Sep 23:24

Fixed

  • scene validate called every Packed*Array property a type mismatch, including on files the editor wrote. Line2D.points, Polygon2D.polygon, Gradient.offsets and colors — anything typed PackedVector2Array, PackedFloat32Array and the rest. Every one of those is documented as a class, so the generated table gave them the kinds of a resource reference and validate demanded an ExtResource(...) where Godot writes PackedVector2Array(0, 0, 10, 10). A repo fixture saved by Godot itself had been failing validation this whole time. Found by sweeping 98 scenes before shipping the table below, which is the whole reason for sweeping.
  • The class table was missing 195 classes — every class that lives in an engine module or platform rather than doc/classes: the tilemap family, GridMap, the CSG nodes, the audio streams. The generator only ever scanned doc/classes, so scene validate skipped all of them in silence. It scans modules/*/doc_classes and platform/*/doc_classes now. Godot moved the tilemap classes into a module for 4.8, so regenerating without this would have dropped TileMap, TileMapLayer, TileSet and TileSetAtlasSource from the table.

Changed

  • The class table is regenerated against Godot 4.8: 520 classes to 715. Strictly additive — no class removed, no property rule made stricter, so nothing that passed before can fail now except the two fixture files that are meant to.

Added

  • scene validate reports how wide its result is: classes_checked, and unknown_classes for every type the class table does not carry, with a message naming them. A class the table lacks is skipped rather than misjudged, so a clean result read exactly like approval of it — another session had to enumerate its node types by hand to find out whether "all scenes validate" meant anything for the scenes it had. A script's class_name or an addon class is expected in that list; a core class means the table is behind the engine.
  • CI reports drift between the committed class table and the engine it tests against, from tools/check_class_table.sh. src/godot/class_table.zig carries a godot_version and nothing read it, so the table backing every scene validate property and signal check could fall behind the engine in silence. It has: against Godot 4.8-dev4 the engine's own class reference carries classes the table does not. Reported, never blocking — regenerating changes what scene validate enforces, so it stays a deliberate change. Prompted by another session finding the same shape in their own repo: a content-hash pin nothing verified.

Fixed

  • An intermittent test-suite failure, roughly one run in twenty, that had gone unexplained for two days. One CLI smoke test took its temp path from mktemp /tmp/godot_cli_scene_XXXXXX.tscn. BSD mktemp does not substitute when a suffix follows the Xs: it returns that literal path, and a second concurrent call fails with File exists. The two test binaries run in parallel, so the step raced with itself and passed on every retry. It uses a temp directory now.

Install

curl -fsSL https://raw.githubusercontent.com/unabated-games/godot-cli/main/install.sh | bash

Installs to ~/.godot-cli (binary, scene templates, agent docs, examples,
shell completions, man page). Then:

source "$HOME/.godot-cli/env.sh"
godot-cli --version

Or download an archive below and run bin/godot-cli from it.

Verify

shasum -a 256 -c SHA256SUMS --ignore-missing
gh attestation verify godot-cli-0.25.0-<target>.tar.gz --repo unabated-games/godot-cli

v0.24.0

Choose a tag to compare

@github-actions github-actions released this 10 Sep 20:27

Fixed

  • A scene transition inside project run left the old screen on top of the new one. SceneTree.change_scene_to_file removes current_scene and nothing else, and the generated driver added the scene with root.add_child without ever setting current_scene — so the old screen stayed in the tree and both drew at once, for the whole rest of the run. Every --click- or --type-driven transition looked broken. Found while checking a trial's claim that the overlap was a one-frame capture artifact; it was still there twenty frames later.

Added

  • project run --type '<node-path>@<frame>=<text>' fills a form, so the screen behind a sign-in can be watched rather than code-reviewed. --press sends input actions and --click clicks a node; neither could type, which left every form-gated transition undrivable. The field is focused and emptied, then the text goes in as real key events, so text_changed fires and a validating form runs the way it does for a person — assigning LineEdit.text emits nothing, which is why this does not do that. Works headless. Requested by another agent's session with a sign-in, a create-account and three password-reset screens it could reach and screenshot but not drive.
  • project run --focus '<node-path>@<frame>' moves keyboard focus without typing, for a frame that shows a focus ring or proves a tab order.
  • The frame number is read from between the last @ of the node path and the first = of the text, so someone@example.com and a password containing = both survive. An empty value clears the field. Typing into anything but a LineEdit or TextEdit fails the run naming the class it found, and a field that will not take focus fails rather than sending the keys to whatever was focused before.

Install

curl -fsSL https://raw.githubusercontent.com/unabated-games/godot-cli/main/install.sh | bash

Installs to ~/.godot-cli (binary, scene templates, agent docs, examples,
shell completions, man page). Then:

source "$HOME/.godot-cli/env.sh"
godot-cli --version

Or download an archive below and run bin/godot-cli from it.

Verify

shasum -a 256 -c SHA256SUMS --ignore-missing
gh attestation verify godot-cli-0.24.0-<target>.tar.gz --repo unabated-games/godot-cli

v0.23.3

Choose a tag to compare

@github-actions github-actions released this 10 Sep 17:48

Fixed

  • uid cache list and uid cache lookup answered a damaged cache with the bare word Corrupt. 0.23.2 stopped scene validate failing on it, but left the two commands someone reaches for to diagnose that saying nothing: no path, no cause, no remedy. They now fail with uid_cache_unreadable, naming .godot/uid_cache.bin and how to rebuild it. Failing is still right there — the cache is what those commands are about — but the answer has to say which file. Prompted by the reporting session's observation that the original failure named the file you passed, "which is exactly where the problem is not".

Install

curl -fsSL https://raw.githubusercontent.com/unabated-games/godot-cli/main/install.sh | bash

Installs to ~/.godot-cli (binary, scene templates, agent docs, examples,
shell completions, man page). Then:

source "$HOME/.godot-cli/env.sh"
godot-cli --version

Or download an archive below and run bin/godot-cli from it.

Verify

shasum -a 256 -c SHA256SUMS --ignore-missing
gh attestation verify godot-cli-0.23.3-<target>.tar.gz --repo unabated-games/godot-cli

v0.23.2

Choose a tag to compare

@github-actions github-actions released this 10 Sep 17:37

Fixed

  • A gap in the ext_resource numbering closed a scene to further additions. The generated id took the count of ext_resources plus one, which is only free while the numbering is dense. Remove 2_abc12 from a file holding 1..4 and every later scene ext add, scene instance add, assign_ext and instance_add proposed 4_abc12 — already taken — and failed with DuplicateResourceId. Neither --no-id-session nor scene normalize offered a way back. The id is now allocated past the highest index present and stepped forward until free, in one function both the command and the patch paths call. Reported by another agent's session, which had to abandon a scene node and instantiate from code instead — the exact shape this tool exists to avoid.
  • scene validate reported Corrupt on scenes that were not corrupt. The word came from the project's .godot/uid_cache.bin, not the file being validated: an unreadable cache failed the whole command with {"kind": "command_failed", "message": "Corrupt"} on a scene validate <file> call, while scene describe and Godot itself read the same scene without complaint. The cache belongs to the project and feeds one check, so an unreadable one now costs stale_uid_for_path and says so in messages, naming the cache path and how to rebuild it. catalog validate already tolerated this; the two paths disagreed.
  • An unknown option on a command that has subcommands and no options of its own answered this command takes and stopped. It names the subcommands now. Introduced in 0.22.0 by the message that fixed the previous version of this problem.

Notes

  • scene normalize still does not renumber ids to close a gap, and should not. Godot renumbers only as a side effect of rebuilding a file from a live scene tree, where it also discards every resource nothing references — checked against 4.8-dev4, which dropped two unreferenced ext_resource lines and renumbered the third. Reproducing half of that would write a file Godot would not have written; reproducing all of it would silently delete resources. Allocating past the highest index removes the need.

Install

curl -fsSL https://raw.githubusercontent.com/unabated-games/godot-cli/main/install.sh | bash

Installs to ~/.godot-cli (binary, scene templates, agent docs, examples,
shell completions, man page). Then:

source "$HOME/.godot-cli/env.sh"
godot-cli --version

Or download an archive below and run bin/godot-cli from it.

Verify

shasum -a 256 -c SHA256SUMS --ignore-missing
gh attestation verify godot-cli-0.23.2-<target>.tar.gz --repo unabated-games/godot-cli

v0.23.1

Choose a tag to compare

@github-actions github-actions released this 10 Sep 10:40

Fixed

  • A component with no script read as a component whose script could not be parsed. catalog show reported exports_source: "gdscript_heuristic" and script_parse_complete: false for a scene carrying no script at all — the same answer a script the parser choked on would give — so a correct empty export list was indistinguishable from a failure to read one. There are three states now: gdscript_heuristic/true (a script was read), none/true (there is no script, so nothing to export), and none/false (a script is named and could not be read, which is the only one worth acting on). Found by another agent's session reasoning around it correctly on a bare LinkButton entry.
  • script_parse_complete meant "a script interface was obtained", not what its name and the documentation said. The heuristic parser's own parse_complete was hardcoded true and never read, so no value of the field ever indicated a partial parse; the dead flag is gone and the documented meaning now matches the behaviour.

Install

curl -fsSL https://raw.githubusercontent.com/unabated-games/godot-cli/main/install.sh | bash

Installs to ~/.godot-cli (binary, scene templates, agent docs, examples,
shell completions, man page). Then:

source "$HOME/.godot-cli/env.sh"
godot-cli --version

Or download an archive below and run bin/godot-cli from it.

Verify

shasum -a 256 -c SHA256SUMS --ignore-missing
gh attestation verify godot-cli-0.23.1-<target>.tar.gz --repo unabated-games/godot-cli

v0.23.0

Choose a tag to compare

@github-actions github-actions released this 10 Sep 10:30

Added

  • Every manifest field the catalog reads can be written by catalog add. export_root_script, function_docs and prefer_over_ids were read by the schema, surfaced by catalog validate, and settable only by hand-editing the manifest — in a tool whose first rule is not to hand-edit. New flags: --export-doc <property>=<meaning>, --function-doc <function>=<meaning>, --prefer-over-ids, --export-root-script. Reported by another agent's session with 12 catalogued components, six carrying root scripts with 21 exports between them.
  • @export documentation, scaffolded the way signals already were. catalog add leaves a row per export the root script declares, --export-doc fills it in, and catalog show returns each export's doc alongside its name, type and default, with doc_source saying whether a person wrote it or it is just what the parser found. The script parse gives a caller the names of what it can set; only a person can say what setting one does. The result also reports exports_scaffolded and names the rows still blank, since an undocumented export reads to the next caller exactly like one nobody needed to explain.
  • unresolved_catalog_reference: a related_ids or prefer_over_ids entry naming no component in the project and no builtin is a warning from catalog validate rather than a pointer that silently goes nowhere. Listed in the catalog design doc since the beginning and never implemented.

Changed

  • The validation guide and the skill's reference say plainly what a clean scene validate does not prove. unknown_property skips more than it checks — any node with a script, any instanced node, and every namespaced name — and a partial check that says nothing reads exactly like a complete one that found nothing. Raised by the session the check came from.
  • The components guide and the skill cover documenting exports, the four new flags, and what doc_source means. export_root_script had never been documented anywhere a user would look: it is the script to read exports and signals from when they are not on the root node's own.

Install

curl -fsSL https://raw.githubusercontent.com/unabated-games/godot-cli/main/install.sh | bash

Installs to ~/.godot-cli (binary, scene templates, agent docs, examples,
shell completions, man page). Then:

source "$HOME/.godot-cli/env.sh"
godot-cli --version

Or download an archive below and run bin/godot-cli from it.

Verify

shasum -a 256 -c SHA256SUMS --ignore-missing
gh attestation verify godot-cli-0.23.0-<target>.tar.gz --repo unabated-games/godot-cli

v0.22.0

Choose a tag to compare

@github-actions github-actions released this 09 Sep 17:51

Fixed

  • unknown_option named neither the option nor the command. {"kind": "unknown_option", "message": "unknown option", "details": null} with an empty command was the whole answer, which is close to invisible in a piped --json workflow. It now names the flag, the command it was given to, every option that command accepts, and the nearest accepted spelling when there is one — and the envelope's command is filled in, which it could not be before because the failure happened before the invocation existed. The same details reach MCP clients, where the envelope is all a client gets. Reported by another agent's session, which lost a scene: a rejected scene node reparent --to went unnoticed, and a scene node remove --recursive ran next on the subtree the reparent had not moved.
  • A missing required option answered with the bare word Usage. No usage text, no name of the option. Required options are enforced in the parser now rather than in each handler, so every command answers with the missing option, the synopsis --help would print, and the full set the command requires.

Added

  • scene validate reports a property the class does not have — unknown_property, the thing Godot keeps in the file and silently ignores, so the setting does nothing and nothing says so. A warning rather than an error, because the class table cannot see everything a property may come from: a node with a script attached or an instanced node is skipped entirely, along with namespaced names (theme_override_constants/…, metadata/…) and the properties Godot registers as internal. layout_mode and anchors_preset are in that last group, are written into every scene the editor touches, and are absent from the class reference — an earlier draft of this check called every editor-saved scene broken, which is why the exclusions are there and tested.

Install

curl -fsSL https://raw.githubusercontent.com/unabated-games/godot-cli/main/install.sh | bash

Installs to ~/.godot-cli (binary, scene templates, agent docs, examples,
shell completions, man page). Then:

source "$HOME/.godot-cli/env.sh"
godot-cli --version

Or download an archive below and run bin/godot-cli from it.

Verify

shasum -a 256 -c SHA256SUMS --ignore-missing
gh attestation verify godot-cli-0.22.0-<target>.tar.gz --repo unabated-games/godot-cli

v0.21.0

Choose a tag to compare

@github-actions github-actions released this 09 Sep 08:54

Fixed

  • --frames N did not mean N frames, so input scheduled late in a run never happened. Godot paces physics off the wall clock while --quit-after counts main-loop iterations, and only --write-movie forces the two together — which a windowed run gets and a headless one did not. A 40-frame headless run reached physics frame 18 on the machine this was found on, and fewer on a busier one, so --click …@20 was silently dropped: ok: true, errors: 0, clicks: 1, and a handler that never ran. --press move@10..30 was held for 9 frames on one run and 10 on the next instead of 21. The run now passes --fixed-fps set to the project's physics/common/physics_ticks_per_second, so one iteration is one physics step and one frame means the same thing in both modes, on any machine. Reported from another agent's session against 0.20.1.
  • --click works under --headless now, rather than being documented as not working. The headless display server reports no window size, which leaves the root viewport at 64×64 with every Control laid out in that corner — so a click computed from a real layout landed outside it. The run puts the project's viewport size back before the first click, and layout and input picking then match a windowed run: a Button centred at (960, 540) in a 1920×1080 project is pressed at (960, 540) and its pressed signal fires. This makes a button's wiring checkable on a machine with no display. What headless still cannot give you is the frame, which is what the run now says instead.
  • The claim that clicks need a window is gone from the --headless and --click help, the verification guide, the skill's troubleshooting table and the MCP cheat sheet. It was mine, and it was wrong twice over: the 64×64 viewport is fixable, and the guard that was supposed to catch a click landing outside it could not fire, because the frame it was scheduled on was never reached.

Install

curl -fsSL https://raw.githubusercontent.com/unabated-games/godot-cli/main/install.sh | bash

Installs to ~/.godot-cli (binary, scene templates, agent docs, examples,
shell completions, man page). Then:

source "$HOME/.godot-cli/env.sh"
godot-cli --version

Or download an archive below and run bin/godot-cli from it.

Verify

shasum -a 256 -c SHA256SUMS --ignore-missing
gh attestation verify godot-cli-0.21.0-<target>.tar.gz --repo unabated-games/godot-cli