Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package asdf for openSUSE:Factory checked in 
at 2026-09-22 15:52:57
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/asdf (Old)
 and      /work/SRC/openSUSE:Factory/.asdf.new.383539 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "asdf"

Tue Sep 22 15:52:57 2026 rev:10 rq:1379604 version:0.20.1

Changes:
--------
--- /work/SRC/openSUSE:Factory/asdf/asdf.changes        2026-07-07 
21:09:34.051499562 +0200
+++ /work/SRC/openSUSE:Factory/.asdf.new.383539/asdf.changes    2026-09-22 
15:54:26.755278378 +0200
@@ -1,0 +2,22 @@
+Tue Sep 22 06:31:32 UTC 2026 - Alessio Biancalana <[email protected]>
+
+- Update to version 0.20.1:
+  * chore: release 0.20.1 (#2295)
+  * fix: override urfave/cli built-in help printer for top level asdf 
-h/--help (#2328)
+  * fix: never generate a shim named "asdf" during reshim (#2307)
+  * fix: prevent incomplete installations from being shown as installed (#2303)
+  * chore: bump @types/node from 26.1.2 to 26.4.0 in /docs in the docs group 
(#2320)
+  * docs: fix the ordering of paragraphs in the versions page (#2318)
+  * docs: explain env vars for dashed tool names (#2317)
+  * chore: bump github.com/go-git/go-git/v5 from 5.19.1 to 5.19.2 (#2312)
+  * chore: bump postcss from 8.5.12 to 8.5.25 in /docs (#2310)
+  * chore: bump the docs group in /docs with 2 updates (#2309)
+  * chore: bump actions/setup-node from 6 to 7 (#2300)
+  * chore: bump actions/setup-python from 6 to 7 (#2302)
+  * chore: bump actions/setup-go from 6 to 7 (#2301)
+  * fix: Print stderr for list-all / latest-stable callbacks
+  * fix: always update local Git repsitory in git.Update even if repositories 
have diverged (#2298)
+  * chore: bump golang.org/x/crypto from 0.51.0 to 0.52.0 (#2296)
+  * fix: remove urlfave/cli built-in `-h` flag that is causing confusing 
behavior (#2294)
+
+-------------------------------------------------------------------

Old:
----
  asdf-0.20.0.obscpio

New:
----
  asdf-0.20.1.obscpio

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ asdf.spec ++++++
--- /var/tmp/diff_new_pack.0MGjRS/_old  2026-09-22 15:54:27.613313726 +0200
+++ /var/tmp/diff_new_pack.0MGjRS/_new  2026-09-22 15:54:27.614313767 +0200
@@ -17,7 +17,7 @@
 
 
 Name:           asdf
-Version:        0.20.0
+Version:        0.20.1
 Release:        0
 Summary:        Extendable version manager
 License:        MIT

++++++ _service ++++++
--- /var/tmp/diff_new_pack.0MGjRS/_old  2026-09-22 15:54:27.659315621 +0200
+++ /var/tmp/diff_new_pack.0MGjRS/_new  2026-09-22 15:54:27.664315827 +0200
@@ -3,7 +3,7 @@
     <param name="url">https://github.com/asdf-vm/asdf/</param>
     <param name="scm">git</param>
     <param name="exclude">.git</param>
-    <param name="revision">v0.20.0</param>
+    <param name="revision">v0.20.1</param>
     <param name="versionformat">@PARENT_TAG@</param>
     <param name="changesgenerate">enable</param>
     <param name="versionrewrite-pattern">v(.*)</param>

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.0MGjRS/_old  2026-09-22 15:54:27.715317928 +0200
+++ /var/tmp/diff_new_pack.0MGjRS/_new  2026-09-22 15:54:27.725318340 +0200
@@ -1,6 +1,6 @@
 <servicedata>
 <service name="tar_scm">
                 <param name="url">https://github.com/asdf-vm/asdf/</param>
-              <param 
name="changesrevision">150aaf051b3b88ac9ad73136d7e629bbdf332bd6</param></service></servicedata>
+              <param 
name="changesrevision">8fa9e668c12a11fb078f7e08c2d854e38519a7a4</param></service></servicedata>
 (No newline at EOF)
 

++++++ asdf-0.20.0.obscpio -> asdf-0.20.1.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asdf-0.20.0/.release-please-manifest.json 
new/asdf-0.20.1/.release-please-manifest.json
--- old/asdf-0.20.0/.release-please-manifest.json       2026-07-07 
16:04:51.000000000 +0200
+++ new/asdf-0.20.1/.release-please-manifest.json       2026-09-22 
04:14:48.000000000 +0200
@@ -1,3 +1,3 @@
 {
-  ".": "0.20.0"
+  ".": "0.20.1"
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asdf-0.20.0/CHANGELOG.md new/asdf-0.20.1/CHANGELOG.md
--- old/asdf-0.20.0/CHANGELOG.md        2026-07-07 16:04:51.000000000 +0200
+++ new/asdf-0.20.1/CHANGELOG.md        2026-09-22 04:14:48.000000000 +0200
@@ -1,5 +1,17 @@
 # Changelog
 
+## [0.20.1](https://github.com/asdf-vm/asdf/compare/v0.20.0...v0.20.1) 
(2026-09-22)
+
+
+### Bug Fixes
+
+* always update local Git repsitory in git.Update even if repositories have 
diverged ([#2298](https://github.com/asdf-vm/asdf/issues/2298)) 
([b684098](https://github.com/asdf-vm/asdf/commit/b684098826642883560f31bd755568d537add933))
+* never generate a shim named "asdf" during reshim 
([#2307](https://github.com/asdf-vm/asdf/issues/2307)) 
([ca98e44](https://github.com/asdf-vm/asdf/commit/ca98e44ff49cb0a38966b23b42db962203478b59))
+* override urfave/cli built-in help printer for top level asdf -h/--help 
([#2328](https://github.com/asdf-vm/asdf/issues/2328)) 
([10df6e9](https://github.com/asdf-vm/asdf/commit/10df6e9f5fc7cb4b477bbfa32a5f5a187210f8da))
+* prevent incomplete installations from being shown as installed 
([#2303](https://github.com/asdf-vm/asdf/issues/2303)) 
([419e2da](https://github.com/asdf-vm/asdf/commit/419e2da7d7785d42d621f1c4fa93b7793cd18168))
+* Print stderr for list-all / latest-stable callbacks 
([c1e632e](https://github.com/asdf-vm/asdf/commit/c1e632e937dd0794e9809081fef55f0ab359c21e))
+* remove urlfave/cli built-in `-h` flag that is causing confusing behavior 
([#2294](https://github.com/asdf-vm/asdf/issues/2294)) 
([017a770](https://github.com/asdf-vm/asdf/commit/017a770b046f0edc687f1d67ea80f211c88d5ab4))
+
 ## [0.20.0](https://github.com/asdf-vm/asdf/compare/v0.19.0...v0.20.0) 
(2026-07-07)
 
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asdf-0.20.0/CONTRIBUTING.md 
new/asdf-0.20.1/CONTRIBUTING.md
--- old/asdf-0.20.0/CONTRIBUTING.md     2026-07-07 16:04:51.000000000 +0200
+++ new/asdf-0.20.1/CONTRIBUTING.md     2026-09-22 04:14:48.000000000 +0200
@@ -18,6 +18,10 @@
 
 See [docs/contribute/core.md](docs/contribute/core.md) or on our [Docs 
Site](https://asdf-vm.com/contribute/core.html).
 
+### Architectural Decision Records
+
+Architectural Decision Records (ADRs) are stored in `docs/adr/`. These 
documents capture important architectural decisions made in the project and are 
useful for understanding the rationale behind past design choices.
+
 ## Documentation
 
 Documentation can always be improved! See 
[docs/contribute/documentation.md](docs/contribute/documentation.md) or on our 
[Docs Site](https://asdf-vm.com/contribute/documentation.html).
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asdf-0.20.0/SECURITY.md new/asdf-0.20.1/SECURITY.md
--- old/asdf-0.20.0/SECURITY.md 2026-07-07 16:04:51.000000000 +0200
+++ new/asdf-0.20.1/SECURITY.md 2026-09-22 04:14:48.000000000 +0200
@@ -9,7 +9,7 @@
 <!-- x-release-please-start-version -->
 
 ```
-0.20.0
+0.20.1
 ```
 
 <!-- x-release-please-end -->
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asdf-0.20.0/cmd/asdf/main.go 
new/asdf-0.20.1/cmd/asdf/main.go
--- old/asdf-0.20.0/cmd/asdf/main.go    2026-07-07 16:04:51.000000000 +0200
+++ new/asdf-0.20.1/cmd/asdf/main.go    2026-09-22 04:14:48.000000000 +0200
@@ -10,7 +10,7 @@
 )
 
 // Do not touch this next line
-var version = "0.20.0" // x-release-please-version
+var version = "0.20.1" // x-release-please-version
 
 // Placeholder for the real code
 func main() {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asdf-0.20.0/cmd/asdf/main_test.go 
new/asdf-0.20.1/cmd/asdf/main_test.go
--- old/asdf-0.20.0/cmd/asdf/main_test.go       2026-07-07 16:04:51.000000000 
+0200
+++ new/asdf-0.20.1/cmd/asdf/main_test.go       2026-09-22 04:14:48.000000000 
+0200
@@ -20,7 +20,10 @@
 
        // Run tests with the asdf binary in the temp directory
 
-       // Uncomment these as they are implemented
+       t.Run("set_command", func(t *testing.T) {
+               runBatsFile(t, dir, "set_command.bats")
+       })
+
        t.Run("current_command", func(t *testing.T) {
                runBatsFile(t, dir, "current_command.bats")
        })
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asdf-0.20.0/docs/.vitepress/config.ts 
new/asdf-0.20.1/docs/.vitepress/config.ts
--- old/asdf-0.20.0/docs/.vitepress/config.ts   2026-07-07 16:04:51.000000000 
+0200
+++ new/asdf-0.20.1/docs/.vitepress/config.ts   2026-09-22 04:14:48.000000000 
+0200
@@ -7,6 +7,7 @@
   title: "asdf",
   description: "Manage multiple runtime versions with a single CLI tool",
   lastUpdated: true,
+  srcExclude: ['adr/**'],
   locales: {
     root: {
       label: "English",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/asdf-0.20.0/docs/adr/001-incomplete-install-marker.md 
new/asdf-0.20.1/docs/adr/001-incomplete-install-marker.md
--- old/asdf-0.20.0/docs/adr/001-incomplete-install-marker.md   1970-01-01 
01:00:00.000000000 +0100
+++ new/asdf-0.20.1/docs/adr/001-incomplete-install-marker.md   2026-09-22 
04:14:48.000000000 +0200
@@ -0,0 +1,48 @@
+# ADR 001: Use marker file to signal incomplete installs
+
+## Status
+
+Accepted
+
+## Context
+
+Due to the initial implementation of asdf all plugin versions must be 
installed into a subdirectory inside the asdf installs directory. When an 
installation is terminated by `SIGTERM`, `SIGKILL`, a dropped network 
connection, or loss of power, the installation directory persists, even though 
the installation itself was interrupted and is likely in an incomplete or 
broken state. When a user later runs commands that check for installed versions 
asdf incorrectly treats these incomplete installations as installed. This 
happens because asdf considers every directory inside of 
`$ASDF_DATA_DIR/installs` as a valid installation. This leads to confusing 
behavior and errors the user may be unable to trace back to the root cause.
+
+## Decision
+
+We will implement a mechanism to mark incomplete version installation 
directories using a marker file stored in a dedicated directory. The mechanism 
will be robust and prevent any type of failure from resulting in an 
installation that asdf treats as valid when it is incomplete.
+
+Here is how the install process will work:
+
+1. Before beginning an installation asdf will check if the installation 
directory already exists along with a marker file in the install-locks 
directory with the same name. If it does, both the installation directory and 
marker are removed to clean up any stale incomplete installations.
+2. asdf creates a marker file in the install-locks directory first (e.g., 
`$ASDF_DATA_DIR/install-locks/<tool>/<version>`), then creates the installation 
directory itself (e.g., `$ASDF_DATA_DIR/installs/<tool>/<version>`). Using a 
separate top-level directory ensures that:
+   - Plugins which completely replace `$ASDF_INSTALL_PATH` contents do not 
inadvertently remove the marker
+   - The install-locks directory is completely separate from the installs 
directory
+3. The plugin's download and install callbacks are invoked, along with any 
pre-download and pre-install hooks. If the install callback fails the 
installation directory is removed.
+4. When the installation is finished asdf removes the marker file.
+
+Additionally, signal handlers will be registered for `SIGINT` and `SIGTERM` 
before installation that will trigger removal of the install directory.
+
+Commands that list installed versions or check for an installed version do 
this by reading directories. Now there will be an additional check for the 
marker file in the install-locks directory for each version directory.
+
+## Consequences
+
+### Positive
+
+* Signal interruption (Ctrl+C) properly cleans up partial installations
+* No manual cleanup required for failed installations
+* Clear distinction between complete and incomplete installations
+* Users no longer encounter confusing errors from partial installations
+* Backward compatibility is maintained, all plugins will continue to work
+* Marker files survive plugins that replace `$ASDF_INSTALL_PATH` contents 
(e.g., asdf-nim)
+* Simple implementation without temporary directory staging
+
+### Negative/Neutral
+
+* `install-locks` directory will be created at the top level of 
`$ASDF_DATA_DIR`
+* Requires creating install-locks directory structure on first incomplete 
marker creation
+
+## Related Issues
+
+* https://github.com/asdf-vm/asdf/issues/2184
+* https://github.com/asdf-vm/asdf/issues/2036
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asdf-0.20.0/docs/guide/getting-started.md 
new/asdf-0.20.1/docs/guide/getting-started.md
--- old/asdf-0.20.0/docs/guide/getting-started.md       2026-07-07 
16:04:51.000000000 +0200
+++ new/asdf-0.20.1/docs/guide/getting-started.md       2026-09-22 
04:14:48.000000000 +0200
@@ -34,7 +34,7 @@
 
 <!-- x-release-please-start-version -->
 1. [Install Go](https://go.dev/doc/install)
-2. Run `go install github.com/asdf-vm/asdf/cmd/[email protected]`
+2. Run `go install github.com/asdf-vm/asdf/cmd/[email protected]`
 <!-- x-release-please-end -->
 
 ::::
@@ -48,7 +48,7 @@
 <!-- x-release-please-start-version -->
 1. Clone the asdf repository:
   ```shell
-  git clone https://github.com/asdf-vm/asdf.git --branch v0.20.0
+  git clone https://github.com/asdf-vm/asdf.git --branch v0.20.1
   ```
 <!-- x-release-please-end -->
 2. Run `make`
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asdf-0.20.0/docs/ja-jp/guide/getting-started.md 
new/asdf-0.20.1/docs/ja-jp/guide/getting-started.md
--- old/asdf-0.20.0/docs/ja-jp/guide/getting-started.md 2026-07-07 
16:04:51.000000000 +0200
+++ new/asdf-0.20.1/docs/ja-jp/guide/getting-started.md 2026-09-22 
04:14:48.000000000 +0200
@@ -33,7 +33,7 @@
 
 <!-- x-release-please-start-version -->
 1. [Goをインストールする](https://go.dev/doc/install)
-2. コマンドを実行: `go install github.com/asdf-vm/asdf/cmd/[email protected]`
+2. コマンドを実行: `go install github.com/asdf-vm/asdf/cmd/[email protected]`
 <!-- x-release-please-end -->
 
 ::::
@@ -47,7 +47,7 @@
 <!-- x-release-please-start-version -->
 1. asdfリポジトリをクローン:
   ```shell
-  git clone https://github.com/asdf-vm/asdf.git --branch v0.20.0
+  git clone https://github.com/asdf-vm/asdf.git --branch v0.20.1
   ```
 <!-- x-release-please-end -->
 2. `make`を実行。
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asdf-0.20.0/docs/ko-kr/guide/getting-started.md 
new/asdf-0.20.1/docs/ko-kr/guide/getting-started.md
--- old/asdf-0.20.0/docs/ko-kr/guide/getting-started.md 2026-07-07 
16:04:51.000000000 +0200
+++ new/asdf-0.20.1/docs/ko-kr/guide/getting-started.md 2026-09-22 
04:14:48.000000000 +0200
@@ -34,7 +34,7 @@
 
 <!-- x-release-please-start-version -->
 1. [Go 설치](https://go.dev/doc/install)
-2. `go install github.com/asdf-vm/asdf/cmd/[email protected]` 실행
+2. `go install github.com/asdf-vm/asdf/cmd/[email protected]` 실행
 <!-- x-release-please-end -->
 
 ::::
@@ -48,7 +48,7 @@
 <!-- x-release-please-start-version -->
 1. asdf 리포지토리 클론:
   ```shell
-  git clone https://github.com/asdf-vm/asdf.git --branch v0.20.0
+  git clone https://github.com/asdf-vm/asdf.git --branch v0.20.1
   ```
 <!-- x-release-please-end -->
 2. `make` 실행
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asdf-0.20.0/docs/manage/versions.md 
new/asdf-0.20.1/docs/manage/versions.md
--- old/asdf-0.20.0/docs/manage/versions.md     2026-07-07 16:04:51.000000000 
+0200
+++ new/asdf-0.20.1/docs/manage/versions.md     2026-09-22 04:14:48.000000000 
+0200
@@ -102,6 +102,10 @@
 
 Will tell asdf to use Elixir `1.18.1` in the current shell session.
 
+For tool names that contain dashes, replace each dash with an underscore when
+constructing the environment variable name. For example, the `aws-sam-cli`
+tool set `ASDF_AWS_SAM_CLI_VERSION`.
+
 :::warning
 Because this is an environment variable, it only takes effect where it is set.
 Any other shell sessions that are running will still use to whatever version is
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asdf-0.20.0/docs/package-lock.json 
new/asdf-0.20.1/docs/package-lock.json
--- old/asdf-0.20.0/docs/package-lock.json      2026-07-07 16:04:51.000000000 
+0200
+++ new/asdf-0.20.1/docs/package-lock.json      2026-09-22 04:14:48.000000000 
+0200
@@ -5,8 +5,8 @@
   "packages": {
     "": {
       "devDependencies": {
-        "@types/node": "^26.1.0",
-        "prettier": "^3.9.4",
+        "@types/node": "^26.4.0",
+        "prettier": "^3.9.6",
         "vitepress": "^1.6.4"
       }
     },
@@ -1255,9 +1255,9 @@
       "dev": true
     },
     "node_modules/@types/node": {
-      "version": "26.1.0",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.0.tgz";,
-      "integrity": 
"sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw==",
+      "version": "26.4.0",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-26.4.0.tgz";,
+      "integrity": 
"sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -1983,9 +1983,9 @@
       "license": "MIT"
     },
     "node_modules/nanoid": {
-      "version": "3.3.11",
-      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz";,
-      "integrity": 
"sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+      "version": "3.3.16",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz";,
+      "integrity": 
"sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
       "dev": true,
       "funding": [
         {
@@ -2028,9 +2028,9 @@
       "license": "ISC"
     },
     "node_modules/postcss": {
-      "version": "8.5.12",
-      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz";,
-      "integrity": 
"sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==",
+      "version": "8.5.25",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz";,
+      "integrity": 
"sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==",
       "dev": true,
       "funding": [
         {
@@ -2048,7 +2048,7 @@
       ],
       "license": "MIT",
       "dependencies": {
-        "nanoid": "^3.3.11",
+        "nanoid": "^3.3.16",
         "picocolors": "^1.1.1",
         "source-map-js": "^1.2.1"
       },
@@ -2068,9 +2068,9 @@
       }
     },
     "node_modules/prettier": {
-      "version": "3.9.4",
-      "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.4.tgz";,
-      "integrity": 
"sha512-yWG/o/4oJfo036EKAfK6ACAoDOfHeRHx4tuxkfBZiauURiaSmYwlpOr5LQqKtIkRD2z1PLteme2WoxEnj4tHTg==",
+      "version": "3.9.6",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz";,
+      "integrity": 
"sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==",
       "dev": true,
       "license": "MIT",
       "bin": {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asdf-0.20.0/docs/package.json 
new/asdf-0.20.1/docs/package.json
--- old/asdf-0.20.0/docs/package.json   2026-07-07 16:04:51.000000000 +0200
+++ new/asdf-0.20.1/docs/package.json   2026-09-22 04:14:48.000000000 +0200
@@ -7,8 +7,8 @@
     "preview": "vitepress preview"
   },
   "devDependencies": {
-    "@types/node": "^26.1.0",
-    "prettier": "^3.9.4",
+    "@types/node": "^26.4.0",
+    "prettier": "^3.9.6",
     "vitepress": "^1.6.4"
   }
 }
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asdf-0.20.0/docs/pt-br/guide/getting-started.md 
new/asdf-0.20.1/docs/pt-br/guide/getting-started.md
--- old/asdf-0.20.0/docs/pt-br/guide/getting-started.md 2026-07-07 
16:04:51.000000000 +0200
+++ new/asdf-0.20.1/docs/pt-br/guide/getting-started.md 2026-09-22 
04:14:48.000000000 +0200
@@ -39,7 +39,7 @@
 <!-- x-release-please-start-version -->
 
 ```shell
-git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch v0.20.0
+git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch v0.20.1
 ```
 
 <!-- x-release-please-end -->
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asdf-0.20.0/docs/zh-hans/guide/getting-started.md 
new/asdf-0.20.1/docs/zh-hans/guide/getting-started.md
--- old/asdf-0.20.0/docs/zh-hans/guide/getting-started.md       2026-07-07 
16:04:51.000000000 +0200
+++ new/asdf-0.20.1/docs/zh-hans/guide/getting-started.md       2026-09-22 
04:14:48.000000000 +0200
@@ -34,7 +34,7 @@
 
 <!-- x-release-please-start-version -->
 1. [安装 Go](https://go.dev/doc/install)
-2. 运行 `go install github.com/asdf-vm/asdf/cmd/[email protected]`
+2. 运行 `go install github.com/asdf-vm/asdf/cmd/[email protected]`
 <!-- x-release-please-end -->
 
 ::::
@@ -48,7 +48,7 @@
 <!-- x-release-please-start-version -->
 1. 克隆 asdf 仓库:
   ```shell
-  git clone https://github.com/asdf-vm/asdf.git --branch v0.20.0
+  git clone https://github.com/asdf-vm/asdf.git --branch v0.20.1
   ```
 <!-- x-release-please-end -->
 2. 运行 `make`
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asdf-0.20.0/go.mod new/asdf-0.20.1/go.mod
--- old/asdf-0.20.0/go.mod      2026-07-07 16:04:51.000000000 +0200
+++ new/asdf-0.20.1/go.mod      2026-09-22 04:14:48.000000000 +0200
@@ -3,13 +3,13 @@
 go 1.26.3
 
 require (
-       github.com/go-git/go-git/v5 v5.19.1
+       github.com/go-git/go-git/v5 v5.19.2
        github.com/mgechev/revive v1.7.0
        github.com/otiai10/copy v1.14.0
        github.com/rogpeppe/go-internal v1.14.1
-       github.com/stretchr/testify v1.11.1
-       github.com/urfave/cli/v3 v3.3.3
-       golang.org/x/sys v0.45.0
+       github.com/stretchr/testify v1.12.1
+       github.com/urfave/cli/v3 v3.13.0
+       golang.org/x/sys v0.46.0
        gopkg.in/ini.v1 v1.67.0
        honnef.co/go/tools v0.5.1
        mvdan.cc/gofumpt v0.7.0
@@ -23,7 +23,6 @@
        github.com/chavacava/garif v0.1.0 // indirect
        github.com/cloudflare/circl v1.6.3 // indirect
        github.com/cyphar/filepath-securejoin v0.6.1 // indirect
-       github.com/davecgh/go-spew v1.1.1 // indirect
        github.com/emirpasic/gods v1.18.1 // indirect
        github.com/fatih/color v1.18.0 // indirect
        github.com/fatih/structtag v1.2.0 // indirect
@@ -41,20 +40,19 @@
        github.com/mgechev/dots v0.0.0-20210922191527-e955255bf517 // indirect
        github.com/olekukonko/tablewriter v0.0.5 // indirect
        github.com/pjbgf/sha1cd v0.6.0 // indirect
-       github.com/pmezard/go-difflib v1.0.0 // indirect
        github.com/rivo/uniseg v0.4.7 // indirect
        github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // 
indirect
        github.com/skeema/knownhosts v1.3.1 // indirect
        github.com/spf13/afero v1.12.0 // indirect
        github.com/xanzy/ssh-agent v0.3.3 // indirect
-       golang.org/x/crypto v0.51.0 // indirect
+       go.yaml.in/yaml/v3 v3.0.5 // indirect
+       golang.org/x/crypto v0.53.0 // indirect
        golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678 // 
indirect
-       golang.org/x/mod v0.35.0 // indirect
-       golang.org/x/net v0.55.0 // indirect
-       golang.org/x/sync v0.20.0 // indirect
-       golang.org/x/text v0.37.0 // indirect
-       golang.org/x/tools v0.44.0 // indirect
+       golang.org/x/mod v0.37.0 // indirect
+       golang.org/x/net v0.56.0 // indirect
+       golang.org/x/sync v0.21.0 // indirect
+       golang.org/x/text v0.39.0 // indirect
+       golang.org/x/tools v0.47.0 // indirect
        golang.org/x/tools/go/expect v0.1.1-deprecated // indirect
        gopkg.in/warnings.v0 v0.1.2 // indirect
-       gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asdf-0.20.0/go.sum new/asdf-0.20.1/go.sum
--- old/asdf-0.20.0/go.sum      2026-07-07 16:04:51.000000000 +0200
+++ new/asdf-0.20.1/go.sum      2026-09-22 04:14:48.000000000 +0200
@@ -18,7 +18,6 @@
 github.com/cyphar/filepath-securejoin v0.6.1 
h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
 github.com/cyphar/filepath-securejoin v0.6.1/go.mod 
h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
 github.com/davecgh/go-spew v1.1.0/go.mod 
h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.1 
h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod 
h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/elazarl/goproxy v1.7.2 
h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
 github.com/elazarl/goproxy v1.7.2/go.mod 
h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
@@ -36,8 +35,8 @@
 github.com/go-git/go-billy/v5 v5.9.0/go.mod 
h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw=
 github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 
h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
 github.com/go-git/go-git-fixtures/v4 
v4.3.2-0.20231010084843-55a94097c399/go.mod 
h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
-github.com/go-git/go-git/v5 v5.19.1 
h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00=
-github.com/go-git/go-git/v5 v5.19.1/go.mod 
h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ=
+github.com/go-git/go-git/v5 v5.19.2 
h1:wkfn7vOlUBu8ivAWKBWisTiwJK4jYHzTF8Ndv1LyGqY=
+github.com/go-git/go-git/v5 v5.19.2/go.mod 
h1:QqCBE1EFN5ddFmrliLQ3/ntRCUjZU3EJuwuB/jWEHjk=
 github.com/go-quicktest/qt v1.101.0 
h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
 github.com/go-quicktest/qt v1.101.0/go.mod 
h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
 github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 
h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
@@ -82,7 +81,6 @@
 github.com/pjbgf/sha1cd v0.6.0/go.mod 
h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod 
h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pmezard/go-difflib v1.0.0 
h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod 
h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/rivo/uniseg v0.2.0/go.mod 
h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
@@ -104,26 +102,28 @@
 github.com/stretchr/testify v1.7.1/go.mod 
h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.8.0/go.mod 
h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/stretchr/testify v1.8.4/go.mod 
h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
-github.com/stretchr/testify v1.11.1 
h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
-github.com/stretchr/testify v1.11.1/go.mod 
h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
-github.com/urfave/cli/v3 v3.3.3 h1:byCBaVdIXuLPIDm5CYZRVG6NvT7tv1ECqdU4YzlEa3I=
-github.com/urfave/cli/v3 v3.3.3/go.mod 
h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
+github.com/stretchr/testify v1.12.1 
h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
+github.com/stretchr/testify v1.12.1/go.mod 
h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
+github.com/urfave/cli/v3 v3.13.0 
h1:Dr6jqMfIyyFsRVn7Nz5mqLsMY+ZMpfh3a0aMs+umPVY=
+github.com/urfave/cli/v3 v3.13.0/go.mod 
h1:vXn6HxPNccJSzQr2QvwVncOKrgYGIHU0HY5h8B2nQj4=
 github.com/xanzy/ssh-agent v0.3.3 
h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
 github.com/xanzy/ssh-agent v0.3.3/go.mod 
h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
+go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
+go.yaml.in/yaml/v3 v3.0.5/go.mod 
h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
 golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod 
h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
-golang.org/x/crypto v0.51.0/go.mod 
h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
+golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
+golang.org/x/crypto v0.53.0/go.mod 
h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
 golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f 
h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
 golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod 
h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
 golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678 
h1:1P7xPZEwZMoBoz0Yze5Nx2/4pxj6nw9ZqHWXqP0iRgQ=
 golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678/go.mod 
h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
-golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
-golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
+golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
+golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod 
h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
-golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
-golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
-golang.org/x/sync v0.20.0/go.mod 
h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
+golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
+golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
+golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
+golang.org/x/sync v0.21.0/go.mod 
h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
 golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod 
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod 
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod 
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -131,17 +131,17 @@
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod 
h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod 
h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
-golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
+golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod 
h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
-golang.org/x/term v0.43.0/go.mod 
h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
+golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
+golang.org/x/term v0.44.0/go.mod 
h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
-golang.org/x/text v0.37.0/go.mod 
h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
+golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus=
+golang.org/x/text v0.39.0/go.mod 
h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod 
h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
-golang.org/x/tools v0.44.0/go.mod 
h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
+golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
+golang.org/x/tools v0.47.0/go.mod 
h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
 golang.org/x/tools/go/expect v0.1.1-deprecated 
h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM=
 golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod 
h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod 
h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asdf-0.20.0/internal/cli/cli.go 
new/asdf-0.20.1/internal/cli/cli.go
--- old/asdf-0.20.0/internal/cli/cli.go 2026-07-07 16:04:51.000000000 +0200
+++ new/asdf-0.20.1/internal/cli/cli.go 2026-09-22 04:14:48.000000000 +0200
@@ -54,14 +54,36 @@
        log.SetFlags(0)
 
        app := &cli.Command{
-               Name:      "asdf",
-               Version:   version,
-               Copyright: "(c) 2024 Trevor Brown",
+               HideHelp:        false,
+               HideHelpCommand: true,
+               Name:            "asdf",
+               Version:         version,
+               Copyright:       "(c) 2024 Trevor Brown",
                Authors: []any{
-                       mail.Address{Name: "Trevor Brown", Address: 
"[email protected]"},
+                       mail.Address{Name: "Trevor Brown"},
                },
                Usage:     "The multiple runtime version manager",
                UsageText: usageText,
+               Action: func(_ context.Context, cmd *cli.Command) error {
+                       // If no args, show help
+                       if cmd.Args().Len() == 0 {
+                               return helpCommand(logger, version, "", "")
+                       }
+
+                       // Check if first arg matches any command - if so, let 
urfave/cli handle it
+                       firstArg := cmd.Args().Get(0)
+                       for _, c := range cmd.Commands {
+                               if c.Name == firstArg {
+                                       return nil
+                               }
+                       }
+
+                       // No matching command - manually invoke 
CommandNotFound behavior
+                       logger.Printf("invalid command provided: %s\n\n", 
firstArg)
+                       helpCommand(logger, version, "", "")
+                       cli.OsExiter(1)
+                       return cli.Exit("", 1)
+               },
                Commands: []*cli.Command{
                        {
                                Name: "cmd",
@@ -339,6 +361,17 @@
                },
        }
 
+       defaultHelpPrinter := cli.HelpPrinter
+       cli.HelpPrinter = func(w io.Writer, templ string, data interface{}) {
+               // Only override help output for the root "asdf" command. 
Subcommand
+               // help still uses urfave/cli's built-in printer.
+               if cmd, ok := data.(*cli.Command); ok && cmd == app {
+                       helpCommand(logger, version, "", "")
+                       return
+               }
+               defaultHelpPrinter(w, templ, data)
+       }
+
        err := unsetAsdfReservedEnvVars()
        if err != nil {
                cli.OsExiter(1)
@@ -1035,7 +1068,7 @@
        } else if toolVersion == "latest" || strings.HasPrefix(toolVersion, 
"latest:") {
                query := strings.TrimPrefix(toolVersion, "latest")
                query = strings.TrimPrefix(query, ":")
-               resolved, err := versions.Latest(plugin, query)
+               resolved, err := versions.Latest(plugin, query, os.Stderr)
                if err != nil {
                        failTest(l, "could not get latest version")
                }
@@ -1173,7 +1206,7 @@
        }
 
        if !all {
-               err = latestForPlugin(conf, toolName, pattern, false)
+               err = latestForPlugin(conf, toolName, pattern, false, os.Stderr)
                if err != nil {
                        cli.OsExiter(1)
                }
@@ -1190,7 +1223,7 @@
        var maybeErr error
        // loop over all plugins and show latest for each one.
        for _, plugin := range plugins {
-               maybeErr = latestForPlugin(conf, plugin.Name, "", true)
+               maybeErr = latestForPlugin(conf, plugin.Name, "", true, 
os.Stderr)
                if maybeErr != nil {
                        err = maybeErr
                }
@@ -1558,10 +1591,10 @@
        return shims.GenerateForVersion(conf, plugin, version, out, errOut)
 }
 
-func latestForPlugin(conf config.Config, toolName, pattern string, showStatus 
bool) error {
+func latestForPlugin(conf config.Config, toolName, pattern string, showStatus 
bool, errOut io.Writer) error {
        // show single plugin
        plugin := plugins.New(conf, toolName)
-       latest, err := versions.Latest(plugin, pattern)
+       latest, err := versions.Latest(plugin, pattern, errOut)
        if err != nil && err.Error() != "no latest version found" {
                fmt.Printf("unable to load latest version: %s\n", err)
                return err
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asdf-0.20.0/internal/cli/set/set.go 
new/asdf-0.20.1/internal/cli/set/set.go
--- old/asdf-0.20.0/internal/cli/set/set.go     2026-07-07 16:04:51.000000000 
+0200
+++ new/asdf-0.20.1/internal/cli/set/set.go     2026-09-22 04:14:48.000000000 
+0200
@@ -40,7 +40,7 @@
                parsedVersion := toolversions.ParseFromCliArg(version)
                if parsedVersion.Type == "latest" {
                        plugin := plugins.New(conf, args[0])
-                       resolvedVersion, err := versions.Latest(plugin, 
parsedVersion.Value)
+                       resolvedVersion, err := versions.Latest(plugin, 
parsedVersion.Value, stderr)
                        if err != nil {
                                return fmt.Errorf("unable to resolve latest 
version for %s", plugin.Name)
                        }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asdf-0.20.0/internal/data/data.go 
new/asdf-0.20.1/internal/data/data.go
--- old/asdf-0.20.0/internal/data/data.go       2026-07-07 16:04:51.000000000 
+0200
+++ new/asdf-0.20.1/internal/data/data.go       2026-09-22 04:14:48.000000000 
+0200
@@ -9,6 +9,7 @@
 const (
        dataDirDownloads = "downloads"
        dataDirInstalls  = "installs"
+       dataDirLocks     = "install-locks"
        dataDirPlugins   = "plugins"
 )
 
@@ -33,3 +34,8 @@
 func PluginDirectory(dataDir, pluginName string) string {
        return filepath.Join(dataDir, dataDirPlugins, pluginName)
 }
+
+// InstallLocksDirectory returns the directory for install marker/lock files
+func InstallLocksDirectory(dataDir, pluginName string) string {
+       return filepath.Join(dataDir, dataDirLocks, pluginName)
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asdf-0.20.0/internal/git/git.go 
new/asdf-0.20.1/internal/git/git.go
--- old/asdf-0.20.0/internal/git/git.go 2026-07-07 16:04:51.000000000 +0200
+++ new/asdf-0.20.1/internal/git/git.go 2026-09-22 04:14:48.000000000 +0200
@@ -113,7 +113,7 @@
 
        commonOpts := []string{"git", "-C", r.Directory}
 
-       refSpec := fmt.Sprintf("%s:%s", shortRef, shortRef)
+       refSpec := fmt.Sprintf("+%s:%s", shortRef, shortRef)
        cmdStr := append(commonOpts, []string{"fetch", "--prune", 
"--update-head-ok", remoteName, refSpec}...)
 
        _, stderr, err := exec(cmdStr)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asdf-0.20.0/internal/git/git_test.go 
new/asdf-0.20.1/internal/git/git_test.go
--- old/asdf-0.20.0/internal/git/git_test.go    2026-07-07 16:04:51.000000000 
+0200
+++ new/asdf-0.20.1/internal/git/git_test.go    2026-09-22 04:14:48.000000000 
+0200
@@ -212,6 +212,39 @@
                assert.Equal(t, hash, latestHash)
                assert.Equal(t, newHash, latestHash)
        })
+
+       t.Run("updates repo when remote has diverged with non-fast-forward 
changes", func(t *testing.T) {
+               remoteDir := generateRepo(t)
+               localDir := t.TempDir()
+               localRepo := NewRepo(localDir)
+
+               err := localRepo.Clone(remoteDir, "")
+               assert.Nil(t, err)
+
+               originalRemoteHash, err := getCurrentCommit(remoteDir)
+               assert.Nil(t, err)
+
+               originalLocalHash, err := getCurrentCommit(localDir)
+               assert.Nil(t, err)
+               assert.Equal(t, originalRemoteHash, originalLocalHash)
+
+               err = amendCommitInRepo(remoteDir)
+               assert.Nil(t, err)
+
+               newRemoteHash, err := getCurrentCommit(remoteDir)
+               assert.Nil(t, err)
+               assert.NotEqual(t, originalRemoteHash, newRemoteHash)
+
+               updatedToRef, oldHash, newHash, err := localRepo.Update("")
+               assert.Nil(t, err)
+               assert.Equal(t, "refs/heads/master", updatedToRef)
+               assert.Equal(t, originalLocalHash, oldHash)
+               assert.Equal(t, newRemoteHash, newHash)
+
+               currentLocalHash, err := getCurrentCommit(localDir)
+               assert.Nil(t, err)
+               assert.Equal(t, newRemoteHash, currentLocalHash)
+       })
 }
 
 func getCurrentCommit(path string) (string, error) {
@@ -253,6 +286,24 @@
        return previousHash.String(), nil
 }
 
+func amendCommitInRepo(path string) error {
+       repo, err := git.PlainOpen(path)
+       if err != nil {
+               return err
+       }
+
+       worktree, err := repo.Worktree()
+       if err != nil {
+               return err
+       }
+
+       _, err = worktree.Commit("amended commit", &git.CommitOptions{
+               Amend: true,
+       })
+
+       return err
+}
+
 func generateRepo(t *testing.T) string {
        t.Helper()
        tempDir := t.TempDir()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asdf-0.20.0/internal/installs/installs.go 
new/asdf-0.20.1/internal/installs/installs.go
--- old/asdf-0.20.0/internal/installs/installs.go       2026-07-07 
16:04:51.000000000 +0200
+++ new/asdf-0.20.1/internal/installs/installs.go       2026-09-22 
04:14:48.000000000 +0200
@@ -4,6 +4,8 @@
 package installs
 
 import (
+       "errors"
+       "fmt"
        "io/fs"
        "os"
        "path/filepath"
@@ -14,6 +16,12 @@
        "github.com/asdf-vm/asdf/internal/toolversions"
 )
 
+// IncompleteMarkerPath returns the path to the incomplete marker file for a 
version
+func IncompleteMarkerPath(conf config.Config, plugin plugins.Plugin, version 
toolversions.Version) string {
+       formattedVersion := toolversions.FormatForFS(version)
+       return filepath.Join(data.InstallLocksDirectory(conf.DataDir, 
plugin.Name), formattedVersion)
+}
+
 // Installed returns a slice of all installed versions for a given plugin
 func Installed(conf config.Config, plugin plugins.Plugin) (versions []string, 
err error) {
        installDirectory := data.InstallDirectory(conf.DataDir, plugin.Name)
@@ -31,7 +39,18 @@
                        continue
                }
 
-               versions = append(versions, 
toolversions.VersionStringFromFSFormat(file.Name()))
+               displayVersion := 
toolversions.VersionStringFromFSFormat(file.Name())
+               version := toolversions.Parse(displayVersion)
+               _, statErr := os.Stat(IncompleteMarkerPath(conf, plugin, 
version))
+               if statErr == nil {
+                       continue
+               }
+               if !errors.Is(statErr, fs.ErrNotExist) {
+                       err = fmt.Errorf("checking install marker for version 
%s: %w", displayVersion, statErr)
+                       continue
+               }
+
+               versions = append(versions, displayVersion)
        }
 
        return versions, err
@@ -59,7 +78,14 @@
 func IsInstalled(conf config.Config, plugin plugins.Plugin, version 
toolversions.Version) bool {
        installDir := InstallPath(conf, plugin, version)
 
-       // Check if version already installed
        _, err := os.Stat(installDir)
-       return !os.IsNotExist(err)
+       if err != nil {
+               return false
+       }
+
+       _, err = os.Stat(IncompleteMarkerPath(conf, plugin, version))
+       if err == nil {
+               return false
+       }
+       return errors.Is(err, fs.ErrNotExist)
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asdf-0.20.0/internal/installs/installs_test.go 
new/asdf-0.20.1/internal/installs/installs_test.go
--- old/asdf-0.20.0/internal/installs/installs_test.go  2026-07-07 
16:04:51.000000000 +0200
+++ new/asdf-0.20.1/internal/installs/installs_test.go  2026-09-22 
04:14:48.000000000 +0200
@@ -6,6 +6,7 @@
        "testing"
 
        "github.com/asdf-vm/asdf/internal/config"
+       "github.com/asdf-vm/asdf/internal/data"
        "github.com/asdf-vm/asdf/internal/installtest"
        "github.com/asdf-vm/asdf/internal/plugins"
        "github.com/asdf-vm/asdf/internal/repotest"
@@ -63,6 +64,68 @@
                assert.Nil(t, err)
                assert.Equal(t, installedVersions, []string{"1.0.0"})
        })
+
+       t.Run("filters out directories with incomplete marker", func(t 
*testing.T) {
+               mockInstall(t, conf, plugin, "1.0.0")
+               mockInstall(t, conf, plugin, "2.0.0")
+               mockInstall(t, conf, plugin, "3.0.0")
+
+               version2 := toolversions.Version{Type: "version", Value: 
"2.0.0"}
+               err := markIncomplete(conf, plugin, version2)
+               assert.Nil(t, err)
+
+               installedVersions, err := Installed(conf, plugin)
+               assert.Nil(t, err)
+               assert.Equal(t, 2, len(installedVersions))
+               assert.Contains(t, installedVersions, "1.0.0")
+               assert.Contains(t, installedVersions, "3.0.0")
+               assert.NotContains(t, installedVersions, "2.0.0")
+       })
+
+       t.Run("continues scanning and reports error when a marker lookup 
fails", func(t *testing.T) {
+               if os.Geteuid() == 0 {
+                       t.Skip("permission checks do not apply when running as 
root")
+               }
+
+               conf4, plugin4 := generateConfig(t)
+               mockInstall(t, conf4, plugin4, "1.0.0")
+               mockInstall(t, conf4, plugin4, "2.0.0")
+               mockInstall(t, conf4, plugin4, "3.0.0")
+
+               locksDir := data.InstallLocksDirectory(conf4.DataDir, 
plugin4.Name)
+               assert.Nil(t, os.MkdirAll(locksDir, 0o755))
+               assert.Nil(t, os.Chmod(locksDir, 0o000))
+               t.Cleanup(func() { _ = os.Chmod(locksDir, 0o755) })
+
+               installedVersions, err := Installed(conf4, plugin4)
+               assert.Empty(t, installedVersions)
+               assert.NotNil(t, err)
+       })
+
+       t.Run("returns all versions when none have incomplete marker", func(t 
*testing.T) {
+               conf2, plugin2 := generateConfig(t)
+               mockInstall(t, conf2, plugin2, "1.0.0")
+               mockInstall(t, conf2, plugin2, "2.0.0")
+
+               installedVersions, err := Installed(conf2, plugin2)
+               assert.Nil(t, err)
+               assert.Equal(t, 2, len(installedVersions))
+       })
+
+       t.Run("filters out ref installs with incomplete marker", func(t 
*testing.T) {
+               conf3, plugin3 := generateConfig(t)
+               refVersion := toolversions.Version{Type: "ref", Value: "foo"}
+               path := InstallPath(conf3, plugin3, refVersion)
+               err := os.MkdirAll(path, os.ModePerm)
+               assert.Nil(t, err)
+
+               err = markIncomplete(conf3, plugin3, refVersion)
+               assert.Nil(t, err)
+
+               installedVersions, err := Installed(conf3, plugin3)
+               assert.Nil(t, err)
+               assert.Empty(t, installedVersions)
+       })
 }
 
 func TestIsInstalled(t *testing.T) {
@@ -77,6 +140,16 @@
                version := toolversions.Version{Type: "version", Value: "1.0.0"}
                assert.True(t, IsInstalled(conf, plugin, version))
        })
+
+       t.Run("returns false when directory exists but has incomplete marker", 
func(t *testing.T) {
+               version := toolversions.Version{Type: "version", Value: "2.0.0"}
+               mockInstall(t, conf, plugin, "2.0.0")
+
+               err := markIncomplete(conf, plugin, version)
+               assert.Nil(t, err)
+
+               assert.False(t, IsInstalled(conf, plugin, version))
+       })
 }
 
 // helper functions
@@ -106,3 +179,17 @@
        err := installtest.InstallOneVersion(conf, plugin, "version", version)
        assert.Nil(t, err)
 }
+
+func markIncomplete(conf config.Config, plugin plugins.Plugin, version 
toolversions.Version) error {
+       markerPath := IncompleteMarkerPath(conf, plugin, version)
+       err := os.MkdirAll(filepath.Dir(markerPath), 0o755)
+       if err != nil {
+               return err
+       }
+       file, err := os.Create(markerPath)
+       if err != nil {
+               return err
+       }
+       defer file.Close()
+       return nil
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asdf-0.20.0/internal/shims/shims.go 
new/asdf-0.20.1/internal/shims/shims.go
--- old/asdf-0.20.0/internal/shims/shims.go     2026-07-07 16:04:51.000000000 
+0200
+++ new/asdf-0.20.1/internal/shims/shims.go     2026-09-22 04:14:48.000000000 
+0200
@@ -24,6 +24,13 @@
 
 const shimDirName = "shims"
 
+var reservedShimNames = map[string]bool{
+       // shims named `asdf` must never be generated, since it would shadow the
+       // same `asdf` command all asdf shims invoke under the hood.
+       // See https://github.com/asdf-vm/asdf/issues/2166
+       "asdf": true,
+}
+
 // UnknownCommandError is an error returned when a shim is not found
 type UnknownCommandError struct {
        shim string
@@ -309,12 +316,21 @@
 
 // Write generates a shim script and writes it to disk
 func Write(conf config.Config, plugin plugins.Plugin, version 
toolversions.Version, executablePath string) error {
+       shimName := filepath.Base(executablePath)
+       if reservedShimNames[shimName] {
+               // Remove any stale shim left over from before this guard 
existed.
+               shimPath := Path(conf, shimName)
+               if err := os.Remove(shimPath); err != nil && 
!os.IsNotExist(err) {
+                       return err
+               }
+               return nil
+       }
+
        err := ensureShimDirExists(conf)
        if err != nil {
                return err
        }
 
-       shimName := filepath.Base(executablePath)
        shimPath := Path(conf, shimName)
        versions := []toolversions.ToolVersions{{Name: plugin.Name, Versions: 
[]string{toolversions.Format(version)}}}
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asdf-0.20.0/internal/shims/shims_test.go 
new/asdf-0.20.1/internal/shims/shims_test.go
--- old/asdf-0.20.0/internal/shims/shims_test.go        2026-07-07 
16:04:51.000000000 +0200
+++ new/asdf-0.20.1/internal/shims/shims_test.go        2026-09-22 
04:14:48.000000000 +0200
@@ -388,6 +388,35 @@
                assert.Equal(t, want, string(content))
                os.Remove(shimPath)
        })
+
+       for reservedName := range reservedShimNames {
+               t.Run(fmt.Sprintf("never creates a shim named %s, no matter 
what executable is passed", reservedName), func(t *testing.T) {
+                       fakeExecutable := 
filepath.Join(filepath.Dir(executable), reservedName)
+                       err := Write(conf, plugin, version, fakeExecutable)
+                       assert.Nil(t, err)
+
+                       shimPath := Path(conf, reservedName)
+                       _, statErr := os.Stat(shimPath)
+                       assert.True(t, os.IsNotExist(statErr), 
fmt.Sprintf("expected no shim to be created for the reserved name %q", 
reservedName))
+               })
+       }
+
+       for reservedName := range reservedShimNames {
+               t.Run(fmt.Sprintf("removes a pre-existing shim named %s left 
over from before this guard existed", reservedName), func(t *testing.T) {
+                       shimPath := Path(conf, reservedName)
+                       err := os.WriteFile(shimPath, []byte(fmt.Sprintf("exec 
asdf exec %q \"$@\"\n", reservedName)), 0o755)
+                       assert.Nil(t, err)
+                       _, statErr := os.Stat(shimPath)
+                       assert.Nil(t, statErr, "test setup: expected the stale 
shim to exist before Write runs")
+
+                       fakeExecutable := 
filepath.Join(filepath.Dir(executable), reservedName)
+                       err = Write(conf, plugin, version, fakeExecutable)
+                       assert.Nil(t, err)
+
+                       _, statErr = os.Stat(shimPath)
+                       assert.True(t, os.IsNotExist(statErr), 
fmt.Sprintf("expected the pre-existing shims/%s to be removed", reservedName))
+               })
+       }
 }
 
 func TestToolExecutables(t *testing.T) {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asdf-0.20.0/internal/versions/versions.go 
new/asdf-0.20.1/internal/versions/versions.go
--- old/asdf-0.20.0/internal/versions/versions.go       2026-07-07 
16:04:51.000000000 +0200
+++ new/asdf-0.20.1/internal/versions/versions.go       2026-09-22 
04:14:48.000000000 +0200
@@ -6,9 +6,13 @@
        "errors"
        "fmt"
        "io"
+       "io/fs"
        "os"
+       "os/signal"
+       "path/filepath"
        "regexp"
        "strings"
+       "syscall"
 
        "github.com/asdf-vm/asdf/internal/config"
        "github.com/asdf-vm/asdf/internal/hook"
@@ -128,7 +132,7 @@
 
        resolvedVersion := ""
        if version.Type == latestVersion {
-               resolvedVersion, err = Latest(plugin, version.Value)
+               resolvedVersion, err = Latest(plugin, version.Value, stdErr)
                if err != nil {
                        return err
                }
@@ -153,13 +157,61 @@
        if version.Type == "path" {
                return UninstallableVersionError{toolName: plugin.Name, 
versionType: "path"}
        }
-       downloadDir := installs.DownloadPath(conf, plugin, version)
+
        installDir := installs.InstallPath(conf, plugin, version)
+       err = cleanupStaleIncomplete(conf, plugin, version)
+       if err != nil {
+               return fmt.Errorf("failed to cleanup stale incomplete install: 
%w", err)
+       }
 
        if installs.IsInstalled(conf, plugin, version) {
                return VersionAlreadyInstalledError{version: version, toolName: 
plugin.Name}
        }
 
+       downloadDir := installs.DownloadPath(conf, plugin, version)
+
+       installParentDir := filepath.Dir(installDir)
+       err = os.MkdirAll(installParentDir, 0o777)
+       if err != nil {
+               return fmt.Errorf("unable to create install parent dir: %w", 
err)
+       }
+
+       err = markIncomplete(conf, plugin, version)
+       if err != nil {
+               return fmt.Errorf("failed to mark installation as incomplete: 
%w", err)
+       }
+
+       err = os.MkdirAll(installDir, 0o777)
+       if err != nil {
+               return fmt.Errorf("unable to create install dir: %w", err)
+       }
+
+       // Set up signal handler to clean up on interrupt
+       sigChan := make(chan os.Signal, 1)
+       done := make(chan struct{})
+       signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
+
+       stopCleanup := func() {
+               signal.Stop(sigChan)
+               select {
+               case <-done:
+               default:
+                       close(done)
+               }
+       }
+
+       go func() {
+               select {
+               case <-sigChan:
+                       os.RemoveAll(installDir)
+                       os.Exit(1)
+               case <-done:
+                       return
+               }
+       }()
+
+       defer stopCleanup()
+
        concurrency, _ := conf.Concurrency()
        env := map[string]string{
                "ASDF_INSTALL_TYPE":    version.Type,
@@ -189,19 +241,24 @@
                return fmt.Errorf("failed to run pre-install hook: %w", err)
        }
 
-       err = os.MkdirAll(installDir, 0o777)
+       err = plugin.RunCallback("install", []string{}, env, stdOut, stdErr)
        if err != nil {
-               return fmt.Errorf("unable to create install dir: %w", err)
+               if rmErr := os.RemoveAll(installDir); rmErr != nil {
+                       fmt.Fprintf(stdErr, "failed to clean up '%s' due to 
%s\n", installDir, rmErr)
+               }
+               return fmt.Errorf("failed to run install callback: %w", err)
        }
 
-       err = plugin.RunCallback("install", []string{}, env, stdOut, stdErr)
+       err = markComplete(conf, plugin, version)
        if err != nil {
                if rmErr := os.RemoveAll(installDir); rmErr != nil {
                        fmt.Fprintf(stdErr, "failed to clean up '%s' due to 
%s\n", installDir, rmErr)
                }
-               return fmt.Errorf("failed to run install callback: %w", err)
+               return fmt.Errorf("failed to mark installation as complete: 
%w", err)
        }
 
+       stopCleanup()
+
        // Reshim
        err = shims.GenerateAll(conf, stdOut, stdErr)
        if err != nil {
@@ -235,11 +292,10 @@
 // the version it returns. If the callback is missing it invokes the list-all
 // callback and returns the last version matching the query, if a query is
 // provided.
-func Latest(plugin plugins.Plugin, query string) (version string, err error) {
+func Latest(plugin plugins.Plugin, query string, stdErr io.Writer) (version 
string, err error) {
        var stdOut strings.Builder
-       var stdErr strings.Builder
 
-       err = plugin.RunCallback("latest-stable", []string{query}, 
map[string]string{}, &stdOut, &stdErr)
+       err = plugin.RunCallback("latest-stable", []string{query}, 
map[string]string{}, &stdOut, stdErr)
        if err == nil {
                versions := parseVersions(stdOut.String())
                if len(versions) < 1 {
@@ -253,7 +309,7 @@
                return version, err
        }
 
-       allVersions, err := AllVersions(plugin)
+       allVersions, err := AllVersions(plugin, stdErr)
        if err != nil {
                return version, err
        }
@@ -276,11 +332,10 @@
 
 // AllVersions returns a slice of all available versions for the tool managed 
by
 // the given plugin by invoking the plugin's list-all callback
-func AllVersions(plugin plugins.Plugin) (versions []string, err error) {
+func AllVersions(plugin plugins.Plugin, stdErr io.Writer) (versions []string, 
err error) {
        var stdout strings.Builder
-       var stderr strings.Builder
 
-       err = plugin.RunCallback("list-all", []string{}, map[string]string{}, 
&stdout, &stderr)
+       err = plugin.RunCallback("list-all", []string{}, map[string]string{}, 
&stdout, stdErr)
        if err != nil {
                return versions, err
        }
@@ -368,3 +423,53 @@
        }
        return versions
 }
+
+// markComplete removes the incomplete marker file
+func markComplete(conf config.Config, plugin plugins.Plugin, version 
toolversions.Version) error {
+       markerPath := installs.IncompleteMarkerPath(conf, plugin, version)
+       err := os.Remove(markerPath)
+       if err != nil {
+               return fmt.Errorf("failed to remove incomplete marker: %w", err)
+       }
+       return nil
+}
+
+// markIncomplete creates an incomplete marker file
+func markIncomplete(conf config.Config, plugin plugins.Plugin, version 
toolversions.Version) error {
+       markerPath := installs.IncompleteMarkerPath(conf, plugin, version)
+       err := os.MkdirAll(filepath.Dir(markerPath), 0o755)
+       if err != nil {
+               return fmt.Errorf("failed to create marker directory: %w", err)
+       }
+       file, err := os.Create(markerPath)
+       if err != nil {
+               return fmt.Errorf("failed to create incomplete marker: %w", err)
+       }
+       defer file.Close()
+       return nil
+}
+
+// cleanupStaleIncomplete removes an install directory if it has an incomplete 
marker
+func cleanupStaleIncomplete(conf config.Config, plugin plugins.Plugin, version 
toolversions.Version) error {
+       markerPath := installs.IncompleteMarkerPath(conf, plugin, version)
+       _, err := os.Stat(markerPath)
+       if errors.Is(err, fs.ErrNotExist) {
+               return nil
+       }
+       if err != nil {
+               return fmt.Errorf("failed to check for incomplete marker: %w", 
err)
+       }
+
+       installPath := installs.InstallPath(conf, plugin, version)
+       err = os.RemoveAll(installPath)
+       if err != nil {
+               return fmt.Errorf("failed to remove stale incomplete directory: 
%w", err)
+       }
+
+       err = os.Remove(markerPath)
+       if err != nil {
+               return fmt.Errorf("failed to remove stale incomplete marker: 
%w", err)
+       }
+
+       return nil
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asdf-0.20.0/internal/versions/versions_test.go 
new/asdf-0.20.1/internal/versions/versions_test.go
--- old/asdf-0.20.0/internal/versions/versions_test.go  2026-07-07 
16:04:51.000000000 +0200
+++ new/asdf-0.20.1/internal/versions/versions_test.go  2026-09-22 
04:14:48.000000000 +0200
@@ -1,13 +1,17 @@
 package versions
 
 import (
+       "errors"
        "fmt"
+       "io"
+       "io/fs"
        "os"
        "path/filepath"
        "strings"
        "testing"
 
        "github.com/asdf-vm/asdf/internal/config"
+       "github.com/asdf-vm/asdf/internal/installs"
        "github.com/asdf-vm/asdf/internal/plugins"
        "github.com/asdf-vm/asdf/internal/repotest"
        "github.com/asdf-vm/asdf/internal/toolversions"
@@ -319,6 +323,29 @@
                // no-download install script prints 'install'
                assert.Equal(t, "install", stdout.String())
        })
+
+       t.Run("cleans up stale incomplete directory from previous failed 
install", func(t *testing.T) {
+               conf, plugin := generateConfig(t)
+               stdout, stderr := buildOutputs()
+
+               version := toolversions.Version{Type: "version", Value: "1.0.0"}
+               installPath := installs.InstallPath(conf, plugin, version)
+               err := os.MkdirAll(installPath, 0o777)
+               assert.Nil(t, err)
+
+               markerPath := installs.IncompleteMarkerPath(conf, plugin, 
version)
+               err = os.MkdirAll(filepath.Dir(markerPath), 0o755)
+               assert.Nil(t, err)
+               err = os.WriteFile(markerPath, []byte{}, 0o644)
+               assert.Nil(t, err)
+
+               err = InstallOneVersion(conf, plugin, "1.0.0", false, &stdout, 
&stderr)
+               assert.Nil(t, err)
+
+               _, err = os.Stat(markerPath)
+               assert.True(t, errors.Is(err, fs.ErrNotExist), "incomplete 
marker should not exist after successful retry")
+               assertVersionInstalled(t, conf.DataDir, plugin.Name, "1.0.0")
+       })
 }
 
 func TestLatest(t *testing.T) {
@@ -334,7 +361,7 @@
                assert.Nil(t, err)
                plugin := plugins.New(conf, pluginName)
 
-               version, err := Latest(plugin, "")
+               version, err := Latest(plugin, "", io.Discard)
                assert.Nil(t, err)
                assert.Equal(t, "2.0.0", version)
        })
@@ -350,25 +377,25 @@
                err = os.WriteFile(latestScript, []byte("#!/usr/bin/env 
bash\necho 1.2.3-dev"), 0o777)
                assert.Nil(t, err)
 
-               version, err := Latest(plugin, "")
+               version, err := Latest(plugin, "", io.Discard)
                assert.Nil(t, err)
                assert.Equal(t, "1.2.3-dev", version)
        })
 
        t.Run("when given query matching no versions return empty slice of 
versions", func(t *testing.T) {
-               version, err := Latest(plugin, "impossible-to-satisfy-query")
+               version, err := Latest(plugin, "impossible-to-satisfy-query", 
io.Discard)
                assert.Error(t, err, "no latest version found")
                assert.Equal(t, version, "")
        })
 
        t.Run("when given no query returns latest version of plugin", func(t 
*testing.T) {
-               version, err := Latest(plugin, "")
+               version, err := Latest(plugin, "", io.Discard)
                assert.Nil(t, err)
                assert.Equal(t, "5.1.0", version)
        })
 
        t.Run("when given no query returns latest version of plugin", func(t 
*testing.T) {
-               version, err := Latest(plugin, "4")
+               version, err := Latest(plugin, "4", io.Discard)
                assert.Nil(t, err)
                assert.Equal(t, "4.0.0", version)
        })
@@ -387,7 +414,7 @@
                err = os.Remove(latestScript)
                assert.Nil(t, err)
 
-               version, err := Latest(plugin, "")
+               version, err := Latest(plugin, "", io.Discard)
                assert.Nil(t, err)
                assert.Equal(t, "3.4.5", version)
        })
@@ -424,7 +451,7 @@
                assert.Nil(t, err)
 
                plugin := plugins.New(conf, pluginName)
-               version, err := Latest(plugin, "")
+               version, err := Latest(plugin, "", io.Discard)
                assert.Nil(t, err)
                assert.Equal(t, tt.expectedOutput, version)
        }
@@ -438,7 +465,7 @@
        plugin := plugins.New(conf, pluginName)
 
        t.Run("returns slice of available versions from plugin", func(t 
*testing.T) {
-               versions, err := AllVersions(plugin)
+               versions, err := AllVersions(plugin, io.Discard)
                assert.Nil(t, err)
                assert.Equal(t, versions, []string{"1.0.0", "1.1.0", "2.0.0"})
        })
@@ -449,10 +476,24 @@
                assert.Nil(t, err)
                plugin := plugins.New(conf, pluginName)
 
-               versions, err := AllVersions(plugin)
+               versions, err := AllVersions(plugin, io.Discard)
                assert.Equal(t, err.(plugins.NoCallbackError).Error(), "Plugin 
named list-all-fail does not have a callback named list-all")
                assert.Empty(t, versions)
        })
+
+       t.Run("returns error when callback fails", func(t *testing.T) {
+               pluginName = "list-all-stderr"
+               _, err := repotest.InstallPlugin("dummy_broken_plugin", 
conf.DataDir, pluginName)
+               assert.Nil(t, err)
+               plugin := plugins.New(conf, pluginName)
+
+               var stdErr strings.Builder
+
+               versions, err := AllVersions(plugin, &stdErr)
+               assert.Equal(t, "exit status 1", err.Error())
+               assert.Empty(t, versions)
+               assert.Equal(t, "List-all failed!\n", stdErr.String())
+       })
 }
 
 func TestUninstall(t *testing.T) {
@@ -577,3 +618,105 @@
        err := os.WriteFile(filepath.Join(dir, ".tool-versions"), 
[]byte(contents), 0o666)
        assert.Nil(t, err)
 }
+
+func TestCleanupStaleIncomplete(t *testing.T) {
+       conf, plugin := generateConfig(t)
+
+       t.Run("removes directory when incomplete marker exists", func(t 
*testing.T) {
+               version := toolversions.Version{Type: "version", Value: "5.0.0"}
+               installPath := installs.InstallPath(conf, plugin, version)
+               err := os.MkdirAll(installPath, 0o777)
+               assert.Nil(t, err)
+
+               err = markIncomplete(conf, plugin, version)
+               assert.Nil(t, err)
+
+               err = cleanupStaleIncomplete(conf, plugin, version)
+               assert.Nil(t, err)
+
+               _, err = os.Stat(installPath)
+               assert.True(t, errors.Is(err, fs.ErrNotExist), "directory 
should be removed")
+       })
+
+       t.Run("does nothing when directory does not exist", func(t *testing.T) {
+               version := toolversions.Version{Type: "version", Value: "6.0.0"}
+               err := cleanupStaleIncomplete(conf, plugin, version)
+               assert.Nil(t, err)
+       })
+
+       t.Run("does nothing when directory exists without incomplete marker", 
func(t *testing.T) {
+               version := toolversions.Version{Type: "version", Value: "7.0.0"}
+               installPath := installs.InstallPath(conf, plugin, version)
+               err := os.MkdirAll(installPath, 0o777)
+               assert.Nil(t, err)
+
+               err = cleanupStaleIncomplete(conf, plugin, version)
+               assert.Nil(t, err)
+
+               _, err = os.Stat(installPath)
+               assert.Nil(t, err, "directory should still exist")
+       })
+
+       t.Run("removes marker when install directory was never created", func(t 
*testing.T) {
+               version := toolversions.Version{Type: "version", Value: "8.0.0"}
+
+               err := markIncomplete(conf, plugin, version)
+               assert.Nil(t, err)
+
+               err = cleanupStaleIncomplete(conf, plugin, version)
+               assert.Nil(t, err, "should not error when install directory is 
missing")
+
+               markerPath := installs.IncompleteMarkerPath(conf, plugin, 
version)
+               _, err = os.Stat(markerPath)
+               assert.True(t, errors.Is(err, fs.ErrNotExist), "marker should 
be removed")
+       })
+}
+
+func TestMarkIncomplete(t *testing.T) {
+       conf, plugin := generateConfig(t)
+
+       t.Run("creates marker file in install-locks directory", func(t 
*testing.T) {
+               version := toolversions.Version{Type: "version", Value: "1.0.0"}
+               err := markIncomplete(conf, plugin, version)
+               assert.Nil(t, err)
+
+               markerPath := installs.IncompleteMarkerPath(conf, plugin, 
version)
+               _, err = os.Stat(markerPath)
+               assert.Nil(t, err, "marker file should exist")
+       })
+
+       t.Run("creates marker file even when directory does not exist", func(t 
*testing.T) {
+               version := toolversions.Version{Type: "version", Value: "2.0.0"}
+               err := markIncomplete(conf, plugin, version)
+               assert.Nil(t, err)
+
+               markerPath := installs.IncompleteMarkerPath(conf, plugin, 
version)
+               _, err = os.Stat(markerPath)
+               assert.Nil(t, err, "marker file should exist")
+       })
+}
+
+func TestMarkComplete(t *testing.T) {
+       conf, plugin := generateConfig(t)
+
+       t.Run("removes marker file from install-locks directory", func(t 
*testing.T) {
+               version := toolversions.Version{Type: "version", Value: "1.0.0"}
+               markerPath := installs.IncompleteMarkerPath(conf, plugin, 
version)
+               err := os.MkdirAll(filepath.Dir(markerPath), 0o755)
+               assert.Nil(t, err)
+               err = os.WriteFile(markerPath, []byte{}, 0o644)
+               assert.Nil(t, err)
+
+               err = markComplete(conf, plugin, version)
+               assert.Nil(t, err)
+
+               _, err = os.Stat(markerPath)
+               assert.True(t, errors.Is(err, fs.ErrNotExist), "marker file 
should not exist")
+       })
+
+       t.Run("returns error when marker file does not exist", func(t 
*testing.T) {
+               version := toolversions.Version{Type: "version", Value: "2.0.0"}
+               err := markComplete(conf, plugin, version)
+               assert.NotNil(t, err)
+       })
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asdf-0.20.0/test/set_command.bats 
new/asdf-0.20.1/test/set_command.bats
--- old/asdf-0.20.0/test/set_command.bats       1970-01-01 01:00:00.000000000 
+0100
+++ new/asdf-0.20.1/test/set_command.bats       2026-09-22 04:14:48.000000000 
+0200
@@ -0,0 +1,116 @@
+#!/usr/bin/env bats
+
+load test_helpers
+
+setup() {
+  setup_asdf_dir
+  install_dummy_plugin
+  install_dummy_version "1.0.0"
+  install_dummy_version "1.1.0"
+
+  PROJECT_DIR="$HOME/project"
+  mkdir -p "$PROJECT_DIR"
+
+  cd "$PROJECT_DIR" || exit
+}
+
+teardown() {
+  clean_asdf_dir
+}
+
+@test "set should emit an error when called with incorrect arity" {
+  run asdf set
+  [ "$status" -eq 1 ]
+  [ "$output" = "tool and version must be provided as arguments" ]
+}
+
+@test "set should emit an error when version is not provided" {
+  run asdf set "dummy"
+  [ "$status" -eq 1 ]
+  [ "$output" = "version must be provided as an argument" ]
+}
+
+@test "set should create .tool-versions file in current directory" {
+  run asdf set "dummy" "1.0.0"
+  [ "$status" -eq 0 ]
+  [ -f "$PROJECT_DIR/.tool-versions" ]
+  run cat "$PROJECT_DIR/.tool-versions"
+  [ "$output" = "dummy 1.0.0" ]
+}
+
+@test "set should create .tool-versions file in home directory when --home 
flag is used" {
+  run asdf set --home "dummy" "1.0.0"
+  [ "$status" -eq 0 ]
+  [ -f "$HOME/.tool-versions" ]
+  run cat "$HOME/.tool-versions"
+  [ "$output" = "dummy 1.0.0" ]
+}
+
+@test "set -u should be an alias for --home" {
+  run asdf set -u "dummy" "1.0.0"
+  [ "$status" -eq 0 ]
+  [ -f "$HOME/.tool-versions" ]
+  run cat "$HOME/.tool-versions"
+  [ "$output" = "dummy 1.0.0" ]
+}
+
+@test "set should update parent directory .tool-versions when --parent flag is 
used" {
+  echo "dummy 1.0.0" >"$PROJECT_DIR/.tool-versions"
+
+  CHILD_DIR="$PROJECT_DIR/child"
+  mkdir -p "$CHILD_DIR"
+  cd "$CHILD_DIR"
+
+  run asdf set --parent "dummy" "1.1.0"
+  [ "$status" -eq 0 ]
+  [ -f "$PROJECT_DIR/.tool-versions" ]
+  [ ! -f "$CHILD_DIR/.tool-versions" ]
+  run cat "$PROJECT_DIR/.tool-versions"
+  [ "$output" = "dummy 1.1.0" ]
+}
+
+@test "set -p should be an alias for --parent" {
+  echo "dummy 1.0.0" >"$PROJECT_DIR/.tool-versions"
+
+  CHILD_DIR="$PROJECT_DIR/child"
+  mkdir -p "$CHILD_DIR"
+  cd "$CHILD_DIR"
+
+  run asdf set -p "dummy" "1.1.0"
+  [ "$status" -eq 0 ]
+  [ -f "$PROJECT_DIR/.tool-versions" ]
+  [ ! -f "$CHILD_DIR/.tool-versions" ]
+  run cat "$PROJECT_DIR/.tool-versions"
+  [ "$output" = "dummy 1.1.0" ]
+}
+
+@test "set should emit an error when both --home and --parent flags are used" {
+  run asdf set --home --parent "dummy" "1.0.0"
+  [ "$status" -eq 1 ]
+  [ "$output" = "home and parent flags cannot both be specified; must be one 
location or the other" ]
+}
+
+@test "set should emit an error when --parent is used but no .tool-versions 
file exists in parent directory" {
+  CHILD_DIR="$PROJECT_DIR/child"
+  mkdir -p "$CHILD_DIR"
+  cd "$CHILD_DIR"
+
+  run asdf set --parent "dummy" "1.0.0"
+  [ "$status" -eq 1 ]
+  [[ "$output" == *"No .tool-versions version file found in parent directory"* 
]]
+}
+
+@test "set with -h flag should show command help" {
+  run asdf set -h
+  [ "$status" -eq 0 ]
+  [[ "$output" == *"USAGE:"* ]]
+  [[ "$output" == *"asdf set [options]"* ]]
+}
+
+@test "set should support multiple versions" {
+  run asdf set "dummy" "1.0.0" "1.1.0"
+  [ "$status" -eq 0 ]
+  [ -f "$PROJECT_DIR/.tool-versions" ]
+  run cat "$PROJECT_DIR/.tool-versions"
+  [ "$output" = "dummy 1.0.0 1.1.0" ]
+}

++++++ asdf.obsinfo ++++++
--- /var/tmp/diff_new_pack.0MGjRS/_old  2026-09-22 15:54:28.044331482 +0200
+++ /var/tmp/diff_new_pack.0MGjRS/_new  2026-09-22 15:54:28.048331647 +0200
@@ -1,5 +1,5 @@
 name: asdf
-version: 0.20.0
-mtime: 1783433091
-commit: 150aaf051b3b88ac9ad73136d7e629bbdf332bd6
+version: 0.20.1
+mtime: 1790043288
+commit: 8fa9e668c12a11fb078f7e08c2d854e38519a7a4
 

++++++ vendor.tar.gz ++++++
++++ 39240 lines of diff (skipped)

Reply via email to