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 ++++++
