Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package arkade for openSUSE:Factory checked 
in at 2026-10-20 12:37:53
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/arkade (Old)
 and      /work/SRC/openSUSE:Factory/.arkade.new.3698736 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "arkade"

Tue Oct 20 12:37:53 2026 rev:95 rq:1386150 version:0.11.132

Changes:
--------
--- /work/SRC/openSUSE:Factory/arkade/arkade.changes    2026-10-05 
13:03:14.805902851 +0200
+++ /work/SRC/openSUSE:Factory/.arkade.new.3698736/arkade.changes       
2026-10-20 12:38:44.776576063 +0200
@@ -1,0 +2,20 @@
+Mon Oct 19 05:10:07 UTC 2026 - Johannes Kastl 
<[email protected]>
+
+- Update to version 0.11.132:
+  * Install Codex with its companion package
+  * Install Crossplane from the stable chart
+- Update to version 0.11.131:
+  * Add shfmt and shellcheck to arkade get
+- Update to version 0.11.130:
+  * Stream the wanted binary out of the OCI image
+  * Pass OCI platform through to crane and add oidc-cli
+  * Disable symlink extraction for OCI images
+  * Pin sofka and run CI builds only on master
+  * Document OCI-extracted tools in AGENTS.md
+  * Render OCI tools as plain names in the markdown catalog
+  * Skip OCI-extracted tools in the URL checker
+  * Add OCI-extracted tools to arkade get
+- Update to version 0.11.129:
+  * Document arkade release as a remote-side operation
+
+-------------------------------------------------------------------

Old:
----
  arkade-0.11.128.obscpio

New:
----
  arkade-0.11.132.obscpio

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

Other differences:
------------------
++++++ arkade.spec ++++++
--- /var/tmp/diff_new_pack.K6yLkZ/_old  2026-10-20 12:38:46.193634166 +0200
+++ /var/tmp/diff_new_pack.K6yLkZ/_new  2026-10-20 12:38:46.194634207 +0200
@@ -17,7 +17,7 @@
 
 
 Name:           arkade
-Version:        0.11.128
+Version:        0.11.132
 Release:        0
 Summary:        Open Source Kubernetes Marketplace
 License:        Apache-2.0

++++++ _service ++++++
--- /var/tmp/diff_new_pack.K6yLkZ/_old  2026-10-20 12:38:46.269637282 +0200
+++ /var/tmp/diff_new_pack.K6yLkZ/_new  2026-10-20 12:38:46.274637487 +0200
@@ -3,7 +3,7 @@
     <param name="url">https://github.com/alexellis/arkade.git</param>
     <param name="scm">git</param>
     <param name="exclude">.git</param>
-    <param name="revision">refs/tags/0.11.128</param>
+    <param name="revision">refs/tags/0.11.132</param>
     <param name="versionformat">@PARENT_TAG@</param>
     <param name="versionrewrite-pattern">v(.*)</param>
     <param name="changesgenerate">enable</param>

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.K6yLkZ/_old  2026-10-20 12:38:46.297638430 +0200
+++ /var/tmp/diff_new_pack.K6yLkZ/_new  2026-10-20 12:38:46.302638635 +0200
@@ -3,6 +3,6 @@
                 <param name="url">https://github.com/alexellis/arkade</param>
               <param 
name="changesrevision">37af31c7b58de8f16a10067051e3bbe7ecb6aa79</param></service><service
 name="tar_scm">
                 <param 
name="url">https://github.com/alexellis/arkade.git</param>
-              <param 
name="changesrevision">90173cc580b31cd46ec01d807b37dc6f9b9fb55f</param></service></servicedata>
+              <param 
name="changesrevision">8b8a849ed4f9b54952f2a3b27168648f8fea31f2</param></service></servicedata>
 (No newline at EOF)
 

++++++ arkade-0.11.128.obscpio -> arkade-0.11.132.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/arkade-0.11.128/AGENTS.md 
new/arkade-0.11.132/AGENTS.md
--- old/arkade-0.11.128/AGENTS.md       2026-10-03 10:53:20.000000000 +0200
+++ new/arkade-0.11.132/AGENTS.md       2026-10-07 19:24:32.000000000 +0200
@@ -13,7 +13,7 @@
 
 - **`arkade system install`** - Linux-only system-level tools like Node.js, 
Go, Prometheus. These require additional installation steps or system 
configuration.
 
-- **`arkade oci install`** - Fetches binaries out of OCI images. Ideal for 
projects that use private repositories like slicer/actuated/k3sup-pro.
+- **`arkade oci install`** - Fetches binaries out of OCI images. Ideal for 
projects that use private repositories like slicer/actuated/k3sup-pro. Tools in 
the `arkade get` catalog may also declare an `OCIImage` field (see below), 
which makes them installable through `arkade get` as well.
 
 - **`arkade install`** - Kubernetes Helm charts or manifests for add-ons like 
OpenFaaS CE, Istio, PostgreSQL. These deploy software to Kubernetes clusters.
 
@@ -185,6 +185,45 @@
 
 ---
 
+## 1b. Adding an OCI-Extracted Tool (installable via both `arkade get` and 
`arkade oci install`)
+
+Some tools ship their binaries inside OCI images (e.g. 
`ghcr.io/openfaasltd/signet`) instead of
+GitHub release assets. These are defined **without** 
`URLTemplate`/`BinaryTemplate`, using the
+`OCIImage` field instead:
+
+```go
+Tool{
+    Name:        "signet",
+    Description: "Lightweight OpenID Connect provider for agents and 
automation",
+    OCIImage:    "ghcr.io/openfaasltd/signet",
+},
+```
+
+Key behaviour (`pkg/get/oci.go`):
+
+- `arkade get NAME` pulls the image with anonymous auth, extracts it to a temp 
dir, finds the
+  binary named `tool.Name` (`.exe` on Windows) at any depth, and installs it to
+  `$HOME/.arkade/bin/` (or `--path`).
+- Versions resolve to the registry's `latest` tag unless the tool is pinned or 
the user passes
+  `tool@tag` / `--version`. No GitHub release lookup happens.
+- Unsupported OS/arch values fail fast with a clear error before any pull 
(amd64/arm64 on
+  linux/darwin/windows only).
+- `arkade oci install NAME` continues to work via its own alias table in 
`cmd/oci/install.go`
+  (`resolveShortcutImage`) — keep both surfaces in sync when adding a tool.
+
+When adding an OCI tool:
+
+- Set only `Name`, `Description` and `OCIImage` (no `Owner`/`Repo`, they are 
unused).
+- Skip `./hack/test-tool.sh` — there is no download URL. Instead verify each 
OS/arch with
+  `arkade get NAME --os ... --arch ... --path /tmp/x` and `file` the result.
+- The URL checker (`Test_CheckTools`) skips OCI tools automatically; no test 
change needed.
+- `go run . get --format markdown` renders OCI tools as plain names (no GitHub 
link), so the
+  README table needs no extra column.
+- If the image is private, `arkade oci install` supports authenticated pulls; 
the `arkade get`
+  path is anonymous-only — document which applies in the PR.
+
+---
+
 ## 2. How to Review a New CLI Being Added as an AI Agent
 
 ### Pre-Review Checklist
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/arkade-0.11.128/README.md 
new/arkade-0.11.132/README.md
--- old/arkade-0.11.128/README.md       2026-10-03 10:53:20.000000000 +0200
+++ new/arkade-0.11.132/README.md       2026-10-07 19:24:32.000000000 +0200
@@ -192,6 +192,16 @@
 
 Files are stored at `$HOME/.arkade/bin/`
 
+`arkade get codex` installs the full upstream CLI package into
+`$HOME/.arkade/bin/.arkade-codex/`, and links `codex` and
+`codex-code-mode-host` into `$HOME/.arkade/bin/`. The package includes the
+manifest and resources needed to start Codex's app-server daemon. Keep the
+links with that directory; moving or copying an individual executable loses
+the package layout. With `--path`, the package and links are installed together
+in the specified directory. Windows hosts need Developer Mode or elevation
+to create these symbolic links. This requires a Codex release which publishes
+the `codex-package` archive for the selected platform.
+
 Want to download tools to a custom path such as into the GitHub Actions cached 
tool folder?
 
 ```bash
@@ -792,7 +802,6 @@
 ### Catalog of CLIs
 
 <!-- start of tool list -->
-
 |                                     TOOL                                     
|                                                                            
DESCRIPTION                                                                     
       |
 
|------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------|
 | [act](https://github.com/nektos/act)                                         
| Run GitHub Actions locally                                                    
                                                                                
    |
@@ -889,6 +898,7 @@
 | [k3d](https://github.com/k3d-io/k3d)                                         
| Helper to run Rancher Lab's k3s in Docker.                                    
                                                                                
    |
 | [k3s](https://github.com/k3s-io/k3s)                                         
| Lightweight Kubernetes                                                        
                                                                                
    |
 | [k3sup](https://github.com/alexellis/k3sup)                                  
| Bootstrap Kubernetes with k3s over SSH < 1 min.                               
                                                                                
    |
+| k3sup-pro                                                                    
| Extended edition of k3sup with enterprise support features                    
                                                                                
    |
 | [k6](https://github.com/grafana/k6)                                          
| Open-source, extensible performance testing tool                              
                                                                                
    |
 | [k8sgpt](https://github.com/k8sgpt-ai/k8sgpt)                                
| Kubernetes AI diagnostic tool and companion for cluster operators.            
                                                                                
    |
 | [k9s](https://github.com/derailed/k9s)                                       
| Provides a terminal UI to interact with your Kubernetes clusters.             
                                                                                
    |
@@ -920,6 +930,7 @@
 | [kubetrim](https://github.com/alexellis/kubetrim)                            
| Tidy up old Kubernetes clusters from kubeconfig.                              
                                                                                
    |
 | [kubeval](https://github.com/instrumenta/kubeval)                            
| Validate your Kubernetes configuration files, supports multiple Kubernetes 
versions                                                                        
       |
 | [kubie](https://github.com/kubie-org/kubie)                                  
| A more powerful alternative to kubectx and kubens                             
                                                                                
    |
+| kullu                                                                        
| Self-hosted file sharing and drop for teams                                   
                                                                                
    |
 | [kumactl](https://github.com/kumahq/kuma)                                    
| kumactl is a CLI to interact with Kuma and its data                           
                                                                                
    |
 | [kustomize](https://github.com/kubernetes-sigs/kustomize)                    
| Customization of kubernetes YAML configurations                               
                                                                                
    |
 | [kwok](https://github.com/kubernetes-sigs/kwok)                              
| KWOK stands for Kubernetes WithOut Kubelet, responsible for simulating the 
lifecycle of fake nodes, pods, and other Kubernetes API resources               
       |
@@ -945,6 +956,7 @@
 | [oc](https://github.com/openshift/oc)                                        
| Client to use an OpenShift 4.x cluster.                                       
                                                                                
    |
 | [oh-my-posh](https://github.com/jandedobbeleer/oh-my-posh)                   
| A prompt theme engine for any shell that can display kubernetes information.  
                                                                                
    |
 | [oha](https://github.com/hatoo/oha)                                          
| HTTP load generator inspired by rakyll/hey with a tui animation.              
                                                                                
    |
+| [oidc-cli](https://github.com/alexellis/oidc-cli)                            
| OAuth/OpenID Connect client for testing OIDC flows from the command line      
                                                                                
    |
 | [op](https://github.com/1password/)                                          
| 1Password CLI enables you to automate administrative tasks and securely 
provision secrets across development environments.                              
          |
 | [opa](https://github.com/open-policy-agent/opa)                              
| General-purpose policy engine that enables unified, context-aware policy 
enforcement across the entire stack.                                            
         |
 | [opencode](https://github.com/anomalyco/opencode)                            
| The opencode CLI for running AI agents and tools.                             
                                                                                
    |
@@ -972,7 +984,12 @@
 | [run-job](https://github.com/alexellis/run-job)                              
| Run a Kubernetes Job and get the logs when it's done.                         
                                                                                
    |
 | [scaleway-cli](https://github.com/scaleway/scaleway-cli)                     
| Scaleway CLI is a tool to help you pilot your Scaleway infrastructure 
directly from your terminal.                                                    
            |
 | [seaweedfs](https://github.com/seaweedfs/seaweedfs)                          
| SeaweedFS is a fast distributed storage system for blobs, objects, files, and 
data lake, for billions of files!                                               
    |
+| [shellcheck](https://github.com/koalaman/shellcheck)                         
| A static analysis tool for shell scripts                                      
                                                                                
    |
+| [shfmt](https://github.com/mvdan/sh)                                         
| A shell parser, formatter, and interpreter (sh/bash/mksh)                     
                                                                                
    |
+| signet                                                                       
| Lightweight OpenID Connect provider for agents and automation                 
                                                                                
    |
 | [skupper](https://github.com/skupperproject/skupper)                         
| Skupper is an implementation of a Virtual Application Network, enabling rich 
hybrid cloud communication                                                      
     |
+| slicer                                                                       
| Create and manage Firecracker microVMs                                        
                                                                                
    |
+| slicer-agent                                                                 
| Agent for managing Slicer microVMs on remote hosts                            
                                                                                
    |
 | [snowmachine](https://github.com/rgee0/snowmachine)                          
| Festive cheer for your terminal.                                              
                                                                                
    |
 | [sofka](https://github.com/nklmilojevic/sofka)                               
| A Kubernetes TUI that tells you why it's broken.                              
                                                                                
    |
 | [sops](https://github.com/getsops/sops)                                      
| Simple and flexible tool for managing secrets                                 
                                                                                
    |
@@ -980,6 +997,7 @@
 | [starship](https://github.com/starship/starship)                             
| The minimal, blazing-fast, and infinitely customizable prompt for any shell!  
                                                                                
    |
 | [step](https://github.com/smallstep/cli)                                     
| CLI for creating and managing cryptographic credentials with Smallstep.       
                                                                                
    |
 | [stern](https://github.com/stern/stern)                                      
| Multi pod and container log tailing for Kubernetes.                           
                                                                                
    |
+| superterm                                                                    
| Run and manage coding agents in hosted terminal sessions                      
                                                                                
    |
 | [syft](https://github.com/anchore/syft)                                      
| CLI tool and library for generating a Software Bill of Materials from 
container images and filesystems                                                
            |
 | [talosctl](https://github.com/siderolabs/talos)                              
| The command-line tool for managing Talos Linux OS.                            
                                                                                
    |
 | [task](https://github.com/go-task/task)                                      
| A simple task runner and build tool                                           
                                                                                
    |
@@ -994,17 +1012,19 @@
 | [timoni](https://github.com/stefanprodan/timoni)                             
| A package manager for Kubernetes powered by CUE.                              
                                                                                
    |
 | [tkn](https://github.com/tektoncd/cli)                                       
| A CLI for interacting with Tekton.                                            
                                                                                
    |
 | [tofu](https://github.com/opentofu/opentofu)                                 
| OpenTofu lets you declaratively manage your cloud infrastructure              
                                                                                
    |
+| toilgate                                                                     
| Agent CLI for Toilgate models                                                 
                                                                                
    |
 | [trivy](https://github.com/aquasecurity/trivy)                               
| Vulnerability Scanner for Containers and other Artifacts, Suitable for CI.    
                                                                                
    |
 | [vagrant](https://github.com/hashicorp/vagrant)                              
| Tool for building and distributing development environments.                  
                                                                                
    |
 | [vault](https://github.com/hashicorp/vault)                                  
| A tool for secrets management, encryption as a service, and privileged access 
management.                                                                     
    |
 | [vcluster](https://github.com/loft-sh/vcluster)                              
| Create fully functional virtual Kubernetes clusters - Each vcluster runs 
inside a namespace of the underlying k8s cluster.                               
         |
 | [vhs](https://github.com/charmbracelet/vhs)                                  
| CLI for recording demos                                                       
                                                                                
    |
 | [viddy](https://github.com/sachaos/viddy)                                    
| A modern watch command. Time machine and pager etc.                           
                                                                                
    |
+| vmmeter                                                                      
| Metering and telemetry for Slicer virtual machines                            
                                                                                
    |
 | [vzzn](https://github.com/alexellis/vzzn)                                    
| Vision/OCR client for the toilgate LLM gateway.                               
                                                                                
    |
 | [waypoint](https://github.com/hashicorp/waypoint)                            
| Easy application deployment for Kubernetes and Amazon ECS                     
                                                                                
    |
 | [websocat](https://github.com/vi/websocat)                                   
| Command-line client for WebSockets, like netcat/socat but for WebSockets      
                                                                                
    |
 | [xq](https://github.com/sibprogrammer/xq)                                    
| XML to JSON/YAML converter and query tool.                                    
                                                                                
    |
 | [yq](https://github.com/mikefarah/yq)                                        
| Portable command-line YAML processor.                                         
                                                                                
    |
 | [yt-dlp](https://github.com/yt-dlp/yt-dlp)                                   
| Fork of youtube-dl with additional features and fixes                         
                                                                                
    |
-There are 211 tools, use `arkade get NAME` to download one.
+There are 222 tools, use `arkade get NAME` to download one.
 <!-- end of tool list -->
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/arkade-0.11.128/cmd/apps/crossplane_app.go 
new/arkade-0.11.132/cmd/apps/crossplane_app.go
--- old/arkade-0.11.128/cmd/apps/crossplane_app.go      2026-10-03 
10:53:20.000000000 +0200
+++ new/arkade-0.11.132/cmd/apps/crossplane_app.go      2026-10-07 
19:24:32.000000000 +0200
@@ -81,12 +81,12 @@
                }
 
                updateRepo, _ := crossplane.Flags().GetBool("update-repo")
-               err = helm.AddHelmRepo("crossplane-alpha", 
"https://charts.crossplane.io/alpha";, updateRepo)
+               err = helm.AddHelmRepo("crossplane-stable", 
"https://charts.crossplane.io/stable";, updateRepo)
                if err != nil {
                        return err
                }
 
-               err = helm.FetchChart("crossplane-alpha/crossplane", 
defaultVersion)
+               err = helm.FetchChart("crossplane-stable/crossplane", 
defaultVersion)
                if err != nil {
                        return err
                }
@@ -96,7 +96,7 @@
                        return nsErr
                }
 
-               err = helm.Helm3Upgrade("crossplane-alpha/crossplane",
+               err = helm.Helm3Upgrade("crossplane-stable/crossplane",
                        namespace, []string{"values.yaml"}, "", values, wait)
                if err != nil {
                        return err
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/arkade-0.11.128/cmd/get.go 
new/arkade-0.11.132/cmd/get.go
--- old/arkade-0.11.128/cmd/get.go      2026-10-03 10:53:20.000000000 +0200
+++ new/arkade-0.11.132/cmd/get.go      2026-10-07 19:24:32.000000000 +0200
@@ -149,13 +149,21 @@
                signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM)
 
                arch, _ := command.Flags().GetString("arch")
-               if err := get.ValidateArch(arch); err != nil {
-                       return err
-               }
-
                operatingSystem, _ := command.Flags().GetString("os")
-               if err := get.ValidateOS(operatingSystem); err != nil {
-                       return err
+
+               // Validate OS/arch only for URL-based tools. OCI-backed tools
+               // pass the platform straight to crane, which reports a precise
+               // error when an image doesn't ship the requested platform.
+               for _, tool := range downloadURLs {
+                       if len(tool.OCIImage) == 0 {
+                               if err := get.ValidateArch(arch); err != nil {
+                                       return err
+                               }
+                               if err := get.ValidateOS(operatingSystem); err 
!= nil {
+                                       return err
+                               }
+                               break
+                       }
                }
 
                if parallel > len(downloadURLs) {
@@ -341,11 +349,12 @@
 
                // Collect successful downloads.
                var localToolsStore []get.ToolLocal
-               for _, p := range progress {
+               for i, p := range progress {
                        if p.status == stDone && len(p.path) > 0 {
                                localToolsStore = append(localToolsStore, 
get.ToolLocal{
-                                       Name: p.name,
-                                       Path: p.path,
+                                       Name:    p.name,
+                                       Path:    p.path,
+                                       Package: 
len(downloadURLs[i].PackageBinaries) > 0,
                                })
                        }
                }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/arkade-0.11.128/cmd/release.go 
new/arkade-0.11.132/cmd/release.go
--- old/arkade-0.11.128/cmd/release.go  2026-10-03 10:53:20.000000000 +0200
+++ new/arkade-0.11.132/cmd/release.go  2026-10-07 19:24:32.000000000 +0200
@@ -21,6 +21,14 @@
 incrementing the version, and using the latest commit message as the
 release title.
 
+This is a remote-side operation: the tag, visibility and release are
+resolved via "gh" against the GitHub repository. Local tags and working
+tree contents are not used, and no tags are pushed. The repository is
+selected by gh from the current checkout (or GH_REPO). The default title
+is the subject line of the latest local commit — push first so it
+matches the remote HEAD. Untracked, staged, or uncommitted changes are
+ignored.
+
 The latest release tag is obtained via "gh release list", so private
 repositories are supported when "gh" is authenticated.
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/arkade-0.11.128/hack/test-tool.sh 
new/arkade-0.11.132/hack/test-tool.sh
--- old/arkade-0.11.128/hack/test-tool.sh       2026-10-03 10:53:20.000000000 
+0200
+++ new/arkade-0.11.132/hack/test-tool.sh       2026-10-07 19:24:32.000000000 
+0200
@@ -15,13 +15,12 @@
 
 set -x
 
-./arkade get "$1" --arch arm64 --os darwin --quiet && file 
"$HOME"/.arkade/bin/"$1" && rm "$HOME"/.arkade/bin/"$1" && echo
+./arkade get "$1" --arch arm64 --os darwin --quiet && file -L 
"$HOME"/.arkade/bin/"$1" && rm "$HOME"/.arkade/bin/"$1" && echo
 
-./arkade get "$1" --arch x86_64 --os darwin --quiet && file 
"$HOME"/.arkade/bin/"$1" && rm "$HOME"/.arkade/bin/"$1" && echo
+./arkade get "$1" --arch x86_64 --os darwin --quiet && file -L 
"$HOME"/.arkade/bin/"$1" && rm "$HOME"/.arkade/bin/"$1" && echo
 
-./arkade get "$1" --arch x86_64 --os linux --quiet && file 
"$HOME"/.arkade/bin/"$1" && rm "$HOME"/.arkade/bin/"$1" && echo
+./arkade get "$1" --arch x86_64 --os linux --quiet && file -L 
"$HOME"/.arkade/bin/"$1" && rm "$HOME"/.arkade/bin/"$1" && echo
 
-./arkade get "$1" --arch aarch64 --os linux --quiet && file 
"$HOME"/.arkade/bin/"$1" && rm "$HOME"/.arkade/bin/"$1" && echo
-
-./arkade get "$1" --arch x86_64 --os mingw --quiet && file 
"$HOME"/.arkade/bin/"$1".exe && rm "$HOME"/.arkade/bin/"$1".exe && echo
+./arkade get "$1" --arch aarch64 --os linux --quiet && file -L 
"$HOME"/.arkade/bin/"$1" && rm "$HOME"/.arkade/bin/"$1" && echo
 
+./arkade get "$1" --arch x86_64 --os mingw --quiet && file -L 
"$HOME"/.arkade/bin/"$1".exe && rm "$HOME"/.arkade/bin/"$1".exe && echo
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/arkade-0.11.128/pkg/get/download.go 
new/arkade-0.11.132/pkg/get/download.go
--- old/arkade-0.11.128/pkg/get/download.go     2026-10-03 10:53:20.000000000 
+0200
+++ new/arkade-0.11.132/pkg/get/download.go     2026-10-07 19:24:32.000000000 
+0200
@@ -100,6 +100,10 @@
 
 func downloadTool(tool *Tool, arch, operatingSystem, version string, movePath 
string, displayProgress, quiet, verify bool, cb ProgressCallback) (string, 
string, error) {
 
+       if len(tool.OCIImage) > 0 {
+               return downloadFromOCI(tool, arch, operatingSystem, version, 
movePath, quiet, cb)
+       }
+
        downloadURL, resolvedVersion, err := GetDownloadURL(tool,
                strings.ToLower(operatingSystem),
                strings.ToLower(arch),
@@ -279,6 +283,12 @@
                }
        }
 
+       if len(tool.PackageBinaries) > 0 {
+               defer os.RemoveAll(filepath.Dir(outFilePath))
+               localPath, err := installPackage(tool, outFilePath, 
downloadURL, operatingSystem, movePath)
+               return localPath, resolvedVersion, err
+       }
+
        if isArchiveStr(downloadURL) {
 
                outPath, err := decompress(tool, downloadURL, outFilePath, 
operatingSystem, arch, version, quiet)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/arkade-0.11.128/pkg/get/get.go 
new/arkade-0.11.132/pkg/get/get.go
--- old/arkade-0.11.128/pkg/get/get.go  2026-10-03 10:53:20.000000000 +0200
+++ new/arkade-0.11.132/pkg/get/get.go  2026-10-07 19:24:32.000000000 +0200
@@ -111,6 +111,11 @@
        // URL.
        BinaryTemplate string
 
+       // PackageBinaries opts into preserving a tar.gz package's directory 
layout.
+       // These named executables from bin/ are linked into the install 
directory.
+       // Windows targets automatically receive the .exe suffix.
+       PackageBinaries []string
+
        // NoExtension is required for tooling such as kubectx
        // which at time of writing is a bash script.
        NoExtension bool
@@ -118,6 +123,12 @@
        VerifyTemplate string
 
        VerifyStrategy string
+
+       // OCIImage, when set, means the binary is extracted from this
+       // OCI image (e.g. "ghcr.io/openfaasltd/signet") rather than
+       // downloaded from a GitHub release. The image must ship the
+       // binary for each supported OS/architecture.
+       OCIImage string
 }
 
 type ReleaseLocation struct {
@@ -152,6 +163,8 @@
 type ToolLocal struct {
        Name string
        Path string
+       // Package means the entry point requires its adjacent package 
directory.
+       Package bool
 }
 
 var templateFuncs = map[string]interface{}{
@@ -191,6 +204,12 @@
                return ver, nil
        }
 
+       // OCI-backed tools ship a "latest" tag, so there is no version
+       // lookup unless the tool is pinned or a version was given.
+       if len(tool.OCIImage) > 0 {
+               return "latest", nil
+       }
+
        var releaseType string
        if len(tool.URLTemplate) == 0 ||
                strings.Contains(tool.URLTemplate, "https://github.com/";) {
@@ -579,7 +598,27 @@
 func PostInstallationMsg(movePath string, localToolsStore []ToolLocal) 
([]byte, error) {
 
        var buf bytes.Buffer
-       multi := len(localToolsStore) > 1
+       for _, tl := range localToolsStore {
+               if tl.Package {
+                       if movePath == "" && !ArkadeInPath() {
+                               buf.WriteString("# Add arkade binary directory 
to your PATH variable\n")
+                               buf.WriteString(pathExportInstructions())
+                               buf.WriteString("\n")
+                       }
+                       buf.WriteString("# Keep package entry points with their 
adjacent .arkade-NAME directories.\n")
+                       buf.WriteString("# Install to system (optional):\n")
+                       for _, tool := range localToolsStore {
+                               if tool.Package {
+                                       fmt.Fprintf(&buf, "sudo arkade get %s 
--path /usr/local/bin\n", tool.Name)
+                               } else if movePath == "" {
+                                       fmt.Fprintf(&buf, "sudo mv %s 
/usr/local/bin/\n", tool.Path)
+                               } else {
+                                       fmt.Fprintf(&buf, "sudo install -m 755 
%s /usr/local/bin/%s\n", tool.Path, tool.Name)
+                               }
+                       }
+                       return bytes.TrimRight(buf.Bytes(), "\n"), nil
+               }
+       }
 
        if movePath != "" {
                buf.WriteString("# Install to system (optional):\n")
@@ -587,27 +626,17 @@
                        fmt.Fprintf(&buf, "sudo install -m 755 %s 
/usr/local/bin/%s\n", tl.Path, tl.Name)
                }
        } else if ArkadeInPath() {
-               if multi {
-                       buf.WriteString("# Install to system (optional):\n")
-                       buf.WriteString("sudo mv $HOME/.arkade/bin/* 
/usr/local/bin/\n")
-               } else {
-                       buf.WriteString("# Install to system (optional):\n")
-                       for _, tl := range localToolsStore {
-                               fmt.Fprintf(&buf, "sudo mv %s 
/usr/local/bin/\n", tl.Path)
-                       }
+               buf.WriteString("# Install to system (optional):\n")
+               for _, tl := range localToolsStore {
+                       fmt.Fprintf(&buf, "sudo mv %s /usr/local/bin/\n", 
tl.Path)
                }
        } else {
                buf.WriteString("# Add arkade binary directory to your PATH 
variable\n")
                buf.WriteString(pathExportInstructions())
                buf.WriteString("\n")
-               if multi {
-                       buf.WriteString("# Install to system (optional):\n")
-                       buf.WriteString("sudo mv $HOME/.arkade/bin/* 
/usr/local/bin/\n")
-               } else {
-                       buf.WriteString("# Install to system (optional):\n")
-                       for _, tl := range localToolsStore {
-                               fmt.Fprintf(&buf, "sudo mv %s 
/usr/local/bin/\n", tl.Path)
-                       }
+               buf.WriteString("# Install to system (optional):\n")
+               for _, tl := range localToolsStore {
+                       fmt.Fprintf(&buf, "sudo mv %s /usr/local/bin/\n", 
tl.Path)
                }
        }
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/arkade-0.11.128/pkg/get/get_test.go 
new/arkade-0.11.132/pkg/get/get_test.go
--- old/arkade-0.11.128/pkg/get/get_test.go     2026-10-03 10:53:20.000000000 
+0200
+++ new/arkade-0.11.132/pkg/get/get_test.go     2026-10-07 19:24:32.000000000 
+0200
@@ -602,7 +602,8 @@
 export PATH="$HOME/.arkade/bin:$PATH"
 
 # Install to system (optional):
-sudo mv $HOME/.arkade/bin/* /usr/local/bin/`,
+sudo mv /home/user/.arkade/bin/yq /usr/local/bin/
+sudo mv /home/user/.arkade/bin/jq /usr/local/bin/`,
                },
                {
                        name:               "default path, arkade not in PATH, 
single tool",
@@ -633,7 +634,8 @@
                                        Path: "/home/user/.arkade/bin/jq",
                                }},
                        want: `# Install to system (optional):
-sudo mv $HOME/.arkade/bin/* /usr/local/bin/`,
+sudo mv /home/user/.arkade/bin/yq /usr/local/bin/
+sudo mv /home/user/.arkade/bin/jq /usr/local/bin/`,
                },
                {
                        name:               "default path, arkade already in 
PATH, single tool",
@@ -3559,7 +3561,7 @@
 func Test_DownloadCodex(t *testing.T) {
        tools := MakeTools()
        name := "codex"
-       const version = "rust-v0.89.0"
+       const version = "rust-v0.160.1"
 
        tool := getTool(name, tools)
 
@@ -3568,37 +3570,37 @@
                        os:      "ming",
                        arch:    arch64bit,
                        version: version,
-                       url:     
`https://github.com/openai/codex/releases/download/rust-v0.89.0/codex-x86_64-pc-windows-msvc.exe.zip`,
+                       url:     
`https://github.com/openai/codex/releases/download/rust-v0.160.1/codex-package-x86_64-pc-windows-msvc.tar.gz`,
                },
                {
                        os:      "ming",
                        arch:    archARM64,
                        version: version,
-                       url:     
`https://github.com/openai/codex/releases/download/rust-v0.89.0/codex-aarch64-pc-windows-msvc.exe.zip`,
+                       url:     
`https://github.com/openai/codex/releases/download/rust-v0.160.1/codex-package-aarch64-pc-windows-msvc.tar.gz`,
                },
                {
                        os:      "linux",
                        arch:    arch64bit,
                        version: version,
-                       url:     
`https://github.com/openai/codex/releases/download/rust-v0.89.0/codex-x86_64-unknown-linux-musl.tar.gz`,
+                       url:     
`https://github.com/openai/codex/releases/download/rust-v0.160.1/codex-package-x86_64-unknown-linux-musl.tar.gz`,
                },
                {
                        os:      "linux",
                        arch:    archARM64,
                        version: version,
-                       url:     
`https://github.com/openai/codex/releases/download/rust-v0.89.0/codex-aarch64-unknown-linux-musl.tar.gz`,
+                       url:     
`https://github.com/openai/codex/releases/download/rust-v0.160.1/codex-package-aarch64-unknown-linux-musl.tar.gz`,
                },
                {
                        os:      "darwin",
                        arch:    arch64bit,
                        version: version,
-                       url:     
`https://github.com/openai/codex/releases/download/rust-v0.89.0/codex-x86_64-apple-darwin.tar.gz`,
+                       url:     
`https://github.com/openai/codex/releases/download/rust-v0.160.1/codex-package-x86_64-apple-darwin.tar.gz`,
                },
                {
                        os:      "darwin",
                        arch:    archDarwinARM64,
                        version: version,
-                       url:     
`https://github.com/openai/codex/releases/download/rust-v0.89.0/codex-aarch64-apple-darwin.tar.gz`,
+                       url:     
`https://github.com/openai/codex/releases/download/rust-v0.160.1/codex-package-aarch64-apple-darwin.tar.gz`,
                },
        }
 
@@ -10992,6 +10994,180 @@
                        got, _, err := tool.GetURL(tc.os, tc.arch, tc.version, 
false)
                        if err != nil {
                                t.Fatal(err)
+                       }
+                       if got != tc.url {
+                               t.Errorf("\nwant: %s\ngot:  %s", tc.url, got)
+                       }
+               })
+       }
+}
+
+func Test_DownloadOidcCli(t *testing.T) {
+       tools := MakeTools()
+       name := "oidc-cli"
+
+       tool := getTool(name, tools)
+
+       const toolVersion = "v0.0.1"
+
+       tests := []test{
+               {
+                       os:      "linux",
+                       arch:    arch64bit,
+                       version: toolVersion,
+                       url:     
"https://github.com/alexellis/oidc-cli/releases/download/v0.0.1/oidc-cli";,
+               },
+               {
+                       os:      "linux",
+                       arch:    archARM64,
+                       version: toolVersion,
+                       url:     
"https://github.com/alexellis/oidc-cli/releases/download/v0.0.1/oidc-cli-arm64";,
+               },
+               {
+                       os:      "linux",
+                       arch:    "armv7l",
+                       version: toolVersion,
+                       url:     
"https://github.com/alexellis/oidc-cli/releases/download/v0.0.1/oidc-cli-armhf";,
+               },
+               {
+                       os:      "darwin",
+                       arch:    arch64bit,
+                       version: toolVersion,
+                       url:     
"https://github.com/alexellis/oidc-cli/releases/download/v0.0.1/oidc-cli-darwin";,
+               },
+               {
+                       os:      "darwin",
+                       arch:    archDarwinARM64,
+                       version: toolVersion,
+                       url:     
"https://github.com/alexellis/oidc-cli/releases/download/v0.0.1/oidc-cli-darwin-arm64";,
+               },
+               {
+                       os:      "ming",
+                       arch:    arch64bit,
+                       version: toolVersion,
+                       url:     
"https://github.com/alexellis/oidc-cli/releases/download/v0.0.1/oidc-cli.exe";,
+               },
+       }
+
+       for _, tc := range tests {
+               t.Run(tc.os+" "+tc.arch, func(t *testing.T) {
+                       got, _, err := tool.GetURL(tc.os, tc.arch, tc.version, 
false)
+                       if err != nil {
+                               t.Fatal(err)
+                       }
+                       if got != tc.url {
+                               t.Errorf("\nwant: %s\ngot:  %s", tc.url, got)
+                       }
+               })
+       }
+}
+
+func Test_DownloadShfmt(t *testing.T) {
+       tools := MakeTools()
+       name := "shfmt"
+
+       tool := getTool(name, tools)
+
+       const toolVersion = "v3.13.1"
+
+       tests := []test{
+               {
+                       os:      "linux",
+                       arch:    arch64bit,
+                       version: toolVersion,
+                       url:     
"https://github.com/mvdan/sh/releases/download/v3.13.1/shfmt_v3.13.1_linux_amd64";,
+               },
+               {
+                       os:      "linux",
+                       arch:    archARM64,
+                       version: toolVersion,
+                       url:     
"https://github.com/mvdan/sh/releases/download/v3.13.1/shfmt_v3.13.1_linux_arm64";,
+               },
+               {
+                       os:      "linux",
+                       arch:    "armv7l",
+                       version: toolVersion,
+                       url:     
"https://github.com/mvdan/sh/releases/download/v3.13.1/shfmt_v3.13.1_linux_arm";,
+               },
+               {
+                       os:      "darwin",
+                       arch:    arch64bit,
+                       version: toolVersion,
+                       url:     
"https://github.com/mvdan/sh/releases/download/v3.13.1/shfmt_v3.13.1_darwin_amd64";,
+               },
+               {
+                       os:      "darwin",
+                       arch:    archDarwinARM64,
+                       version: toolVersion,
+                       url:     
"https://github.com/mvdan/sh/releases/download/v3.13.1/shfmt_v3.13.1_darwin_arm64";,
+               },
+               {
+                       os:      "ming",
+                       arch:    arch64bit,
+                       version: toolVersion,
+                       url:     
"https://github.com/mvdan/sh/releases/download/v3.13.1/shfmt_v3.13.1_windows_amd64.exe";,
+               },
+       }
+
+       for _, tc := range tests {
+               t.Run(tc.os+" "+tc.arch, func(t *testing.T) {
+                       got, _, err := tool.GetURL(tc.os, tc.arch, tc.version, 
false)
+                       if err != nil {
+                               t.Fatal(err)
+                       }
+                       if got != tc.url {
+                               t.Errorf("\nwant: %s\ngot:  %s", tc.url, got)
+                       }
+               })
+       }
+}
+
+func Test_DownloadShellcheck(t *testing.T) {
+       tools := MakeTools()
+       name := "shellcheck"
+
+       tool := getTool(name, tools)
+
+       const toolVersion = "v0.11.0"
+
+       tests := []test{
+               {
+                       os:      "linux",
+                       arch:    arch64bit,
+                       version: toolVersion,
+                       url:     
"https://github.com/koalaman/shellcheck/releases/download/v0.11.0/shellcheck-v0.11.0.linux.x86_64.tar.gz";,
+               },
+               {
+                       os:      "linux",
+                       arch:    archARM64,
+                       version: toolVersion,
+                       url:     
"https://github.com/koalaman/shellcheck/releases/download/v0.11.0/shellcheck-v0.11.0.linux.aarch64.tar.gz";,
+               },
+               {
+                       os:      "darwin",
+                       arch:    arch64bit,
+                       version: toolVersion,
+                       url:     
"https://github.com/koalaman/shellcheck/releases/download/v0.11.0/shellcheck-v0.11.0.darwin.x86_64.tar.gz";,
+               },
+               {
+                       os:      "darwin",
+                       arch:    archDarwinARM64,
+                       version: toolVersion,
+                       url:     
"https://github.com/koalaman/shellcheck/releases/download/v0.11.0/shellcheck-v0.11.0.darwin.aarch64.tar.gz";,
+               },
+               {
+                       os:      "ming",
+                       arch:    arch64bit,
+                       version: toolVersion,
+                       url:     
"https://github.com/koalaman/shellcheck/releases/download/v0.11.0/shellcheck-v0.11.0.zip";,
+               },
+       }
+
+       for _, tc := range tests {
+               t.Run(tc.os+" "+tc.arch, func(t *testing.T) {
+                       got, _, err := tool.GetURL(tc.os, tc.arch, tc.version, 
false)
+                       if err != nil {
+                               t.Fatal(err)
                        }
                        if got != tc.url {
                                t.Errorf("\nwant: %s\ngot:  %s", tc.url, got)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/arkade-0.11.128/pkg/get/oci.go 
new/arkade-0.11.132/pkg/get/oci.go
--- old/arkade-0.11.128/pkg/get/oci.go  1970-01-01 01:00:00.000000000 +0100
+++ new/arkade-0.11.132/pkg/get/oci.go  2026-10-07 19:24:32.000000000 +0200
@@ -0,0 +1,178 @@
+// Copyright (c) arkade author(s) 2026. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for 
full license information.
+
+package get
+
+import (
+       "archive/tar"
+       "fmt"
+       "io"
+       "os"
+       "path/filepath"
+       "strings"
+
+       "github.com/alexellis/arkade/pkg/config"
+       "github.com/alexellis/arkade/pkg/env"
+       "github.com/google/go-containerregistry/pkg/authn"
+       "github.com/google/go-containerregistry/pkg/crane"
+       v1 "github.com/google/go-containerregistry/pkg/v1"
+)
+
+// downloadFromOCI pulls the tool's OCI image for the requested
+// platform, extracts the binary and installs it into movePath (or
+// the arkade binary dir).
+func downloadFromOCI(tool *Tool, arch, operatingSystem, version, movePath 
string, quiet bool, _ ProgressCallback) (string, string, error) {
+
+       tag := GetToolVersion(tool, version)
+       if tag == "" {
+               tag = "latest"
+       }
+
+       platform, err := ociPlatform(arch, operatingSystem)
+       if err != nil {
+               return "", "", err
+       }
+
+       opts := []crane.Option{
+               crane.WithPlatform(platform),
+               crane.WithAuth(authn.Anonymous),
+       }
+
+       imageName := fmt.Sprintf("%s:%s", tool.OCIImage, tag)
+
+       if !quiet {
+               fmt.Printf("Resolving %s\n", imageName)
+       }
+
+       img, err := crane.Pull(imageName, opts...)
+       if err != nil {
+               return "", "", err
+       }
+
+       wantedName := tool.Name
+       if osName := strings.ToLower(operatingSystem); strings.Contains(osName, 
"ming") || osName == "windows" {
+               wantedName = wantedName + ".exe"
+       }
+
+       // Stream the exported image and extract only the wanted binary,
+       // so unrelated entries (including symlinks) are skipped rather
+       // than aborting the install.
+       tempDir, err := os.MkdirTemp("", "arkade-oci-")
+       if err != nil {
+               return "", "", err
+       }
+       defer os.RemoveAll(tempDir)
+
+       tarFile, err := os.CreateTemp(tempDir, "image*.tar")
+       if err != nil {
+               return "", "", err
+       }
+
+       if err := crane.Export(img, tarFile); err != nil {
+               tarFile.Close()
+               return "", "", fmt.Errorf("exporting %s: %w", imageName, err)
+       }
+       _, err = tarFile.Seek(0, 0)
+       if err != nil {
+               tarFile.Close()
+               return "", "", err
+       }
+
+       binaryPath, err := extractFileFromTar(tarFile, wantedName, tempDir)
+       tarFile.Close()
+       if err != nil {
+               return "", "", fmt.Errorf("extracting %q from image %s for %s: 
%w",
+                       wantedName, imageName, platform.String(), err)
+       }
+
+       finalName := tool.Name
+       if osName := strings.ToLower(operatingSystem); strings.Contains(osName, 
"ming") || osName == "windows" {
+               finalName = finalName + ".exe"
+       }
+
+       var localPath string
+       if movePath == "" {
+               if _, err := config.InitUserDir(); err != nil {
+                       return "", "", err
+               }
+               localPath = env.LocalBinary(finalName, "")
+       } else {
+               localPath = filepath.Join(movePath, finalName)
+       }
+
+       if err := os.MkdirAll(filepath.Dir(localPath), 0755); err != nil {
+               return "", "", err
+       }
+
+       if _, err := atomicCopyFile(binaryPath, localPath, 0755); err != nil {
+               return "", "", err
+       }
+
+       return localPath, finalName, nil
+}
+
+// extractFileFromTar scans a tar stream and writes the first regular
+// file whose basename matches name to dir. Directory and symlink
+// entries are skipped, so unrelated image contents never abort the
+// install.
+func extractFileFromTar(r io.Reader, name, dir string) (string, error) {
+       tr := tar.NewReader(r)
+       for {
+               hdr, err := tr.Next()
+               if err == io.EOF {
+                       return "", fmt.Errorf("file %q not found in image", 
name)
+               }
+               if err != nil {
+                       return "", err
+               }
+               if hdr.Typeflag != tar.TypeReg || filepath.Base(hdr.Name) != 
name {
+                       continue
+               }
+
+               outPath := filepath.Join(dir, name)
+               out, err := os.OpenFile(outPath, 
os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.FileMode(hdr.Mode))
+               if err != nil {
+                       return "", err
+               }
+               if _, err := io.Copy(out, tr); err != nil {
+                       out.Close()
+                       return "", err
+               }
+               if err := out.Close(); err != nil {
+                       return "", err
+               }
+               return outPath, nil
+       }
+}
+
+// ociPlatform converts arkade's OS/arch values into a container
+// platform spec, failing fast on values outside the supported set
+// rather than coercing them to a platform that installs the wrong
+// binary.
+func ociPlatform(arch, operatingSystem string) (*v1.Platform, error) {
+       osName := strings.ToLower(operatingSystem)
+       switch {
+       case strings.Contains(osName, "ming") || osName == "windows":
+               osName = "windows"
+       case strings.HasPrefix(osName, "darwin"):
+               osName = "darwin"
+       case strings.HasPrefix(osName, "linux"):
+               osName = "linux"
+       default:
+               return nil, fmt.Errorf("OS %q is not supported for OCI-based 
tools, use linux, darwin or ming (windows)", operatingSystem)
+       }
+
+       archName := strings.ToLower(arch)
+       switch {
+       case archName == "x86_64" || archName == "amd64":
+               archName = "amd64"
+       case archName == "aarch64" || archName == "arm64":
+               archName = "arm64"
+       case archName == "armv6l" || archName == "armv7l":
+               archName = "arm"
+       default:
+               return nil, fmt.Errorf("architecture %q is not supported for 
OCI-based tools, use amd64, arm64 or armv7l", arch)
+       }
+
+       return &v1.Platform{OS: osName, Architecture: archName}, nil
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/arkade-0.11.128/pkg/get/package.go 
new/arkade-0.11.132/pkg/get/package.go
--- old/arkade-0.11.128/pkg/get/package.go      1970-01-01 01:00:00.000000000 
+0100
+++ new/arkade-0.11.132/pkg/get/package.go      2026-10-07 19:24:32.000000000 
+0200
@@ -0,0 +1,144 @@
+package get
+
+import (
+       "fmt"
+       "log"
+       "os"
+       "path/filepath"
+       "strings"
+
+       "github.com/alexellis/arkade/pkg/archive"
+       "github.com/alexellis/arkade/pkg/config"
+       "github.com/alexellis/arkade/pkg/env"
+)
+
+// installPackage preserves resources beside bin/ and exposes only the declared
+// executables. Relative links keep custom --path installations relocatable.
+func installPackage(tool *Tool, archivePath, downloadURL, operatingSystem, 
movePath string) (installedPath string, err error) {
+       if !strings.HasSuffix(downloadURL, ".tar.gz") {
+               return "", fmt.Errorf("%s: package installation requires a 
tar.gz archive", tool.Name)
+       }
+       if movePath == "" {
+               if _, err := config.InitUserDir(); err != nil {
+                       return "", err
+               }
+               movePath = filepath.Dir(env.LocalBinary(tool.Name, ""))
+       }
+
+       stage, err := os.MkdirTemp(movePath, ".arkade-package-*")
+       if err != nil {
+               return "", err
+       }
+       keepStage := false
+       defer func() {
+               if !keepStage {
+                       os.RemoveAll(stage)
+               }
+       }()
+       packageDir := filepath.Join(stage, "package")
+       f, err := os.Open(archivePath)
+       if err != nil {
+               return "", err
+       }
+       err = archive.UntarNested(f, packageDir, true, true, true, false)
+       f.Close()
+       if err != nil {
+               return "", err
+       }
+
+       packageName := ".arkade-" + tool.Name
+       packagePath := filepath.Join(movePath, packageName)
+       names := make([]string, 0, len(tool.PackageBinaries))
+       seen := map[string]bool{}
+       for _, name := range tool.PackageBinaries {
+               if name == "" || name == "." || name == ".." || 
strings.ContainsAny(name, `/\`) || seen[name] {
+                       return "", fmt.Errorf("%s: invalid or duplicate package 
binary %q", tool.Name, name)
+               }
+               seen[name] = true
+               if strings.HasPrefix(strings.ToLower(operatingSystem), "ming") 
&& !tool.NoExtension {
+                       name += ".exe"
+               }
+               source := filepath.Join(packageDir, "bin", name)
+               info, err := os.Lstat(source)
+               if err != nil {
+                       return "", fmt.Errorf("package binary %s: %w", name, 
err)
+               }
+               if !info.Mode().IsRegular() {
+                       return "", fmt.Errorf("package binary %s is not a 
regular file", name)
+               }
+               if err := os.Chmod(source, 0755); err != nil {
+                       return "", err
+               }
+               if info, err := os.Lstat(filepath.Join(movePath, name)); err == 
nil && info.IsDir() {
+                       return "", fmt.Errorf("package binary destination %s is 
a directory", name)
+               } else if err != nil && !os.IsNotExist(err) {
+                       return "", err
+               }
+               // Create links before changing the installation, including 
checking the
+               // host's ability to create symlinks (Developer Mode/elevation 
on Windows).
+               if err := os.Symlink(filepath.Join(packageName, "bin", name), 
filepath.Join(stage, name)); err != nil {
+                       return "", fmt.Errorf("create package link %s: %w", 
name, err)
+               }
+               names = append(names, name)
+       }
+       if !seen[tool.Name] {
+               return "", fmt.Errorf("%s: PackageBinaries must include the 
main binary", tool.Name)
+       }
+
+       // Keep the previous package and entry points until every link is in 
place.
+       // If publication fails, restore them before removing the staging 
directory.
+       previousPackage := filepath.Join(stage, "previous-package")
+       previous := map[string]string{}
+       published := []string{}
+       hadPackage, movedPackage := false, false
+       defer func() {
+               if err == nil {
+                       return
+               }
+               for _, name := range published {
+                       os.Remove(filepath.Join(movePath, name))
+               }
+               for name, backup := range previous {
+                       if restoreErr := os.Rename(backup, 
filepath.Join(movePath, name)); restoreErr != nil {
+                               keepStage = true
+                               log.Printf("Restore %s: %s; backup retained at 
%s", name, restoreErr, backup)
+                       }
+               }
+               if movedPackage {
+                       os.RemoveAll(packagePath)
+               }
+               if hadPackage {
+                       if restoreErr := os.Rename(previousPackage, 
packagePath); restoreErr != nil {
+                               keepStage = true
+                               log.Printf("Restore package %s: %s; backup 
retained at %s", packagePath, restoreErr, previousPackage)
+                       }
+               }
+       }()
+       if err = os.Rename(packagePath, previousPackage); err == nil {
+               hadPackage = true
+       } else if !os.IsNotExist(err) {
+               return "", err
+       }
+       if err = os.Rename(packageDir, packagePath); err != nil {
+               return "", err
+       }
+       movedPackage = true
+       for _, name := range names {
+               destination := filepath.Join(movePath, name)
+               backup := filepath.Join(stage, "previous-"+name)
+               if err = os.Rename(destination, backup); err == nil {
+                       previous[name] = backup
+               } else if !os.IsNotExist(err) {
+                       return "", err
+               }
+               if err = os.Rename(filepath.Join(stage, name), destination); 
err != nil {
+                       return "", err
+               }
+               published = append(published, name)
+       }
+       mainName := tool.Name
+       if strings.HasPrefix(strings.ToLower(operatingSystem), "ming") && 
!tool.NoExtension {
+               mainName += ".exe"
+       }
+       return filepath.Join(movePath, mainName), nil
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/arkade-0.11.128/pkg/get/package_test.go 
new/arkade-0.11.132/pkg/get/package_test.go
--- old/arkade-0.11.128/pkg/get/package_test.go 1970-01-01 01:00:00.000000000 
+0100
+++ new/arkade-0.11.132/pkg/get/package_test.go 2026-10-07 19:24:32.000000000 
+0200
@@ -0,0 +1,129 @@
+package get
+
+import (
+       "archive/tar"
+       "bytes"
+       "compress/gzip"
+       "net/http"
+       "net/http/httptest"
+       "os"
+       "path/filepath"
+       "strings"
+       "testing"
+)
+
+func packageArchive(t *testing.T, files map[string]string) []byte {
+       t.Helper()
+       var buf bytes.Buffer
+       gz := gzip.NewWriter(&buf)
+       tw := tar.NewWriter(gz)
+       for name, contents := range files {
+               if err := tw.WriteHeader(&tar.Header{Name: name, Mode: 0644, 
Size: int64(len(contents))}); err != nil {
+                       t.Fatal(err)
+               }
+               if _, err := tw.Write([]byte(contents)); err != nil {
+                       t.Fatal(err)
+               }
+       }
+       if err := tw.Close(); err != nil {
+               t.Fatal(err)
+       }
+       if err := gz.Close(); err != nil {
+               t.Fatal(err)
+       }
+       return buf.Bytes()
+}
+
+func TestPostInstallationMsgPackage(t *testing.T) {
+       for _, movePath := range []string{"", "/tmp/tools"} {
+               msg, err := PostInstallationMsg(movePath, []ToolLocal{
+                       {Name: "codex", Path: "/tmp/tools/codex", Package: 
true},
+                       {Name: "jq", Path: "/tmp/tools/jq"},
+               })
+               if err != nil {
+                       t.Fatal(err)
+               }
+               text := string(msg)
+               if strings.Contains(text, "sudo mv /tmp/tools/codex") || 
strings.Contains(text, "sudo install -m 755 /tmp/tools/codex") {
+                       t.Fatalf("instructions would detach the package entry 
point: %s", text)
+               }
+               if !strings.Contains(text, "sudo arkade get codex --path 
/usr/local/bin") {
+                       t.Fatalf("missing package installation instructions: 
%s", text)
+               }
+               ordinary := "sudo mv /tmp/tools/jq /usr/local/bin/"
+               if movePath != "" {
+                       ordinary = "sudo install -m 755 /tmp/tools/jq 
/usr/local/bin/jq"
+               }
+               if !strings.Contains(text, ordinary) || strings.Contains(text, 
"sudo arkade get jq") {
+                       t.Fatalf("incorrect ordinary tool installation 
instructions: %s", text)
+               }
+       }
+}
+
+func TestDownloadPackage(t *testing.T) {
+       for _, targetOS := range []string{"linux", "darwin", "mingw"} {
+               t.Run(targetOS, func(t *testing.T) {
+                       ext := ""
+                       if targetOS == "mingw" {
+                               ext = ".exe"
+                       }
+                       files := map[string]string{
+                               "bin/tool" + ext: "main v1", "bin/helper" + 
ext: "helper v1",
+                               "resources/nested/data": "resource", 
"manifest.json": "manifest",
+                       }
+                       body := packageArchive(t, files)
+                       server := httptest.NewServer(http.HandlerFunc(func(w 
http.ResponseWriter, r *http.Request) {
+                               w.Write(body)
+                       }))
+                       defer server.Close()
+                       tool := Tool{Name: "tool", URLTemplate: server.URL + 
"/package.tar.gz", BinaryTemplate: "tool", PackageBinaries: []string{"tool", 
"helper"}}
+                       dir := t.TempDir()
+                       // Also exercise migration from the original 
single-binary install.
+                       if err := os.WriteFile(filepath.Join(dir, "tool"+ext), 
[]byte("old standalone"), 0755); err != nil {
+                               t.Fatal(err)
+                       }
+                       for _, version := range []string{"v1", "v2"} {
+                               files["bin/tool"+ext] = "main " + version
+                               body = packageArchive(t, files)
+                               got, _, err := Download(&tool, "amd64", 
targetOS, version, dir, false, true, false)
+                               if err != nil {
+                                       t.Fatal(err)
+                               }
+                               if got != filepath.Join(dir, "tool"+ext) {
+                                       t.Fatalf("unexpected path: %s", got)
+                               }
+                               for _, name := range []string{"tool", "helper"} 
{
+                                       p := filepath.Join(dir, name+ext)
+                                       info, err := os.Stat(p)
+                                       if err != nil || info.Mode().Perm() != 
0755 {
+                                               t.Fatalf("executable %s: 
info=%v err=%v", p, info, err)
+                                       }
+                                       content, err := os.ReadFile(p)
+                                       if err != nil || string(content) != 
files["bin/"+name+ext] {
+                                               t.Fatalf("binary %s: %q, %v", 
name, content, err)
+                                       }
+                               }
+                       }
+                       for _, name := range []string{"resources/nested/data", 
"manifest.json"} {
+                               content, err := os.ReadFile(filepath.Join(dir, 
".arkade-tool", name))
+                               if err != nil || string(content) != files[name] 
{
+                                       t.Fatalf("resource %s: %q, %v", name, 
content, err)
+                               }
+                       }
+                       // A missing companion must fail before replacing a 
working install.
+                       delete(files, "bin/helper"+ext)
+                       body = packageArchive(t, files)
+                       if _, _, err := Download(&tool, "amd64", targetOS, 
"v3", dir, false, true, false); err == nil {
+                               t.Fatal("expected missing companion error")
+                       }
+                       content, err := os.ReadFile(filepath.Join(dir, 
"tool"+ext))
+                       if err != nil || string(content) != "main v2" {
+                               t.Fatalf("previous install damaged: %q, %v", 
content, err)
+                       }
+                       staging, err := filepath.Glob(filepath.Join(dir, 
".arkade-package-*"))
+                       if err != nil || len(staging) != 0 {
+                               t.Fatalf("staging directories left behind: %v, 
%v", staging, err)
+                       }
+               })
+       }
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/arkade-0.11.128/pkg/get/table.go 
new/arkade-0.11.132/pkg/get/table.go
--- old/arkade-0.11.128/pkg/get/table.go        2026-10-03 10:53:20.000000000 
+0200
+++ new/arkade-0.11.132/pkg/get/table.go        2026-10-07 19:24:32.000000000 
+0200
@@ -81,7 +81,11 @@
                url := fmt.Sprintf("https://github.com/%s/%s";, t.Owner, t.Repo)
 
                if format == MarkdownStyle {
-                       name = fmt.Sprintf("[%s](%s)", t.Name, url)
+                       if len(t.OCIImage) == 0 {
+                               name = fmt.Sprintf("[%s](%s)", t.Name, url)
+                       } else {
+                               name = t.Name
+                       }
                }
                table.Append([]string{name, t.Description})
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/arkade-0.11.128/pkg/get/tools.go 
new/arkade-0.11.132/pkg/get/tools.go
--- old/arkade-0.11.128/pkg/get/tools.go        2026-10-03 10:53:20.000000000 
+0200
+++ new/arkade-0.11.132/pkg/get/tools.go        2026-10-07 19:24:32.000000000 
+0200
@@ -5683,25 +5683,12 @@
        // Codex CLI
        tools = append(tools,
                Tool{
-                       Owner:       "openai",
-                       Repo:        "codex",
-                       Name:        "codex",
-                       Description: "Codex CLI from OpenAI.",
-                       BinaryTemplate: `{{$arch := .Arch}}
-{{- if eq .Arch "amd64" -}}
-{{$arch = "x86_64"}}
-{{- else if eq .Arch "arm64" -}}
-{{$arch = "aarch64"}}
-{{- end}}
-{{$os := .OS}}
-{{- if HasPrefix .OS "ming" -}}
-{{$os = "pc-windows-msvc"}}
-{{- else if eq .OS "darwin" -}}
-{{$os = "apple-darwin"}}
-{{- else -}}
-{{$os = "unknown-linux-musl"}}
-{{- end}}
-codex-{{$arch}}-{{$os}}`,
+                       Owner:           "openai",
+                       Repo:            "codex",
+                       Name:            "codex",
+                       Description:     "Codex CLI from OpenAI.",
+                       BinaryTemplate:  `codex`,
+                       PackageBinaries: []string{"codex", 
"codex-code-mode-host"},
                        URLTemplate: `{{$arch := .Arch}}
 {{- if eq .Arch "amd64" -}}
 {{$arch = "x86_64"}}
@@ -5711,14 +5698,13 @@
 {{$os := .OS}}
 {{$ext := ".tar.gz"}}
 {{- if HasPrefix .OS "ming" -}}
-{{$os = "pc-windows-msvc.exe"}}
-{{$ext = ".zip"}}
+{{$os = "pc-windows-msvc"}}
 {{- else if eq .OS "darwin" -}}
 {{$os = "apple-darwin"}}
-{{- else -}}
+{{- else if eq .OS "linux" -}}
 {{$os = "unknown-linux-musl"}}
 {{- end}}
-https://github.com/openai/codex/releases/download/{{.Version}}/codex-{{$arch}}-{{$os}}{{$ext}}`,
+https://github.com/openai/codex/releases/download/{{.Version}}/codex-package-{{$arch}}-{{$os}}{{$ext}}`,
                })
 
        // Codex Code Mode Host
@@ -6007,9 +5993,13 @@
 
        tools = append(tools,
                Tool{
-                       Owner:       "nklmilojevic",
-                       Repo:        "sofka",
-                       Name:        "sofka",
+                       Owner: "nklmilojevic",
+                       Repo:  "sofka",
+                       Name:  "sofka",
+                       // v0.29.10 (latest) ships no binaries, only source 
archives, so
+                       // pinned at v0.29.9. Transient: unpin once binaries 
are restored.
+                       // Upstream issue tracker: 
https://github.com/nklmilojevic/sofka
+                       Version:     "v0.29.9",
                        Description: "A Kubernetes TUI that tells you why it's 
broken.",
                        URLTemplate: `
                                        {{$os := "unknown-linux-gnu"}}
@@ -6033,5 +6023,139 @@
                        BinaryTemplate: `sofka`,
                })
 
+       tools = append(tools,
+               Tool{
+                       Owner:       "mvdan",
+                       Repo:        "sh",
+                       Name:        "shfmt",
+                       Description: "A shell parser, formatter, and 
interpreter (sh/bash/mksh)",
+                       BinaryTemplate: `
+                                       {{$os := .OS}}
+                                       {{$arch := .Arch}}
+                                       {{$ext := ""}}
+
+                                       {{- if HasPrefix .OS "ming" -}}
+                                               {{$os = "windows"}}
+                                               {{$ext = ".exe"}}
+                                       {{- end -}}
+
+                                       {{- if eq .Arch "x86_64" -}}
+                                               {{$arch = "amd64"}}
+                                       {{- else if (or (eq .Arch "aarch64") 
(eq .Arch "arm64")) -}}
+                                               {{$arch = "arm64"}}
+                                       {{- else if (or (eq .Arch "armv6l") (eq 
.Arch "armv7l")) -}}
+                                               {{$arch = "arm"}}
+                                       {{- end -}}
+
+                                       
{{.Name}}_{{.Version}}_{{$os}}_{{$arch}}{{$ext}}`,
+               })
+
+       tools = append(tools,
+               Tool{
+                       Owner:          "koalaman",
+                       Repo:           "shellcheck",
+                       Name:           "shellcheck",
+                       Description:    "A static analysis tool for shell 
scripts",
+                       BinaryTemplate: `shellcheck`,
+                       URLTemplate: `
+                                       {{$target := ""}}
+                                       {{$ext := "tar.gz"}}
+                                       {{- if eq .OS "linux" -}}
+                                               {{- if eq .Arch "x86_64" -}}
+                                                       {{$target = 
".linux.x86_64"}}
+                                               {{- else if (or (eq .Arch 
"aarch64") (eq .Arch "arm64")) -}}
+                                                       {{$target = 
".linux.aarch64"}}
+                                               {{- end -}}
+                                       {{- else if eq .OS "darwin" -}}
+                                               {{- if eq .Arch "x86_64" -}}
+                                                       {{$target = 
".darwin.x86_64"}}
+                                               {{- else if (or (eq .Arch 
"aarch64") (eq .Arch "arm64")) -}}
+                                                       {{$target = 
".darwin.aarch64"}}
+                                               {{- end -}}
+                                       {{- else if HasPrefix .OS "ming" -}}
+                                               {{- if eq .Arch "x86_64" -}}
+                                                       {{$ext = "zip"}}
+                                                       {{$target = ""}}
+                                               {{- end -}}
+                                       {{- end -}}
+
+                                       
https://github.com/{{.Owner}}/{{.Repo}}/releases/download/{{.Version}}/shellcheck-{{.Version}}{{$target}}.{{$ext}}`,
+               })
+
+       // OCI-extracted tools: the binary is pulled from an OCI image
+       // instead of a GitHub release. These are also available through
+       // "arkade oci install".
+       ociTools := []Tool{
+               {
+                       Owner:       "alexellis",
+                       Repo:        "oidc-cli",
+                       Name:        "oidc-cli",
+                       Description: "OAuth/OpenID Connect client for testing 
OIDC flows from the command line",
+                       BinaryTemplate: `
+                                                                       {{$arch 
:= .Arch}}
+                                                                       {{$name 
:= "oidc-cli"}}
+
+                                                                       {{- if 
eq .Arch "x86_64" -}}
+                                                                       {{- 
else if (or (eq .Arch "aarch64") (eq .Arch "arm64")) -}}
+                                                                               
{{$name = "oidc-cli-arm64"}}
+                                                                       {{- 
else if (or (eq .Arch "armv6l") (eq .Arch "armv7l")) -}}
+                                                                               
{{$name = "oidc-cli-armhf"}}
+                                                                       {{- end 
-}}
+
+                                                                       {{- if 
HasPrefix .OS "ming" -}}
+                                                                               
{{$name = "oidc-cli.exe"}}
+                                                                       {{- 
else if eq .OS "darwin" -}}
+                                                                               
{{- if (or (eq .Arch "aarch64") (eq .Arch "arm64")) -}}
+                                                                               
        {{$name = "oidc-cli-darwin-arm64"}}
+                                                                               
{{- else -}}
+                                                                               
        {{$name = "oidc-cli-darwin"}}
+                                                                               
{{- end -}}
+                                                                       {{- end 
-}}
+
+                                                                       
{{$name}}`,
+               },
+               {
+                       Name:        "vmmeter",
+                       Description: "Metering and telemetry for Slicer virtual 
machines",
+                       OCIImage:    "ghcr.io/openfaasltd/vmmeter",
+               },
+               {
+                       Name:        "slicer",
+                       Description: "Create and manage Firecracker microVMs",
+                       OCIImage:    "ghcr.io/openfaasltd/slicer",
+               },
+               {
+                       Name:        "superterm",
+                       Description: "Run and manage coding agents in hosted 
terminal sessions",
+                       OCIImage:    "ghcr.io/openfaasltd/superterm",
+               },
+               {
+                       Name:        "kullu",
+                       Description: "Self-hosted file sharing and drop for 
teams",
+                       OCIImage:    "ghcr.io/openfaasltd/kullu",
+               },
+               {
+                       Name:        "k3sup-pro",
+                       Description: "Extended edition of k3sup with enterprise 
support features",
+                       OCIImage:    "ghcr.io/openfaasltd/k3sup-pro",
+               },
+               {
+                       Name:        "slicer-agent",
+                       Description: "Agent for managing Slicer microVMs on 
remote hosts",
+                       OCIImage:    "ghcr.io/openfaasltd/slicer-agent",
+               },
+               {
+                       Name:        "signet",
+                       Description: "Lightweight OpenID Connect provider for 
agents and automation",
+                       OCIImage:    "ghcr.io/openfaasltd/signet",
+               },
+               {
+                       Name:        "toilgate",
+                       Description: "Agent CLI for Toilgate models",
+                       OCIImage:    "ghcr.io/openfaasltd/toilgate",
+               },
+       }
+       tools = append(tools, ociTools...)
+
        return tools
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/arkade-0.11.128/pkg/get/url_checker_test.go 
new/arkade-0.11.132/pkg/get/url_checker_test.go
--- old/arkade-0.11.128/pkg/get/url_checker_test.go     2026-10-03 
10:53:20.000000000 +0200
+++ new/arkade-0.11.132/pkg/get/url_checker_test.go     2026-10-07 
19:24:32.000000000 +0200
@@ -29,6 +29,12 @@
                        }
                }
 
+               // OCI-extracted tools are resolved and verified at pull
+               // time by the registry, they have no download URL to check.
+               if len(tool.OCIImage) > 0 {
+                       continue
+               }
+
                if skip {
                        continue
                }

++++++ arkade.obsinfo ++++++
--- /var/tmp/diff_new_pack.K6yLkZ/_old  2026-10-20 12:38:47.127672463 +0200
+++ /var/tmp/diff_new_pack.K6yLkZ/_new  2026-10-20 12:38:47.131672627 +0200
@@ -1,5 +1,5 @@
 name: arkade
-version: 0.11.128
-mtime: 1791017600
-commit: 90173cc580b31cd46ec01d807b37dc6f9b9fb55f
+version: 0.11.132
+mtime: 1791393872
+commit: 8b8a849ed4f9b54952f2a3b27168648f8fea31f2
 

++++++ vendor.tar.gz ++++++

Reply via email to