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-07-01 16:54:35
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/arkade (Old)
 and      /work/SRC/openSUSE:Factory/.arkade.new.11887 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "arkade"

Wed Jul  1 16:54:35 2026 rev:83 rq:1362905 version:0.11.105

Changes:
--------
--- /work/SRC/openSUSE:Factory/arkade/arkade.changes    2026-06-23 
17:43:06.142972855 +0200
+++ /work/SRC/openSUSE:Factory/.arkade.new.11887/arkade.changes 2026-07-01 
16:54:53.843093512 +0200
@@ -1,0 +2,17 @@
+Wed Jul 01 08:06:34 UTC 2026 - Johannes Kastl 
<[email protected]>
+
+- Update to version 0.11.105:
+  * Improve search ranking and add fallback for unmatched queries
+- Update to version 0.11.104:
+  * Initial search command for arkade
+- Update to version 0.11.103:
+  * Fix trailing newline
+  * Flat option for arkade oci install
+  * Remove faasd tool definition
+  * Update number of tools after adding pluto
+  * Add pluto CLI to find deprecated K8s APIs
+  * Make symlink extraction in UntarNested conditional
+  * Fix symlink path-traversal escape in UntarNested
+  * Remove stray file
+
+-------------------------------------------------------------------

Old:
----
  arkade-0.11.102.obscpio

New:
----
  arkade-0.11.105.obscpio

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

Other differences:
------------------
++++++ arkade.spec ++++++
--- /var/tmp/diff_new_pack.CrtcEo/_old  2026-07-01 16:54:56.443183233 +0200
+++ /var/tmp/diff_new_pack.CrtcEo/_new  2026-07-01 16:54:56.475184337 +0200
@@ -17,7 +17,7 @@
 
 
 Name:           arkade
-Version:        0.11.102
+Version:        0.11.105
 Release:        0
 Summary:        Open Source Kubernetes Marketplace
 License:        Apache-2.0

++++++ _service ++++++
--- /var/tmp/diff_new_pack.CrtcEo/_old  2026-07-01 16:54:56.967201315 +0200
+++ /var/tmp/diff_new_pack.CrtcEo/_new  2026-07-01 16:54:57.007202696 +0200
@@ -1,9 +1,9 @@
 <services>
   <service name="obs_scm" mode="manual">
-    <param name="url">https://github.com/alexellis/arkade</param>
+    <param name="url">https://github.com/alexellis/arkade.git</param>
     <param name="scm">git</param>
     <param name="exclude">.git</param>
-    <param name="revision">0.11.102</param>
+    <param name="revision">refs/tags/0.11.105</param>
     <param name="versionformat">@PARENT_TAG@</param>
     <param name="versionrewrite-pattern">v(.*)</param>
     <param name="changesgenerate">enable</param>

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.CrtcEo/_old  2026-07-01 16:54:57.187208907 +0200
+++ /var/tmp/diff_new_pack.CrtcEo/_new  2026-07-01 16:54:57.239210701 +0200
@@ -1,6 +1,8 @@
 <servicedata>
 <service name="tar_scm">
                 <param name="url">https://github.com/alexellis/arkade</param>
-              <param 
name="changesrevision">37af31c7b58de8f16a10067051e3bbe7ecb6aa79</param></service></servicedata>
+              <param 
name="changesrevision">37af31c7b58de8f16a10067051e3bbe7ecb6aa79</param></service><service
 name="tar_scm">
+                <param 
name="url">https://github.com/alexellis/arkade.git</param>
+              <param 
name="changesrevision">15dcd3c06fd80143553e9fc53fb2ef59fb84a409</param></service></servicedata>
 (No newline at EOF)
 

++++++ arkade-0.11.102.obscpio -> arkade-0.11.105.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/arkade-0.11.102/README.md 
new/arkade-0.11.105/README.md
--- old/arkade-0.11.102/README.md       2026-06-22 16:31:10.000000000 +0200
+++ new/arkade-0.11.105/README.md       2026-06-30 13:53:49.000000000 +0200
@@ -183,6 +183,13 @@
 ```
 > This is a time saver compared to searching for download pages every time you 
 > need a tool.
 
+Search CLIs available via `arkade get` by name or keyword, with alias support 
(e.g. "k8s" expands to "Kubernetes"):
+
+```bash
+arkade search helm
+arkade search k8s
+```
+
 Files are stored at `$HOME/.arkade/bin/`
 
 Want to download tools to a custom path such as into the GitHub Actions cached 
tool folder?
@@ -965,7 +972,8 @@
 | [websocat](https://github.com/vi/websocat)                                   
| Command-line client for WebSockets, like netcat/socat but for WebSockets      
                                                                                
    |
 | [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 196 tools, use `arkade get NAME` to download one.                    
                                                                                
                                                                                
     
+There are 196 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.102/cmd/oci/install.go 
new/arkade-0.11.105/cmd/oci/install.go
--- old/arkade-0.11.102/cmd/oci/install.go      2026-06-22 16:31:10.000000000 
+0200
+++ new/arkade-0.11.105/cmd/oci/install.go      2026-06-30 13:53:49.000000000 
+0200
@@ -44,6 +44,10 @@
 
   # Use a shortcut for the image name (vmmeter, slicer, k3sup-pro)
   arkade oci install k3sup-pro
+
+  # Flatten the archive so files are extracted directly into the install path,
+  # ignoring directory structure in the image (e.g. ./usr/local/bin/FILE => 
./FILE)
+  arkade oci install ghcr.io/openfaasltd/slicer --flat
 `,
                SilenceUsage: true,
        }
@@ -57,6 +61,7 @@
        command.Flags().BoolP("gzipped", "g", false, "Is this a gzipped 
tarball?")
        command.Flags().Bool("quiet", false, "Suppress progress output")
        command.Flags().Bool("symlink", false, "Write symlinks when unpacking 
OCI image, only use with trusted sources")
+       command.Flags().Bool("flat", false, "Extract all files directly into 
the install path. Caution: files sharing a basename will overwrite each other 
and symlinks are skipped")
 
        // Hide the deprecated --path flag
        command.Flags().MarkHidden("path")
@@ -71,6 +76,7 @@
                quiet, _ := cmd.Flags().GetBool("quiet")
                allowSymlinks, _ := cmd.Flags().GetBool("symlink")
                showProgress, _ := cmd.Flags().GetBool("progress")
+               flatExtract, _ := cmd.Flags().GetBool("flat")
 
                if len(args) < 1 {
                        return fmt.Errorf("please provide an image name")
@@ -262,7 +268,7 @@
                                // When the alt-screen is active, suppress 
UntarNested's
                                // per-file logging so it doesn't corrupt the 
live frame.
                                untarQuiet := quiet || (tty && renderLive)
-                               if uErr := archive.UntarNested(tarFile, 
installPath, gzipped, untarQuiet, allowSymlinks); uErr != nil {
+                               if uErr := archive.UntarNested(tarFile, 
installPath, gzipped, untarQuiet, allowSymlinks, flatExtract); uErr != nil {
                                        workErr = fmt.Errorf("failed to untar 
%s: %w", tempFile.Name(), uErr)
                                }
                        }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/arkade-0.11.102/cmd/search.go 
new/arkade-0.11.105/cmd/search.go
--- old/arkade-0.11.102/cmd/search.go   1970-01-01 01:00:00.000000000 +0100
+++ new/arkade-0.11.105/cmd/search.go   2026-06-30 13:53:49.000000000 +0200
@@ -0,0 +1,367 @@
+// Copyright (c) arkade author(s) 2022. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for 
full license information.
+
+package cmd
+
+import (
+       "errors"
+       "fmt"
+       "math"
+       "sort"
+       "strings"
+
+       "github.com/spf13/cobra"
+
+       "github.com/alexellis/arkade/pkg/get"
+)
+
+type scoreRank struct {
+       Tool  get.Tool
+       Score float64
+}
+
+// aliases maps common shorthand to full term. Each alias expands to one or 
more
+// space-separated terms so that multi-word expansions are scored correctly.
+var aliasMap = map[string]string{
+       "k8s":    "kubernetes",
+       "kube":   "kubernetes",
+       "eksctl": "amazon eks kubernetes cluster management",
+       "gke":    "google kubernetes engine",
+       "aks":    "azure kubernetes service",
+}
+
+func MakeSearch() *cobra.Command {
+       tools := get.MakeTools()
+
+       cmd := &cobra.Command{
+               Use:   "search [query]",
+               Short: `Search for a tool available in arkade get`,
+               Long:  `Search for tools by name or description using relevance 
ranking. Tools that share keywords with your query are ranked first. Common 
aliases like k8s are expanded to kubernetes, and fuzzy matching finds similar 
names (e.g., "openfaas" matches faas-cli). Multi-word queries match and rank 
tools containing multiple terms higher.`,
+               Example: `  arkade search helm
+
+   # Expand "k8s" to Kubernetes and rank by relevance
+   arkade search k8s
+
+   # Fuzzy name matching — finds faas-cli even though the user types "openfaas"
+   arkade search openfaas
+
+   # Multi-word query (tools with both words ranked higher)
+   arkade search container runtime
+
+   # Show as a list instead of table
+   arkade search helm --format list`,
+       }
+
+       cmd.Flags().String("format", "table", "Output format: list, markdown or 
table")
+
+       cmd.RunE = func(cmd *cobra.Command, args []string) error {
+               query := strings.TrimSpace(strings.Join(args, " "))
+               if query == "" {
+                       return errors.New("please provide a search query")
+               }
+
+               format, _ := cmd.Flags().GetString("format")
+
+               ranked := rankByTFIDF(tools, query)
+
+               sort.SliceStable(ranked, func(i, j int) bool {
+                       return ranked[i].Score > ranked[j].Score
+               })
+
+               var matches []scoreRank
+               for _, r := range ranked {
+                       if r.Score > 0 {
+                               matches = append(matches, r)
+                       }
+               }
+
+               // Last resort: substring fallback on Name, Owner and Repo when 
TF-IDF found nothing.
+               if len(matches) == 0 {
+                       queryTerms := tokenize(expandAliases(query))
+                       matches = fuzzySubstringFallback(tools, queryTerms)
+               }
+
+               if len(matches) == 0 {
+                       cmd.Printf("No tools found matching \"%s\"\n", query)
+                       return nil
+               }
+
+               switch format {
+               case "list":
+                       for i, r := range matches {
+                               fmt.Printf("%d. %s (%.3f)\t%s\n", i+1, 
r.Tool.Name, r.Score, r.Tool.Description)
+                       }
+               case "markdown":
+                       fmt.Println("| Rank | Name | Score | Description |")
+                       fmt.Println("|------|------|-------|-------------|")
+                       for i, r := range matches {
+                               fmt.Printf("| %d | %s | %.3f | %s |\n", i+1, 
r.Tool.Name, r.Score, r.Tool.Description)
+                       }
+               default:
+                       matchesOnly := make([]get.Tool, len(matches))
+                       for i, r := range matches {
+                               matchesOnly[i] = r.Tool
+                       }
+                       cmd.Printf("Found %d tool(s) matching \"%s\":\n\n", 
len(matches), query)
+                       get.CreateToolsTable(matchesOnly, get.TableStyle)
+               }
+
+               return nil
+       }
+
+       return cmd
+}
+
+// fuzzySubstringFallback does a simple case-insensitive substring match across
+// Name, Owner, Repo (not Description) when the TF-IDF index returned no 
results.
+func fuzzySubstringFallback(tools []get.Tool, queryTerms []string) []scoreRank 
{
+       results := make([]scoreRank, 0)
+       for _, t := range tools {
+               var score float64
+               for _, q := range queryTerms {
+                       if strings.Contains(strings.ToLower(t.Name), q) {
+                               score += 3.0
+                       } else if strings.Contains(strings.ToLower(t.Owner), q) 
{
+                               score += 2.0
+                       } else if strings.Contains(strings.ToLower(t.Repo), q) {
+                               score += 1.5
+                       }
+               }
+               if score > 0 {
+                       results = append(results, scoreRank{Tool: t, Score: 
score})
+               }
+       }
+       sort.SliceStable(results, func(i, j int) bool {
+               return results[i].Score > results[j].Score
+       })
+       return results
+}
+
+// splitOnSeparators returns a slice of substrings obtained by splitting on - 
and _.
+func splitOnSeparators(s string) []string {
+       parts := strings.Split(strings.ToLower(s), "-")
+       result := make([]string, 0)
+       for _, p := range parts {
+               subparts := strings.Split(p, "_")
+               for _, sp := range subparts {
+                       if sp != "" {
+                               result = append(result, sp)
+                       }
+               }
+       }
+       return result
+}
+
+func rankByTFIDF(tools []get.Tool, rawQuery string) []scoreRank {
+       docs := make([][]string, len(tools))
+       for i, t := range tools {
+               // Tokenize name with separator splitting to create individual 
IDF entries
+               // for parts like "faas" in "faas-cli", then append the 
expanded description.
+               nameTokens := splitOnSeparators(t.Name)
+               docs[i] = tokenize(
+                       strings.Join(nameTokens, " ") + " " +
+                               t.Owner + " " +
+                               t.Repo + " " +
+                               expandAliases(t.Description),
+               )
+       }
+
+       df := make(map[string]int)
+       for _, doc := range docs {
+               seen := map[string]bool{}
+               for _, w := range doc {
+                       if !seen[w] {
+                               df[w]++
+                               seen[w] = true
+                       }
+               }
+       }
+
+       nDocs := float64(len(tools))
+       idf := make(map[string]float64)
+       for t, freq := range df {
+               idf[t] = math.Log(nDocs/float64(freq)) + 1.0
+       }
+
+       queryTerms := tokenize(expandAliases(rawQuery))
+
+       scores := make([]scoreRank, len(tools))
+       for i, t := range tools {
+               tf := termFreq(docs[i])
+
+               var score float64
+               termsMatched := 0
+
+               // TF-IDF contribution from name + description.
+               for _, q := range queryTerms {
+                       if tf[q] > 0 {
+                               score += tf[q] * idf[q]
+                               termsMatched++
+                       }
+               }
+
+               // Exact name match bonus: if the tool name equals any query 
term (or vice versa),
+               // give it a very high score so it ranks first.
+               lowerName := strings.ToLower(t.Name)
+               for _, q := range queryTerms {
+                       if lowerName == q {
+                               score += 5.0
+                       }
+               }
+
+               // Substring name bonus: if a query term is a substring of the 
tool name but not
+               // matched by TF-IDF (because it's not an independent token), 
score using the
+               // best available IDF weight from that tool's own tokens.
+               for _, q := range queryTerms {
+                       if tf[q] == 0 && strings.Contains(lowerName, q) {
+                               // Use a fallback weight: log(N/1)+1 which is 
the maximum possible IDF,
+                               // giving partial name matches strong relevance.
+                               maxIDF := math.Log(nDocs) + 1.0
+                               score += maxIDF * 2.0
+                               termsMatched++
+                       } else if tf[q] > 0 {
+                               // Name and description both matched — slight 
extra boost.
+                               score += idf[q] * 0.5
+                       }
+               }
+
+               // Levenshtein fuzzy matching on tool name parts only.
+               fuzzyBonus := levenshteinFuzzyScore(queryTerms, t.Name)
+               score += fuzzyBonus
+               if fuzzyBonus > 0 {
+                       termsMatched++
+               }
+
+               // Multi-word query boost: tools that match multiple distinct 
query terms are
+               // ranked higher. The boost is proportional to the fraction of 
query terms matched.
+               if len(queryTerms) > 1 {
+                       frac := float64(termsMatched) / float64(len(queryTerms))
+                       score += score * frac * 0.5
+               }
+
+               scores[i] = scoreRank{Tool: t, Score: score}
+       }
+
+       return scores
+}
+
+// levenshteinFuzzyScore computes a bonus for tools whose name contains
+// words within edit distance of any query token that aren't already matched 
by exact text.
+func levenshteinFuzzyScore(queryTerms []string, toolName string) float64 {
+       if len(queryTerms) == 0 {
+               return 0
+       }
+
+       nameLower := strings.ToLower(toolName)
+       // Use separator-split name parts for comparison rather than whitespace 
tokens.
+       nameWords := splitOnSeparators(nameLower)
+
+       var bonus float64
+       distMax := 2
+
+       for _, q := range queryTerms {
+               if len(q) < 5 {
+                       continue
+               }
+
+               // Quick exact substring check — if already matched, no need to 
fuzzy.
+               if strings.Contains(nameLower, q) {
+                       continue
+               }
+
+               bestDist := distMax + 1
+               for _, w := range nameWords {
+                       if len(w) < 4 {
+                               continue
+                       }
+                       d := levenshteinDistance(q, w)
+                       if d < bestDist && d <= distMax {
+                               bestDist = d
+                       }
+               }
+
+               if bestDist > 0 && bestDist <= distMax {
+                       bonus += float64(distMax-bestDist+1) * 0.8
+               }
+       }
+
+       return bonus
+}
+
+func levenshteinDistance(a, b string) int {
+       aLen := len(a)
+       bLen := len(b)
+
+       if aLen == 0 {
+               return bLen
+       }
+       if bLen == 0 {
+               return aLen
+       }
+
+       dp := make([]int, bLen+1)
+       for j := 0; j <= bLen; j++ {
+               dp[j] = j
+       }
+
+       for i := 1; i <= aLen; i++ {
+               prevDiag := dp[0]
+               dp[0] = i
+               for j := 1; j <= bLen; j++ {
+                       temp := dp[j]
+                       if a[i-1] == b[j-1] {
+                               dp[j] = prevDiag
+                       } else {
+                               m := dp[j-1] + 1
+                               if d := dp[j] + 1; d < m {
+                                       m = d
+                               }
+                               if r := prevDiag + 1; r < m {
+                                       m = r
+                               }
+                               dp[j] = m
+                       }
+                       prevDiag = temp
+               }
+       }
+
+       return dp[bLen]
+}
+
+func expandAliases(s string) string {
+       s = strings.ToLower(s)
+       for alias, expansion := range aliasMap {
+               padded := " " + s + " "
+               s = strings.ReplaceAll(padded, " "+alias+" ", " "+expansion+" ")
+       }
+       return strings.Trim(s, " ")
+}
+
+func tokenize(s string) []string {
+       lower := strings.ToLower(s)
+       fields := strings.Fields(lower)
+       cleaned := make([]string, 0, len(fields))
+       for _, f := range fields {
+               f = strings.Trim(f, ".,:;()'\"")
+               if f != "" {
+                       cleaned = append(cleaned, f)
+               }
+       }
+       return cleaned
+}
+
+func termFreq(words []string) map[string]float64 {
+       counts := make(map[string]int)
+       for _, w := range words {
+               counts[w]++
+       }
+       tf := make(map[string]float64)
+       n := float64(len(words))
+       if n == 0 {
+               return tf
+       }
+       for w, c := range counts {
+               tf[w] = float64(c) / n
+       }
+       return tf
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/arkade-0.11.102/cmd/search_test.go 
new/arkade-0.11.105/cmd/search_test.go
--- old/arkade-0.11.102/cmd/search_test.go      1970-01-01 01:00:00.000000000 
+0100
+++ new/arkade-0.11.105/cmd/search_test.go      2026-06-30 13:53:49.000000000 
+0200
@@ -0,0 +1,90 @@
+package cmd
+
+import (
+       "testing"
+
+       "github.com/alexellis/arkade/pkg/get"
+)
+
+func makeTestTools() []get.Tool {
+       return []get.Tool{
+               {Name: "helm", Owner: "helm", Repo: "helm", Description: "The 
Kubernetes Package Manager"},
+               {Name: "faas-cli", Owner: "openfaas", Repo: "faas-cli", 
Description: "CLI for OpenFaaS"},
+               {Name: "kubectl", Owner: "kubernetes", Repo: "kubernetes", 
Description: "Control plane CLI"},
+       }
+}
+
+func Test_ExactNameMatchRanksFirst(t *testing.T) {
+       ranked := rankByTFIDF(makeTestTools(), "helm")
+
+       if ranked[0].Tool.Name != "helm" {
+               t.Errorf("expected helm to rank first, got %s", 
ranked[0].Tool.Name)
+       }
+}
+
+func Test_MultiWordQueryBoost(t *testing.T) {
+       ranked := rankByTFIDF(makeTestTools(), "kubernetes package")
+
+       var found int
+       for _, r := range ranked {
+               if r.Tool.Name == "helm" && r.Score > 0 {
+                       found++
+               }
+       }
+       if found != 1 {
+               t.Error("expected helm to match 'kubernetes package' query")
+       }
+}
+
+func Test_OwnerRepoInTFIDF(t *testing.T) {
+       ranked := rankByTFIDF(makeTestTools(), "openfaas")
+
+       var found bool
+       for _, r := range ranked {
+               if r.Tool.Name == "faas-cli" && r.Score > 0 {
+                       found = true
+                       break
+               }
+       }
+       if !found {
+               t.Error("expected faas-cli to match 'openfaas' via Owner field")
+       }
+}
+
+func Test_FallbackFiresWhenTFIDFFails(t *testing.T) {
+       matches := fuzzySubstringFallback(makeTestTools(), []string{"kube"})
+
+       var found bool
+       for _, r := range matches {
+               if r.Tool.Name == "kubectl" && r.Score > 0 {
+                       found = true
+                       break
+               }
+       }
+       if !found {
+               t.Error("expected kubectl to match 'kube' via substring 
fallback on Owner")
+       }
+}
+
+func Test_LevenshteinFuzzyNearMiss(t *testing.T) {
+       score := levenshteinFuzzyScore([]string{"faasd"}, "faas-cli")
+       if score <= 0 {
+               t.Error("expected positive fuzzy score for 'faasd' vs 
'faas-cli'")
+       }
+}
+
+func Test_FallbackReturnsEmptyForNoMatch(t *testing.T) {
+       matches := fuzzySubstringFallback(makeTestTools(), 
[]string{"xyznonexistent"})
+       if len(matches) != 0 {
+               t.Errorf("expected no fallback matches, got %d", len(matches))
+       }
+}
+
+func Test_FallbackSortedByScore(t *testing.T) {
+       matches := fuzzySubstringFallback(makeTestTools(), []string{"cli"})
+       for i := 1; i < len(matches); i++ {
+               if matches[i].Score > matches[i-1].Score {
+                       t.Errorf("results not sorted by score: %.2f > %.2f", 
matches[i].Score, matches[i-1].Score)
+               }
+       }
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/arkade-0.11.102/cmd/system/actions_runner.go 
new/arkade-0.11.105/cmd/system/actions_runner.go
--- old/arkade-0.11.102/cmd/system/actions_runner.go    2026-06-22 
16:31:10.000000000 +0200
+++ new/arkade-0.11.105/cmd/system/actions_runner.go    2026-06-30 
13:53:49.000000000 +0200
@@ -100,7 +100,7 @@
                fmt.Printf("Unpacking Actions Runner to: %s\n", 
path.Join(installPath, "actions-runner"))
 
                if err := spinWhile("Unpacking Actions Runner", func() error {
-                       return archive.UntarNested(f, installPath, true, true, 
true)
+                       return archive.UntarNested(f, installPath, true, true, 
true, false)
                }); err != nil {
                        return err
                }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/arkade-0.11.102/cmd/system/containerd.go 
new/arkade-0.11.105/cmd/system/containerd.go
--- old/arkade-0.11.102/cmd/system/containerd.go        2026-06-22 
16:31:10.000000000 +0200
+++ new/arkade-0.11.105/cmd/system/containerd.go        2026-06-30 
13:53:49.000000000 +0200
@@ -120,7 +120,7 @@
                tempDirName := os.TempDir() + "/containerd"
 
                if err := spinWhile("Unpacking containerd", func() error {
-                       return archive.UntarNested(f, tempDirName, true, true, 
true)
+                       return archive.UntarNested(f, tempDirName, true, true, 
true, false)
                }); err != nil {
                        return err
                }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/arkade-0.11.102/cmd/system/go.go 
new/arkade-0.11.105/cmd/system/go.go
--- old/arkade-0.11.102/cmd/system/go.go        2026-06-22 16:31:10.000000000 
+0200
+++ new/arkade-0.11.105/cmd/system/go.go        2026-06-30 13:53:49.000000000 
+0200
@@ -96,7 +96,7 @@
                fmt.Printf("Unpacking Go to: %s\n", path.Join(installPath, 
"go"))
 
                if err := spinWhile("Unpacking Go", func() error {
-                       return archive.UntarNested(f, installPath, true, true, 
true)
+                       return archive.UntarNested(f, installPath, true, true, 
true, false)
                }); err != nil {
                        return err
                }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/arkade-0.11.102/cmd/system/node.go 
new/arkade-0.11.105/cmd/system/node.go
--- old/arkade-0.11.102/cmd/system/node.go      2026-06-22 16:31:10.000000000 
+0200
+++ new/arkade-0.11.105/cmd/system/node.go      2026-06-30 13:53:49.000000000 
+0200
@@ -149,7 +149,7 @@
                        fmt.Printf("Unpacking binaries to: %s\n", 
tempUnpackPath)
                }
                if err = spinWhile("Unpacking Node.js", func() error {
-                       return archive.UntarNested(f, tempUnpackPath, true, 
true, true)
+                       return archive.UntarNested(f, tempUnpackPath, true, 
true, true, false)
                }); err != nil {
                        return err
                }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/arkade-0.11.102/cmd/system/registry.go 
new/arkade-0.11.105/cmd/system/registry.go
--- old/arkade-0.11.102/cmd/system/registry.go  2026-06-22 16:31:10.000000000 
+0200
+++ new/arkade-0.11.105/cmd/system/registry.go  2026-06-30 13:53:49.000000000 
+0200
@@ -135,7 +135,7 @@
                        tempDirName := fmt.Sprintf("%s/%s", os.TempDir(), 
toolName)
                        defer os.RemoveAll(tempDirName)
                        if err := spinWhile("Unpacking "+toolName, func() error 
{
-                               return archive.UntarNested(f, tempDirName, 
true, true, true)
+                               return archive.UntarNested(f, tempDirName, 
true, true, true, false)
                        }); err != nil {
                                return err
                        }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/arkade-0.11.102/main.go new/arkade-0.11.105/main.go
--- old/arkade-0.11.102/main.go 2026-06-22 16:31:10.000000000 +0200
+++ new/arkade-0.11.105/main.go 2026-06-30 13:53:49.000000000 +0200
@@ -52,6 +52,7 @@
        rootCmd.AddCommand(gha.MakeGHA())
        rootCmd.AddCommand(system.MakeSystem())
        rootCmd.AddCommand(oci.MakeOci())
+       rootCmd.AddCommand(cmd.MakeSearch())
 
        if err := rootCmd.Execute(); err != nil {
                os.Exit(1)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/arkade-0.11.102/pkg/archive/untar_nested.go 
new/arkade-0.11.105/pkg/archive/untar_nested.go
--- old/arkade-0.11.102/pkg/archive/untar_nested.go     2026-06-22 
16:31:10.000000000 +0200
+++ new/arkade-0.11.105/pkg/archive/untar_nested.go     2026-06-30 
13:53:49.000000000 +0200
@@ -15,14 +15,18 @@
 // UntarNested reads the gzip-compressed tar file from r and writes it into 
dir.
 // When allowSymlinks is false, any symlink entry in the archive causes an
 // error; when true, symlinks are extracted subject to containment checks.
+// When flatExtract is true, all files are extracted directly into dir using
+// only their basename, ignoring the archive's directory structure (e.g.
+// usr/local/bin/foo -> dir/foo). This is analogous to tar's 
--strip-components,
+// but strips all levels in one go.
 // Copyright 2017 The Go Authors. All rights reserved.
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
-func UntarNested(r io.Reader, dir string, gzipped, quiet, allowSymlinks bool) 
error {
-       return untarNested(r, dir, gzipped, quiet, allowSymlinks)
+func UntarNested(r io.Reader, dir string, gzipped, quiet, allowSymlinks, 
flatExtract bool) error {
+       return untarNested(r, dir, gzipped, quiet, allowSymlinks, flatExtract)
 }
 
-func untarNested(r io.Reader, dir string, gzipped, quiet, allowSymlinks bool) 
(err error) {
+func untarNested(r io.Reader, dir string, gzipped, quiet, allowSymlinks, 
flatExtract bool) (err error) {
        t0 := time.Now()
        nFiles := 0
        madeDir := map[string]bool{}
@@ -59,6 +63,7 @@
        cleanDir := filepath.Clean(dir)
 
        tr := tar.NewReader(r)
+
        loggedChtimesError := false
        for {
                f, err := tr.Next()
@@ -72,7 +77,11 @@
                if !validRelPath(f.Name) {
                        return fmt.Errorf("tar contained invalid name error 
%q", f.Name)
                }
-               rel := filepath.FromSlash(f.Name)
+               name := f.Name
+               if flatExtract {
+                       name = filepath.Base(name)
+               }
+               rel := filepath.FromSlash(name)
                abs := filepath.Join(dir, rel)
 
                fi := f.FileInfo()
@@ -141,6 +150,9 @@
                        }
                        nFiles++
                case mode.IsDir():
+                       if flatExtract {
+                               continue
+                       }
                        // Guard before MkdirAll, as with regular files.
                        if err := assertExistingPrefixWithinRoot(cleanDir, 
abs); err != nil {
                                return err
@@ -150,6 +162,10 @@
                        }
                        madeDir[abs] = true
                case mode.Type() == os.ModeSymlink:
+                       if flatExtract {
+                               log.Printf("skipping symlink %q during flat 
extraction (target may not resolve correctly)", f.Name)
+                               continue
+                       }
                        if !allowSymlinks {
                                return fmt.Errorf("tar file entry %s is a 
symlink, but symlink extraction is disabled", f.Name)
                        }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/arkade-0.11.102/pkg/archive/untar_nested_test.go 
new/arkade-0.11.105/pkg/archive/untar_nested_test.go
--- old/arkade-0.11.102/pkg/archive/untar_nested_test.go        2026-06-22 
16:31:10.000000000 +0200
+++ new/arkade-0.11.105/pkg/archive/untar_nested_test.go        2026-06-30 
13:53:49.000000000 +0200
@@ -60,7 +60,7 @@
                {hdr: tar.Header{Name: "escape-link/escape.txt", Typeflag: 
tar.TypeReg, Mode: 0644}, body: []byte("escaped\n")},
        })
 
-       if err := UntarNested(bytes.NewReader(data), installDir, false, true, 
true); err == nil {
+       if err := UntarNested(bytes.NewReader(data), installDir, false, true, 
true, false); err == nil {
                t.Fatal("want error, got nil")
        }
 
@@ -92,7 +92,7 @@
                {hdr: tar.Header{Name: "escape-link/escape.txt", Typeflag: 
tar.TypeReg, Mode: 0644}, body: []byte("escaped\n")},
        })
 
-       if err := UntarNested(bytes.NewReader(data), installDir, false, true, 
true); err == nil {
+       if err := UntarNested(bytes.NewReader(data), installDir, false, true, 
true, false); err == nil {
                t.Fatal("want error, got nil")
        }
 
@@ -121,7 +121,7 @@
                {hdr: tar.Header{Name: "hop1/hop2/outside/escape.txt", 
Typeflag: tar.TypeReg, Mode: 0644}, body: []byte("escaped\n")},
        })
 
-       if err := UntarNested(bytes.NewReader(data), installDir, false, true, 
true); err == nil {
+       if err := UntarNested(bytes.NewReader(data), installDir, false, true, 
true, false); err == nil {
                t.Fatal("want error, got nil")
        }
 
@@ -149,7 +149,7 @@
                {hdr: tar.Header{Name: "hop1/hop2", Typeflag: tar.TypeSymlink, 
Linkname: "..", Mode: 0777}},
        })
 
-       if err := UntarNested(bytes.NewReader(data), installDir, false, true, 
true); err == nil {
+       if err := UntarNested(bytes.NewReader(data), installDir, false, true, 
true, false); err == nil {
                t.Fatal("want error, got nil")
        }
 
@@ -188,7 +188,7 @@
                {hdr: tar.Header{Name: "planted", Typeflag: tar.TypeSymlink, 
Linkname: "safe/file", Mode: 0777}},
        })
 
-       if err := UntarNested(bytes.NewReader(data), installDir, false, true, 
true); err == nil {
+       if err := UntarNested(bytes.NewReader(data), installDir, false, true, 
true, false); err == nil {
                t.Fatalf("expected extraction to be rejected, got nil error")
        }
 
@@ -213,7 +213,7 @@
                {hdr: tar.Header{Name: "README.md", Typeflag: tar.TypeReg, 
Mode: 0644}, body: []byte("hello\n")},
        })
 
-       if err := UntarNested(bytes.NewReader(data), installDir, false, true, 
true); err != nil {
+       if err := UntarNested(bytes.NewReader(data), installDir, false, true, 
true, false); err != nil {
                t.Fatalf("expected clean extraction, got: %v", err)
        }
        for _, rel := range []string{"bin/tool", "README.md"} {
@@ -238,7 +238,7 @@
                {hdr: tar.Header{Name: "link/file.txt", Typeflag: tar.TypeReg, 
Mode: 0644}, body: []byte("hello\n")},
        })
 
-       if err := UntarNested(bytes.NewReader(data), installDir, false, true, 
true); err != nil {
+       if err := UntarNested(bytes.NewReader(data), installDir, false, true, 
true, false); err != nil {
                t.Fatalf("expected clean extraction with write through internal 
symlink, got: %v", err)
        }
        if _, err := os.Stat(filepath.Join(installDir, "subdir", "file.txt")); 
err != nil {
@@ -260,7 +260,7 @@
                {hdr: tar.Header{Name: "link/newdir", Typeflag: tar.TypeDir, 
Mode: 0755}},
        })
 
-       if err := UntarNested(bytes.NewReader(data), installDir, false, true, 
true); err != nil {
+       if err := UntarNested(bytes.NewReader(data), installDir, false, true, 
true, false); err != nil {
                t.Fatalf("expected clean extraction with dir write-through 
internal symlink, got: %v", err)
        }
        if _, err := os.Stat(filepath.Join(installDir, "subdir", "newdir")); 
err != nil {
@@ -296,7 +296,7 @@
                {hdr: tar.Header{Name: "link/subdir/escape.txt", Typeflag: 
tar.TypeReg, Mode: 0644}, body: []byte("escaped\n")},
        })
 
-       if err := UntarNested(bytes.NewReader(data), installDir, false, true, 
true); err == nil {
+       if err := UntarNested(bytes.NewReader(data), installDir, false, true, 
true, false); err == nil {
                t.Fatal("want error, got nil")
        }
 
@@ -332,7 +332,7 @@
                {hdr: tar.Header{Name: "evil", Typeflag: tar.TypeReg, Mode: 
0644}, body: []byte("HACKED")},
        })
 
-       if err := UntarNested(bytes.NewReader(data), installDir, false, true, 
true); err == nil {
+       if err := UntarNested(bytes.NewReader(data), installDir, false, true, 
true, false); err == nil {
                t.Fatal("want error, got nil")
        }
 
@@ -354,7 +354,7 @@
                {hdr: tar.Header{Name: "tool", Typeflag: tar.TypeSymlink, 
Linkname: "tool-v1", Mode: 0777}},
        })
 
-       if err := UntarNested(bytes.NewReader(data), installDir, false, true, 
true); err != nil {
+       if err := UntarNested(bytes.NewReader(data), installDir, false, true, 
true, false); err != nil {
                t.Fatalf("expected clean extraction with internal symlink, got: 
%v", err)
        }
        linkPath := filepath.Join(installDir, "tool")
@@ -381,7 +381,7 @@
                {hdr: tar.Header{Name: "nested/link", Typeflag: 
tar.TypeSymlink, Linkname: "tool-v1", Mode: 0777}},
        })
 
-       if err := UntarNested(bytes.NewReader(data), installDir, false, true, 
true); err != nil {
+       if err := UntarNested(bytes.NewReader(data), installDir, false, true, 
true, false); err != nil {
                t.Fatalf("expected clean extraction with on-demand parent dir 
for symlink, got: %v", err)
        }
        linkPath := filepath.Join(installDir, "nested", "link")
@@ -408,10 +408,49 @@
                {hdr: tar.Header{Name: "tool", Typeflag: tar.TypeSymlink, 
Linkname: "tool-v1", Mode: 0777}},
        })
 
-       if err := UntarNested(bytes.NewReader(data), installDir, false, true, 
false); err == nil {
+       if err := UntarNested(bytes.NewReader(data), installDir, false, true, 
false, false); err == nil {
                t.Fatalf("expected error when extracting symlink with symlinks 
disabled, got nil")
        }
        if _, err := os.Lstat(filepath.Join(installDir, "tool")); err == nil {
                t.Fatalf("expected symlink not to be created when symlinks 
disabled")
        }
 }
+
+// Flat extraction should place all files at the top level of the install dir,
+// skip directory entries, and skip symlink entries.
+func Test_UntarNested_FlatExtraction(t *testing.T) {
+       installDir, err := os.MkdirTemp("", "arkade-untar-flat-*")
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer os.RemoveAll(installDir)
+
+       data := buildTar(t, []tarEntry{
+               {hdr: tar.Header{Name: "usr/local/bin/", Typeflag: tar.TypeDir, 
Mode: 0755}},
+               {hdr: tar.Header{Name: "usr/local/bin/slicer", Typeflag: 
tar.TypeReg, Mode: 0755}, body: []byte("bin\n")},
+               {hdr: tar.Header{Name: "usr/share/man/slicer.1.gz", Typeflag: 
tar.TypeReg, Mode: 0644}, body: []byte("man\n")},
+               {hdr: tar.Header{Name: "link", Typeflag: tar.TypeSymlink, 
Linkname: "slicer", Mode: 0777}},
+       })
+
+       if err := UntarNested(bytes.NewReader(data), installDir, false, true, 
true, true); err != nil {
+               t.Fatalf("expected clean flat extraction, got: %v", err)
+       }
+
+       // Files should be at top level.
+       for _, rel := range []string{"slicer", "slicer.1.gz"} {
+               if _, err := os.Stat(filepath.Join(installDir, rel)); err != 
nil {
+                       t.Fatalf("expected %q to exist: %v", rel, err)
+               }
+       }
+
+       // No nested directories should remain.
+       files, _ := filepath.Glob(filepath.Join(installDir, "usr"))
+       if len(files) > 0 {
+               t.Fatalf("expected no nested dirs with flat extraction, found: 
%v", files)
+       }
+
+       // Symlink should not be created.
+       if _, err := os.Lstat(filepath.Join(installDir, "link")); err == nil {
+               t.Fatal("expected symlink to be skipped in flat mode")
+       }
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/arkade-0.11.102/pkg/get/get_test.go 
new/arkade-0.11.105/pkg/get/get_test.go
--- old/arkade-0.11.102/pkg/get/get_test.go     2026-06-22 16:31:10.000000000 
+0200
+++ new/arkade-0.11.105/pkg/get/get_test.go     2026-06-30 13:53:49.000000000 
+0200
@@ -8147,45 +8147,6 @@
        }
 }
 
-func Test_DownloadFaasd(t *testing.T) {
-       tools := MakeTools()
-       name := "faasd"
-       const version = "0.18.8"
-
-       tool := getTool(name, tools)
-
-       tests := []test{
-               {
-                       os:      "linux",
-                       arch:    arch64bit,
-                       version: version,
-                       url:     
`https://github.com/openfaas/faasd/releases/download/0.18.8/faasd`,
-               },
-               {
-                       os:      "linux",
-                       arch:    archARM64,
-                       version: version,
-                       url:     
`https://github.com/openfaas/faasd/releases/download/0.18.8/faasd-arm64`,
-               },
-               {
-                       os:      "linux",
-                       arch:    archARM7,
-                       version: version,
-                       url:     
`https://github.com/openfaas/faasd/releases/download/0.18.8/faasd-armhf`,
-               },
-       }
-
-       for _, tc := range tests {
-               got, _, err := tool.GetURL(tc.os, tc.arch, tc.version, false)
-               if err != nil {
-                       t.Fatal(err)
-               }
-               if got != tc.url {
-                       t.Errorf("want: %s, got: %s", tc.url, got)
-               }
-       }
-}
-
 func Test_DownloadKubeScore(t *testing.T) {
        tools := MakeTools()
        name := "kube-score"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/arkade-0.11.102/pkg/get/tools.go 
new/arkade-0.11.105/pkg/get/tools.go
--- old/arkade-0.11.102/pkg/get/tools.go        2026-06-22 16:31:10.000000000 
+0200
+++ new/arkade-0.11.105/pkg/get/tools.go        2026-06-30 13:53:49.000000000 
+0200
@@ -4545,25 +4545,6 @@
 
        tools = append(tools,
                Tool{
-                       Owner:       "openfaas",
-                       Repo:        "faasd",
-                       Name:        "faasd",
-                       Description: "faasd - a lightweight & portable faas 
engine",
-                       BinaryTemplate: `
-                               {{$arch := ""}}
-
-                                       {{- if or (eq .Arch "aarch64") (eq 
.Arch "arm64") -}}
-                                       {{$arch = "-arm64"}}
-                                       {{- else if or (eq .Arch "armv6l") (eq 
.Arch "armv7l") -}}
-                                       {{$arch = "-armhf"}}
-                                       {{- end -}}
-
-                                       {{.Name}}{{$arch}}
-                                       `,
-               })
-
-       tools = append(tools,
-               Tool{
                        Owner:       "zegl",
                        Repo:        "kube-score",
                        Name:        "kube-score",

++++++ arkade.obsinfo ++++++
--- /var/tmp/diff_new_pack.CrtcEo/_old  2026-07-01 16:55:00.299316295 +0200
+++ /var/tmp/diff_new_pack.CrtcEo/_new  2026-07-01 16:55:00.335317538 +0200
@@ -1,5 +1,5 @@
 name: arkade
-version: 0.11.102
-mtime: 1782138670
-commit: 37af31c7b58de8f16a10067051e3bbe7ecb6aa79
+version: 0.11.105
+mtime: 1782820429
+commit: 15dcd3c06fd80143553e9fc53fb2ef59fb84a409
 

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

Reply via email to