mirror of
https://github.com/lukaszraczylo/kportal.git
synced 2026-06-30 05:44:37 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c2528f7eec | |||
| a37574371f | |||
| 0bab59d97d | |||
| 7acac8e885 | |||
| 87ce85b07b | |||
| d5125a0d62 | |||
| ab65c2e17b |
@@ -3,3 +3,7 @@ CLAUDE.md
|
|||||||
DEPLOYMENT_SUMMARY.md
|
DEPLOYMENT_SUMMARY.md
|
||||||
HOMEBREW_COMPLIANCE.md
|
HOMEBREW_COMPLIANCE.md
|
||||||
RELEASE_SETUP.md
|
RELEASE_SETUP.md
|
||||||
|
|
||||||
|
# Local/live test configs (cluster-specific, never committed)
|
||||||
|
.kportal.test.yaml
|
||||||
|
.kportal.*.local.yaml
|
||||||
|
|||||||
@@ -490,6 +490,18 @@ make install # Install locally
|
|||||||
|
|
||||||
See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
||||||
|
|
||||||
|
## Telemetry
|
||||||
|
|
||||||
|
On startup this binary sends a single anonymous adoption ping — project name,
|
||||||
|
version, timestamp; no identifiers, no command output, no payload contents.
|
||||||
|
Fire-and-forget with a 2-second timeout; cannot block startup or panic.
|
||||||
|
|
||||||
|
See **[oss-telemetry — Disabling telemetry](https://github.com/lukaszraczylo/oss-telemetry#disabling-telemetry)**
|
||||||
|
for the exact wire format, source, and full opt-out documentation.
|
||||||
|
|
||||||
|
Quick opt-out: set any of `DO_NOT_TRACK=1`, `OSS_TELEMETRY_DISABLED=1`,
|
||||||
|
or `KPORTAL_DISABLE_TELEMETRY=1`.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT License - see [LICENSE](LICENSE).
|
MIT License - see [LICENSE](LICENSE).
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/lukaszraczylo/kportal/internal/complete"
|
||||||
|
)
|
||||||
|
|
||||||
|
// completionCmd handles shell completion generation and installation
|
||||||
|
func completionCmd(args []string) int {
|
||||||
|
fs := flag.NewFlagSet("completion", flag.ContinueOnError)
|
||||||
|
fs.SetOutput(os.Stderr)
|
||||||
|
|
||||||
|
var (
|
||||||
|
installFlag bool
|
||||||
|
shellFlag string
|
||||||
|
uninstall bool
|
||||||
|
)
|
||||||
|
|
||||||
|
fs.BoolVar(&installFlag, "install", false, "Install completions for the shell")
|
||||||
|
fs.BoolVar(&uninstall, "uninstall", false, "Uninstall completions")
|
||||||
|
fs.StringVar(&shellFlag, "shell", "", "Shell type: bash, zsh, or fish (auto-detected if empty)")
|
||||||
|
|
||||||
|
if err := fs.Parse(args); err != nil {
|
||||||
|
if err == flag.ErrHelp {
|
||||||
|
printCompletionHelp()
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine shell type
|
||||||
|
var shell complete.Shell
|
||||||
|
if shellFlag != "" {
|
||||||
|
switch shellFlag {
|
||||||
|
case "bash":
|
||||||
|
shell = complete.ShellBash
|
||||||
|
case "zsh":
|
||||||
|
shell = complete.ShellZsh
|
||||||
|
case "fish":
|
||||||
|
shell = complete.ShellFish
|
||||||
|
default:
|
||||||
|
fprintf(os.Stderr, "Error: unknown shell %q (use bash, zsh, or fish)\n", shellFlag)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
shell = complete.AutoDetectShell()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle uninstall
|
||||||
|
if uninstall {
|
||||||
|
installer := complete.NewInstaller(shell)
|
||||||
|
if err := installer.Uninstall(); err != nil {
|
||||||
|
fprintf(os.Stderr, "Error uninstalling completions: %v\n", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
fmt.Println("✅ Completions uninstalled")
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle install
|
||||||
|
if installFlag {
|
||||||
|
if err := complete.InstallCompletions(shell); err != nil {
|
||||||
|
fprintf(os.Stderr, "Error installing completions: %v\n", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print completion script to stdout
|
||||||
|
if err := complete.Print(shell); err != nil {
|
||||||
|
fprintf(os.Stderr, "Error generating completions: %v\n", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func printCompletionHelp() {
|
||||||
|
fprintf(os.Stdout, `Generate shell completions for kportal.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
kportal completion [flags]
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
--install Install completions for the current shell
|
||||||
|
--uninstall Remove installed completions
|
||||||
|
--shell <type> Shell type: bash, zsh, or fish (auto-detected)
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
# Generate and source completions (bash)
|
||||||
|
source <(kportal completion)
|
||||||
|
|
||||||
|
# Install completions (requires shell restart)
|
||||||
|
kportal completion --install
|
||||||
|
|
||||||
|
# Install for specific shell
|
||||||
|
kportal completion --install --shell zsh
|
||||||
|
|
||||||
|
# Uninstall completions
|
||||||
|
kportal completion --uninstall
|
||||||
|
|
||||||
|
Shell-specific setup:
|
||||||
|
|
||||||
|
Bash (~/.bashrc):
|
||||||
|
source <(kportal completion)
|
||||||
|
|
||||||
|
Zsh (~/.zshrc):
|
||||||
|
autoload -Uz compinit && compinit
|
||||||
|
source <(kportal completion)
|
||||||
|
|
||||||
|
Fish (~/.config/fish/config.fish):
|
||||||
|
kportal completion --install --shell fish
|
||||||
|
# Or manually:
|
||||||
|
kportal completion --shell fish > ~/.config/fish/completions/kportal.fish
|
||||||
|
`)
|
||||||
|
}
|
||||||
+10
-5
@@ -117,9 +117,14 @@ func runMain() int {
|
|||||||
// of long-running modes (headless, verbose-loop, interactive).
|
// of long-running modes (headless, verbose-loop, interactive).
|
||||||
func run(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
func run(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
||||||
// Subcommand dispatch must run BEFORE the main flag set is parsed because
|
// Subcommand dispatch must run BEFORE the main flag set is parsed because
|
||||||
// generate has its own FlagSet and must not see kportal's top-level flags.
|
// generate and completion have their own FlagSets and must not see kportal's top-level flags.
|
||||||
if len(args) >= 1 && args[0] == "generate" {
|
if len(args) >= 1 {
|
||||||
return runGenerate(args[1:])
|
switch args[0] {
|
||||||
|
case "generate":
|
||||||
|
return runGenerate(args[1:])
|
||||||
|
case "completion":
|
||||||
|
return completionCmd(args[1:])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
opts, code, handled := parseFlags(args, stderr)
|
opts, code, handled := parseFlags(args, stderr)
|
||||||
@@ -498,7 +503,7 @@ func runVerboseTable(ctx context.Context, opts runOptions, cfg *config.Config, d
|
|||||||
// Background update check (best effort).
|
// Background update check (best effort).
|
||||||
go func() {
|
go func() {
|
||||||
checker := version.NewChecker(githubOwner, githubRepo, appVersion)
|
checker := version.NewChecker(githubOwner, githubRepo, appVersion)
|
||||||
uctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
uctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
if update := checker.CheckForUpdate(uctx); update != nil {
|
if update := checker.CheckForUpdate(uctx); update != nil {
|
||||||
log.Printf("Update available: v%s (current: v%s) - %s",
|
log.Printf("Update available: v%s (current: v%s) - %s",
|
||||||
@@ -590,7 +595,7 @@ func runInteractive(ctx context.Context, opts runOptions, cfg *config.Config, de
|
|||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
checker := version.NewChecker(githubOwner, githubRepo, appVersion)
|
checker := version.NewChecker(githubOwner, githubRepo, appVersion)
|
||||||
uctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
uctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
if update := checker.CheckForUpdate(uctx); update != nil {
|
if update := checker.CheckForUpdate(uctx); update != nil {
|
||||||
bubbleTeaUI.SetUpdateAvailable(update.LatestVersion, update.ReleaseURL)
|
bubbleTeaUI.SetUpdateAvailable(update.LatestVersion, update.ReleaseURL)
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ require (
|
|||||||
github.com/fsnotify/fsnotify v1.10.1
|
github.com/fsnotify/fsnotify v1.10.1
|
||||||
github.com/go-logr/logr v1.4.3
|
github.com/go-logr/logr v1.4.3
|
||||||
github.com/grandcat/zeroconf v1.0.0
|
github.com/grandcat/zeroconf v1.0.0
|
||||||
github.com/lukaszraczylo/oss-telemetry v0.0.0-20260521005811-e02d51419c52
|
github.com/lukaszraczylo/oss-telemetry v0.2.3
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
k8s.io/api v0.36.0
|
k8s.io/api v0.36.2
|
||||||
k8s.io/apimachinery v0.36.0
|
k8s.io/apimachinery v0.36.2
|
||||||
k8s.io/client-go v0.36.0
|
k8s.io/client-go v0.36.2
|
||||||
k8s.io/klog/v2 v2.140.0
|
k8s.io/klog/v2 v2.140.0
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -30,20 +30,19 @@ require (
|
|||||||
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
|
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||||
github.com/fxamacker/cbor/v2 v2.9.2 // indirect
|
github.com/fxamacker/cbor/v2 v2.9.2 // indirect
|
||||||
github.com/go-openapi/jsonpointer v0.23.1 // indirect
|
github.com/go-openapi/jsonpointer v0.24.0 // indirect
|
||||||
github.com/go-openapi/jsonreference v0.21.5 // indirect
|
github.com/go-openapi/jsonreference v0.21.6 // indirect
|
||||||
github.com/go-openapi/swag v0.26.0 // indirect
|
github.com/go-openapi/swag v0.27.0 // indirect
|
||||||
github.com/go-openapi/swag/cmdutils v0.26.0 // indirect
|
github.com/go-openapi/swag/cmdutils v0.27.0 // indirect
|
||||||
github.com/go-openapi/swag/conv v0.26.0 // indirect
|
github.com/go-openapi/swag/conv v0.27.0 // indirect
|
||||||
github.com/go-openapi/swag/fileutils v0.26.0 // indirect
|
github.com/go-openapi/swag/fileutils v0.27.0 // indirect
|
||||||
github.com/go-openapi/swag/jsonname v0.26.0 // indirect
|
github.com/go-openapi/swag/jsonutils v0.27.0 // indirect
|
||||||
github.com/go-openapi/swag/jsonutils v0.26.0 // indirect
|
github.com/go-openapi/swag/loading v0.27.0 // indirect
|
||||||
github.com/go-openapi/swag/loading v0.26.0 // indirect
|
github.com/go-openapi/swag/mangling v0.27.0 // indirect
|
||||||
github.com/go-openapi/swag/mangling v0.26.0 // indirect
|
github.com/go-openapi/swag/netutils v0.27.0 // indirect
|
||||||
github.com/go-openapi/swag/netutils v0.26.0 // indirect
|
github.com/go-openapi/swag/stringutils v0.27.0 // indirect
|
||||||
github.com/go-openapi/swag/stringutils v0.26.0 // indirect
|
github.com/go-openapi/swag/typeutils v0.27.0 // indirect
|
||||||
github.com/go-openapi/swag/typeutils v0.26.0 // indirect
|
github.com/go-openapi/swag/yamlutils v0.27.0 // indirect
|
||||||
github.com/go-openapi/swag/yamlutils v0.26.0 // indirect
|
|
||||||
github.com/google/gnostic-models v0.7.1 // indirect
|
github.com/google/gnostic-models v0.7.1 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
|
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
|
||||||
@@ -52,7 +51,7 @@ require (
|
|||||||
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
|
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.22 // indirect
|
github.com/mattn/go-isatty v0.0.22 // indirect
|
||||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.23 // indirect
|
github.com/mattn/go-runewidth v0.0.24 // indirect
|
||||||
github.com/miekg/dns v1.1.72 // indirect
|
github.com/miekg/dns v1.1.72 // indirect
|
||||||
github.com/moby/spdystream v0.5.1 // indirect
|
github.com/moby/spdystream v0.5.1 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
@@ -68,21 +67,21 @@ require (
|
|||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
golang.org/x/mod v0.35.0 // indirect
|
golang.org/x/mod v0.37.0 // indirect
|
||||||
golang.org/x/net v0.53.0 // indirect
|
golang.org/x/net v0.56.0 // indirect
|
||||||
golang.org/x/oauth2 v0.36.0 // indirect
|
golang.org/x/oauth2 v0.36.0 // indirect
|
||||||
golang.org/x/sync v0.20.0 // indirect
|
golang.org/x/sync v0.21.0 // indirect
|
||||||
golang.org/x/sys v0.43.0 // indirect
|
golang.org/x/sys v0.46.0 // indirect
|
||||||
golang.org/x/term v0.42.0 // indirect
|
golang.org/x/term v0.44.0 // indirect
|
||||||
golang.org/x/text v0.36.0 // indirect
|
golang.org/x/text v0.38.0 // indirect
|
||||||
golang.org/x/time v0.15.0 // indirect
|
golang.org/x/time v0.15.0 // indirect
|
||||||
golang.org/x/tools v0.44.0 // indirect
|
golang.org/x/tools v0.47.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
|
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
|
||||||
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
|
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
|
||||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||||
k8s.io/kube-openapi v0.0.0-20260505163821-33341827b392 // indirect
|
k8s.io/kube-openapi v0.0.0-20260624041617-8f3fa4921821 // indirect
|
||||||
k8s.io/streaming v0.36.0 // indirect
|
k8s.io/streaming v0.36.2 // indirect
|
||||||
k8s.io/utils v0.0.0-20260319190234-28399d86e0b5 // indirect
|
k8s.io/utils v0.0.0-20260626114624-be93311217bd // indirect
|
||||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
|
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
|
||||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||||
sigs.k8s.io/structured-merge-diff/v6 v6.4.0 // indirect
|
sigs.k8s.io/structured-merge-diff/v6 v6.4.0 // indirect
|
||||||
|
|||||||
@@ -39,40 +39,38 @@ github.com/fxamacker/cbor/v2 v2.9.2 h1:X4Ksno9+x3cz0TZv69ec1hxP/+tymuR8PXQJyDwfh
|
|||||||
github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/go-openapi/jsonpointer v0.23.1 h1:1HBACs7XIwR2RcmItfdSFlALhGbe6S92p0ry4d1GWg4=
|
github.com/go-openapi/jsonpointer v0.24.0 h1:AA6mCjHYHmZ+1RU2Js089EaOK/iwXXNwQsTgnsTha2M=
|
||||||
github.com/go-openapi/jsonpointer v0.23.1/go.mod h1:iWRmZTrGn7XwYhtPt/fvdSFj1OfNBngqRT2UG3BxSqY=
|
github.com/go-openapi/jsonpointer v0.24.0/go.mod h1:Z3rw7dWu1p9IgitXCFamSlA5lmDiklEB6vkaxcNZW5Y=
|
||||||
github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE=
|
github.com/go-openapi/jsonreference v0.21.6 h1:NZ5nGfnaM1n4I43Xjm1e5/M2GjOwQwndQz22uhxwD+Y=
|
||||||
github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw=
|
github.com/go-openapi/jsonreference v0.21.6/go.mod h1:xzbgtQ3ZbWxvET3AxdzCJlJt6vkovbf+IfSPJjD0tUY=
|
||||||
github.com/go-openapi/swag v0.26.0 h1:GVDXCmfvhfu1BxiHo8/FA+BbKmhecHnG3varjON5/RI=
|
github.com/go-openapi/swag v0.27.0 h1:8ecSuZlh4NXc3GsmAOqECIYqDTApCWaMe3gO4gjJNEE=
|
||||||
github.com/go-openapi/swag v0.26.0/go.mod h1:82g3193sZJRbocs7bNCqGfIgq8pkuwVwCfhKIRlEQF0=
|
github.com/go-openapi/swag v0.27.0/go.mod h1:Kkgz9Ht0+ul9/aVdFmc9xSyPzUwf/aFF5KiFPBXfSY0=
|
||||||
github.com/go-openapi/swag/cmdutils v0.26.0 h1:iowihOcvq7y4egO8cOq0dmfohz6wfeQ63U1EnuhO2TU=
|
github.com/go-openapi/swag/cmdutils v0.27.0 h1:aIKiqhB29AaP+7xm8/CPg3uOpeHx2SUp6TvMpu/a31Y=
|
||||||
github.com/go-openapi/swag/cmdutils v0.26.0/go.mod h1:Sm1MVFMkF6guJJ+pQqHnQA3N0j9qALV3NxzDSv6bETM=
|
github.com/go-openapi/swag/cmdutils v0.27.0/go.mod h1:Sm1MVFMkF6guJJ+pQqHnQA3N0j9qALV3NxzDSv6bETM=
|
||||||
github.com/go-openapi/swag/conv v0.26.0 h1:5yGGsPYI1ZCva93U0AoKi/iZrNhaJEjr324YVsiD89I=
|
github.com/go-openapi/swag/conv v0.27.0 h1:EKOH4feXrvdo8DbSsXSAqRT8fz1epEnS5O2IfXUOzE8=
|
||||||
github.com/go-openapi/swag/conv v0.26.0/go.mod h1:tpAmIL7X58VPnHHiSO4uE3jBeRamGsFsfdDeDtb5ECE=
|
github.com/go-openapi/swag/conv v0.27.0/go.mod h1:pfiv0uKQTbaGApk8Zs/lZV3uSjmSpa2FO1y183YngN8=
|
||||||
github.com/go-openapi/swag/fileutils v0.26.0 h1:WJoPRvsA7QRiiWluowkLJa9jaYR7FCuxmDvnCgaRRxU=
|
github.com/go-openapi/swag/fileutils v0.27.0 h1:ib5jMUqGq5tY1EyO4inlrabsaeDAleFU+XD1FXQcgp8=
|
||||||
github.com/go-openapi/swag/fileutils v0.26.0/go.mod h1:0WDJ7lp67eNjPMO50wAWYlKvhOb6CQ37rzR7wrgI8Tc=
|
github.com/go-openapi/swag/fileutils v0.27.0/go.mod h1:VvJFZLTZS0AI854gEQz5tk7dBESdLjiNUMSZ/th2ry8=
|
||||||
github.com/go-openapi/swag/jsonname v0.26.0 h1:gV1NFX9M8avo0YSpmWogqfQISigCmpaiNci8cGECU5w=
|
github.com/go-openapi/swag/jsonutils v0.27.0 h1:VYtd9jEQYeU4j8q5vdn5KWotF4vKywhGdMBrALtAsfE=
|
||||||
github.com/go-openapi/swag/jsonname v0.26.0/go.mod h1:urBBR8bZNoDYGr653ynhIx+gTeIz0ARZxHkAPktJK2M=
|
github.com/go-openapi/swag/jsonutils v0.27.0/go.mod h1:U7pb8AGuwhok3RDicHeHwSG4L3PXSq6PAL98Aon632g=
|
||||||
github.com/go-openapi/swag/jsonutils v0.26.0 h1:FawFML2iAXsPqmERscuMPIHmFsoP1tOqWkxBaKNMsnA=
|
github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.0 h1:+d7C7Ur/SsGg/UZ9G0JEovnfRqtMNZCJQGKc2h/ojoE=
|
||||||
github.com/go-openapi/swag/jsonutils v0.26.0/go.mod h1:2VmA0CJlyFqgawOaPI9psnjFDqzyivIqLYN34t9p91E=
|
github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.0/go.mod h1:mofwUWx70wvskwESqRJ//k/9kURmCgyJl5m5Ppoh5kY=
|
||||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0 h1:apqeINu/ICHouqiRZbyFvuDge5jCmmLTqGQ9V95EaOM=
|
github.com/go-openapi/swag/loading v0.27.0 h1:s8DA9aPEdFH6OluHUYUn3DnIuoTdyWs9RwffXBUfyeI=
|
||||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0/go.mod h1:AyM6QT8uz5IdKxk5akv0y6u4QvcL9GWERt0Jx/F/R8Y=
|
github.com/go-openapi/swag/loading v0.27.0/go.mod h1:VOz+Jg6UGGywcmRvYsI4fvtp+bd7NfioseGEPleYdA4=
|
||||||
github.com/go-openapi/swag/loading v0.26.0 h1:Apg6zaKhCJurpJer0DCxq99qwmhFddBhaMX7kilDcko=
|
github.com/go-openapi/swag/mangling v0.27.0 h1:rpPJuqQHa6z2pDiP3iIpXOyNXlSs9cQCxnJSAxzdfOc=
|
||||||
github.com/go-openapi/swag/loading v0.26.0/go.mod h1:dBxQ/6V2uBaAQdevN18VELE6xSpJWZxLX4txe12JwDg=
|
github.com/go-openapi/swag/mangling v0.27.0/go.mod h1:jtBE2+V+3pILxOR7Vgce+Cwp6A2PgZbvVqfNntbVs0w=
|
||||||
github.com/go-openapi/swag/mangling v0.26.0 h1:Du2YC4YLA/Y5m/YKQd7AnY5qq0wRKSFZTTt8ktFaXcQ=
|
github.com/go-openapi/swag/netutils v0.27.0 h1:lEUG+hHvPvLggB3A8snFk0IRKNf9uC0YKc+7WYqvAF8=
|
||||||
github.com/go-openapi/swag/mangling v0.26.0/go.mod h1:jifS7W9vbg+pw63bT+GI53otluMQL3CeemuyCHKwVx0=
|
github.com/go-openapi/swag/netutils v0.27.0/go.mod h1:J+WYyFMLtvtCGqa6jLv+YNUmIKI3ZRQRrvfNDMoQoEQ=
|
||||||
github.com/go-openapi/swag/netutils v0.26.0 h1:CmZp+ZT7HrmFwrC3GdGsXBq2+42T1bjKBapcqVpIs3c=
|
github.com/go-openapi/swag/stringutils v0.27.0 h1:Of7w/HljWsNZvuxsUAnw3n+hCOyI6HLJOxW2kQRAxio=
|
||||||
github.com/go-openapi/swag/netutils v0.26.0/go.mod h1:5iK+Ok3ZohWWex1C50BFTPexi03UaPwjW4Oj8kgrpwo=
|
github.com/go-openapi/swag/stringutils v0.27.0/go.mod h1:lzRN95CxXmA03XcDWHLOb6nOMcxCqR5rGY0lOgsfRoM=
|
||||||
github.com/go-openapi/swag/stringutils v0.26.0 h1:qZQngLxs5s7SLijc3N2ZO+fUq2o8LjuWAASSrJuh+xg=
|
github.com/go-openapi/swag/typeutils v0.27.0 h1:aCf4MSGo8NLwZP8Q6t32DWLJSvl/WwNqgmEG+xJ6v2o=
|
||||||
github.com/go-openapi/swag/stringutils v0.26.0/go.mod h1:sWn5uY+QIIspwPhvgnqJsH8xqFT2ZbYcvbcFanRyhFE=
|
github.com/go-openapi/swag/typeutils v0.27.0/go.mod h1:Srm0xFNRZ1Y+vCxJclo5qzx8aj+1pAKda/YfFPrG0dQ=
|
||||||
github.com/go-openapi/swag/typeutils v0.26.0 h1:2kdEwdiNWy+JJdOvu5MA2IIg2SylWAFuuyQIKYybfq4=
|
github.com/go-openapi/swag/yamlutils v0.27.0 h1:bQ6eAMil5X9tdcf7dMn4t15alzG6jddnrKPuKa/zxKM=
|
||||||
github.com/go-openapi/swag/typeutils v0.26.0/go.mod h1:oovDuIUvTrEHVMqWilQzKzV4YlSKgyZmFh7AlfABNVE=
|
github.com/go-openapi/swag/yamlutils v0.27.0/go.mod h1:yRfIo7qqVkmJRQjX8exjA3AfcI8rH1KDNPsTparoCv4=
|
||||||
github.com/go-openapi/swag/yamlutils v0.26.0 h1:H7O8l/8NJJQ/oiReEN+oMpnGMyt8G0hl460nRZxhLMQ=
|
github.com/go-openapi/testify/enable/yaml/v2 v2.6.0 h1:gGHwAJ0R/5jU8BEGDbfRNR3hL68dAVi84WuOApp29B0=
|
||||||
github.com/go-openapi/swag/yamlutils v0.26.0/go.mod h1:1evKEGAtP37Pkwcc7EWMF0hedX0/x3Rkvei2wtG/TbU=
|
github.com/go-openapi/testify/enable/yaml/v2 v2.6.0/go.mod h1:tY+St1SGq4NFl0QIqdTY4aEdbChAHxhyB77XQi9iJCo=
|
||||||
github.com/go-openapi/testify/enable/yaml/v2 v2.4.2 h1:5zRca5jw7lzVREKCZVNBpysDNBjj74rBh0N2BGQbSR0=
|
github.com/go-openapi/testify/v2 v2.6.0 h1:5PKH2HE7YJ/LuRPQGvSxBRlFXNQhSetBLlGAgUEu3ug=
|
||||||
github.com/go-openapi/testify/enable/yaml/v2 v2.4.2/go.mod h1:XVevPw5hUXuV+5AkI1u1PeAm27EQVrhXTTCPAF85LmE=
|
github.com/go-openapi/testify/v2 v2.6.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw=
|
||||||
github.com/go-openapi/testify/v2 v2.4.2 h1:tiByHpvE9uHrrKjOszax7ZvKB7QOgizBWGBLuq0ePx4=
|
|
||||||
github.com/go-openapi/testify/v2 v2.4.2/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw=
|
|
||||||
github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c=
|
github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c=
|
||||||
github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
|
github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
@@ -92,14 +90,14 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
|
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
|
||||||
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/lukaszraczylo/oss-telemetry v0.0.0-20260521005811-e02d51419c52 h1:HAm1OV/1uYN3VA/HdDNFjwh8KerTLwl1SoxF+IiNf/M=
|
github.com/lukaszraczylo/oss-telemetry v0.2.3 h1:xoDtBqeZGmXj7IteiE1M5WMuzeoqag58qEleI0Cf2Ms=
|
||||||
github.com/lukaszraczylo/oss-telemetry v0.0.0-20260521005811-e02d51419c52/go.mod h1:+Cn78qZo8rc3T9eZt0v3oICYRdd75wORtSidc8lNjDQ=
|
github.com/lukaszraczylo/oss-telemetry v0.2.3/go.mod h1:+Cn78qZo8rc3T9eZt0v3oICYRdd75wORtSidc8lNjDQ=
|
||||||
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
||||||
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||||
github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
|
github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU=
|
||||||
github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||||
github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||||
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
|
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
|
||||||
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
|
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
|
||||||
@@ -149,35 +147,35 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
|||||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||||
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
|
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
|
||||||
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
|
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
|
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
|
||||||
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
|
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
||||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
||||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||||
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
|
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
|
||||||
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
|
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI=
|
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI=
|
||||||
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
@@ -190,20 +188,20 @@ gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
|||||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
k8s.io/api v0.36.0 h1:SgqDhZzHdOtMk40xVSvCXkP9ME0H05hPM3p9AB1kL80=
|
k8s.io/api v0.36.2 h1:TF6YDLIzKfccK7cq9YpTcGX8TJmEkHVRv78DM51fRYY=
|
||||||
k8s.io/api v0.36.0/go.mod h1:m1LVrGPNYax5NBHdO+QuAedXyuzTt4RryI/qnmNvs34=
|
k8s.io/api v0.36.2/go.mod h1:F4LbMO4brjZYh7yFkXWhynSvtB7YauxV4c+HHkNRGNg=
|
||||||
k8s.io/apimachinery v0.36.0 h1:jZyPzhd5Z+3h9vJLt0z9XdzW9VzNzWAUw+P1xZ9PXtQ=
|
k8s.io/apimachinery v0.36.2 h1:0PE/W/WNy1UX61NLbXY5TMbJ6UwLL6E6lAPkYrKFxbQ=
|
||||||
k8s.io/apimachinery v0.36.0/go.mod h1:FklypaRJt6n5wUIwWXIP6GJlIpUizTgfo1T/As+Tyxc=
|
k8s.io/apimachinery v0.36.2/go.mod h1:fvf/HOLXq9RId0rnDIbN1OEBvHXdQbLMM8nu0LcBUf4=
|
||||||
k8s.io/client-go v0.36.0 h1:pOYi7C4RHChYjMiHpZSpSbIM6ZxVbRXBy7CuiIwqA3c=
|
k8s.io/client-go v0.36.2 h1:bfgxmFKc9CgqsgX4xKLAAdmTQlWee7Ob/HlDOrJ5TBI=
|
||||||
k8s.io/client-go v0.36.0/go.mod h1:ZKKcpwF0aLYfkHFCjillCKaTK/yBkEDHTDXCFY6AS9Y=
|
k8s.io/client-go v0.36.2/go.mod h1:1vgO4OAlfPnoLcb+Rze2GF5rAr14w8qjrYMoyXJzQj0=
|
||||||
k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
|
k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
|
||||||
k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
|
k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
|
||||||
k8s.io/kube-openapi v0.0.0-20260505163821-33341827b392 h1:B7Ylb1OUptHKVX/3kpvXB0i05pDmXU66cGED/4Ta9Bw=
|
k8s.io/kube-openapi v0.0.0-20260624041617-8f3fa4921821 h1:m2wZhD5+vJZyCVkTvUHIfaiXc/mdt3Pxyx3vUnGsKzU=
|
||||||
k8s.io/kube-openapi v0.0.0-20260505163821-33341827b392/go.mod h1:V/QaCUYDa+0QpcHhVVc5l99Uz56wEMEXBSj9oCDkNDY=
|
k8s.io/kube-openapi v0.0.0-20260624041617-8f3fa4921821/go.mod h1:V/QaCUYDa+0QpcHhVVc5l99Uz56wEMEXBSj9oCDkNDY=
|
||||||
k8s.io/streaming v0.36.0 h1:agnTxU+NFulUrtYzXUGKO3ndEa8jKwht1Kwn9nu9x+4=
|
k8s.io/streaming v0.36.2 h1:NSKthPPg9UFSKsRauVJUVGH2Dvn8fhKmY4qrMkw/p98=
|
||||||
k8s.io/streaming v0.36.0/go.mod h1:z6fV3D+NVkoeqRMtWwlUZK6U17SY/LqNzOxWL6GyR/s=
|
k8s.io/streaming v0.36.2/go.mod h1:z6fV3D+NVkoeqRMtWwlUZK6U17SY/LqNzOxWL6GyR/s=
|
||||||
k8s.io/utils v0.0.0-20260319190234-28399d86e0b5 h1:kBawHLSnx/mYHmRnNUf9d4CpjREbeZuxoSGOX/J+aYM=
|
k8s.io/utils v0.0.0-20260626114624-be93311217bd h1:Ea7fgQ5we8Y9T0OX5o0dAHzQOBRI07D/dEYRaB9ZZEs=
|
||||||
k8s.io/utils v0.0.0-20260319190234-28399d86e0b5/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
|
k8s.io/utils v0.0.0-20260626114624-be93311217bd/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
|
||||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
|
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
|
||||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
|
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
|
||||||
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
|
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
|
||||||
|
|||||||
@@ -0,0 +1,458 @@
|
|||||||
|
// Package complete provides shell completion generation for kportal.
|
||||||
|
// It supports bash, zsh, and fish shells with context-aware completions
|
||||||
|
// for flags, subcommands, config values, and Kubernetes resources.
|
||||||
|
package complete
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Shell represents a supported shell type
|
||||||
|
type Shell string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ShellBash Shell = "bash"
|
||||||
|
ShellZsh Shell = "zsh"
|
||||||
|
ShellFish Shell = "fish"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Installer handles shell completion installation
|
||||||
|
type Installer struct {
|
||||||
|
shell Shell
|
||||||
|
prefixDir string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInstaller creates a new completion installer for the specified shell
|
||||||
|
func NewInstaller(shell Shell) *Installer {
|
||||||
|
return &Installer{
|
||||||
|
shell: shell,
|
||||||
|
prefixDir: getDefaultPrefixDir(shell),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDefaultPrefixDir returns the default completion directory for a shell
|
||||||
|
func getDefaultPrefixDir(shell Shell) string {
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
home = os.Getenv("HOME")
|
||||||
|
}
|
||||||
|
if home == "" {
|
||||||
|
home = "/tmp"
|
||||||
|
}
|
||||||
|
|
||||||
|
switch shell {
|
||||||
|
case ShellBash:
|
||||||
|
// Check common bash completion directories
|
||||||
|
dirs := []string{
|
||||||
|
"/etc/bash_completion.d",
|
||||||
|
filepath.Join(home, ".local", "share", "bash-completion", "completions"),
|
||||||
|
filepath.Join(home, ".bash_completion.d"),
|
||||||
|
}
|
||||||
|
for _, dir := range dirs {
|
||||||
|
if pathExists(dir) {
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fallback to user dir (best-effort create; Install reports write errors)
|
||||||
|
userDir := filepath.Join(home, ".local", "share", "bash-completion", "completions")
|
||||||
|
ensureDir(userDir)
|
||||||
|
return userDir
|
||||||
|
|
||||||
|
case ShellZsh:
|
||||||
|
dirs := []string{
|
||||||
|
filepath.Join(home, ".zsh", "completions"),
|
||||||
|
filepath.Join(home, ".oh-my-zsh", "completions"),
|
||||||
|
}
|
||||||
|
for _, dir := range dirs {
|
||||||
|
if pathExists(dir) {
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fallback to standard zsh site-functions
|
||||||
|
usrShare := "/usr/local/share/zsh/site-functions"
|
||||||
|
if pathExists(usrShare) {
|
||||||
|
return usrShare
|
||||||
|
}
|
||||||
|
userDir := filepath.Join(home, ".zsh", "completions")
|
||||||
|
ensureDir(userDir)
|
||||||
|
return userDir
|
||||||
|
|
||||||
|
case ShellFish:
|
||||||
|
dir := filepath.Join(home, ".config", "fish", "completions")
|
||||||
|
ensureDir(dir)
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install installs the completion script for the shell
|
||||||
|
func (i *Installer) Install() error {
|
||||||
|
script, err := Generate(i.shell)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to generate completion script: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := i.getCompletionFilename()
|
||||||
|
filepath := filepath.Join(i.prefixDir, filename)
|
||||||
|
|
||||||
|
// Check if already installed
|
||||||
|
if pathExists(filepath) {
|
||||||
|
return fmt.Errorf("completion file already exists: %s (remove it first to reinstall)", filepath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// #nosec G306 -- completion scripts are non-secret and must be world-readable by the shell
|
||||||
|
if err := os.WriteFile(filepath, []byte(script), 0o644); err != nil {
|
||||||
|
return fmt.Errorf("failed to write completion file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uninstall removes the completion script
|
||||||
|
func (i *Installer) Uninstall() error {
|
||||||
|
filename := i.getCompletionFilename()
|
||||||
|
filepath := filepath.Join(i.prefixDir, filename)
|
||||||
|
|
||||||
|
if err := os.Remove(filepath); err != nil {
|
||||||
|
return fmt.Errorf("failed to remove completion file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getCompletionFilename returns the filename for the completion script
|
||||||
|
func (i *Installer) getCompletionFilename() string {
|
||||||
|
switch i.shell {
|
||||||
|
case ShellBash:
|
||||||
|
return "_kportal"
|
||||||
|
case ShellZsh:
|
||||||
|
return "_kportal"
|
||||||
|
case ShellFish:
|
||||||
|
return "kportal.fish"
|
||||||
|
}
|
||||||
|
return "_kportal"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print prints the completion script to stdout
|
||||||
|
func Print(shell Shell) error {
|
||||||
|
script, err := Generate(shell)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Print(script)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate generates the completion script for the specified shell
|
||||||
|
func Generate(shell Shell) (string, error) {
|
||||||
|
switch shell {
|
||||||
|
case ShellBash:
|
||||||
|
return generateBash()
|
||||||
|
case ShellZsh:
|
||||||
|
return generateZsh()
|
||||||
|
case ShellFish:
|
||||||
|
return generateFish()
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("unsupported shell: %s", shell)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateBash generates bash completion script
|
||||||
|
func generateBash() (string, error) {
|
||||||
|
var sb strings.Builder
|
||||||
|
|
||||||
|
bashScript := `# kportal shell completion - bash
|
||||||
|
# Generated by kportal
|
||||||
|
|
||||||
|
# Don't interfere with other completions
|
||||||
|
if [[ -n "${BASH_COMPLETION_VERSINFO:-}" ]]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
_kportal()
|
||||||
|
{
|
||||||
|
local cur prev words cword split=false
|
||||||
|
_init_completion -s || return
|
||||||
|
|
||||||
|
# Complete the value expected after the previous flag
|
||||||
|
case "$prev" in
|
||||||
|
-c|--config)
|
||||||
|
_filedir yaml
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
--log-format)
|
||||||
|
COMPREPLY=( $(compgen -W "text json" -- "$cur") )
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
--convert)
|
||||||
|
_filedir json
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
--convert-output)
|
||||||
|
_filedir yaml
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
--context)
|
||||||
|
# Complete from kubectl contexts
|
||||||
|
if command -v kubectl &> /dev/null; then
|
||||||
|
COMPREPLY=( $(compgen -W "$(kubectl config get-contexts -o name 2>/dev/null)" -- "$cur") )
|
||||||
|
fi
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
--shell)
|
||||||
|
COMPREPLY=( $(compgen -W "bash zsh fish" -- "$cur") )
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Handle option splitting (e.g., -c=value)
|
||||||
|
[[ "$cur" == -*=* ]] && split=true
|
||||||
|
[[ "$split" == true ]] && prev="${cur%%=*}"
|
||||||
|
|
||||||
|
# Subcommand-specific completion
|
||||||
|
case "${words[1]}" in
|
||||||
|
generate)
|
||||||
|
if [[ "$cur" == -* ]]; then
|
||||||
|
COMPREPLY=( $(compgen -W "--context --config --dry-run" -- "$cur") )
|
||||||
|
elif command -v kubectl &> /dev/null; then
|
||||||
|
COMPREPLY=( $(compgen -W "$(kubectl config get-contexts -o name 2>/dev/null)" -- "$cur") )
|
||||||
|
fi
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
completion)
|
||||||
|
COMPREPLY=( $(compgen -W "--install --uninstall --shell" -- "$cur") )
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Top-level options
|
||||||
|
if [[ "$cur" == -* ]]; then
|
||||||
|
COMPREPLY=( $(compgen -W "-c -v --check --headless --log-format --version --update --convert --convert-output" -- "$cur") )
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Top-level subcommands
|
||||||
|
COMPREPLY=( $(compgen -W "generate completion" -- "$cur") )
|
||||||
|
}
|
||||||
|
|
||||||
|
# Register completion
|
||||||
|
complete -F _kportal kportal
|
||||||
|
|
||||||
|
# Also complete for common aliases
|
||||||
|
complete -F _kportal kp
|
||||||
|
`
|
||||||
|
|
||||||
|
sb.WriteString(bashScript)
|
||||||
|
return sb.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateZsh generates zsh completion script
|
||||||
|
func generateZsh() (string, error) {
|
||||||
|
var sb strings.Builder
|
||||||
|
|
||||||
|
// Per-flag zsh _arguments descriptors. Completion scripts are static text, so
|
||||||
|
// these are literals (single source of truth — keep in sync with the CLI flags).
|
||||||
|
flagDescs := []string{
|
||||||
|
`'-c[Path to configuration file]:config file:_files -g "*.yaml"'`,
|
||||||
|
`'-v[Enable verbose logging]'`,
|
||||||
|
`'--version[Show version and exit]'`,
|
||||||
|
`'--update[Check for updates]'`,
|
||||||
|
`'--check[Validate configuration]'`,
|
||||||
|
`'--headless[Run without UI]'`,
|
||||||
|
`'--log-format[Log format: text or json]:format:(text json)'`,
|
||||||
|
`'--convert[Convert kftray config]:input file:_files -g "*.json"'`,
|
||||||
|
`'--convert-output[Output file]:output file:_files -g "*.yaml"'`,
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.WriteString(`#compdef kportal
|
||||||
|
|
||||||
|
# kportal shell completion - zsh
|
||||||
|
# Generated by kportal
|
||||||
|
|
||||||
|
_kportal()
|
||||||
|
{
|
||||||
|
local -a commands flags generate_flags completion_flags
|
||||||
|
|
||||||
|
commands=(
|
||||||
|
'generate:Interactively generate forwards from cluster'
|
||||||
|
'completion:Generate shell completion scripts'
|
||||||
|
)
|
||||||
|
|
||||||
|
flags=(
|
||||||
|
`)
|
||||||
|
|
||||||
|
for _, desc := range flagDescs {
|
||||||
|
fmt.Fprintf(&sb, " %s\n", desc)
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.WriteString(` )
|
||||||
|
|
||||||
|
generate_flags=(
|
||||||
|
'--context[Kubernetes context]:context:->ctx'
|
||||||
|
'--config[Config file]:file:_files -g "*.yaml"'
|
||||||
|
'--dry-run[Print without saving]'
|
||||||
|
)
|
||||||
|
|
||||||
|
completion_flags=(
|
||||||
|
'--install[Install completions for the shell]'
|
||||||
|
'--uninstall[Remove installed completions]'
|
||||||
|
'--shell[Shell type]:shell:(bash zsh fish)'
|
||||||
|
)
|
||||||
|
|
||||||
|
_arguments -s $flags '1: :->command' '*:: :->args'
|
||||||
|
|
||||||
|
case $state in
|
||||||
|
command)
|
||||||
|
_describe 'command' commands
|
||||||
|
;;
|
||||||
|
args)
|
||||||
|
case ${words[1]} in
|
||||||
|
generate)
|
||||||
|
_arguments -s $generate_flags
|
||||||
|
if [[ "$words[CURRENT]" == --context ]]; then
|
||||||
|
if command -v kubectl &> /dev/null; then
|
||||||
|
local -a contexts
|
||||||
|
contexts=(${(f)"$(kubectl config get-contexts -o name 2>/dev/null)"})
|
||||||
|
_describe 'context' contexts
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
completion)
|
||||||
|
_arguments -s $completion_flags
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
_kportal "$@"
|
||||||
|
`)
|
||||||
|
|
||||||
|
return sb.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateFish generates fish completion script
|
||||||
|
func generateFish() (string, error) {
|
||||||
|
var sb strings.Builder
|
||||||
|
|
||||||
|
sb.WriteString(`# kportal shell completion - fish
|
||||||
|
# Generated by kportal
|
||||||
|
|
||||||
|
# Main completion
|
||||||
|
complete -c kportal -f
|
||||||
|
|
||||||
|
# Subcommands
|
||||||
|
complete -c kportal -n '__fish_use_subcommand' -a 'generate' -d 'Interactively generate forwards from cluster'
|
||||||
|
complete -c kportal -n '__fish_use_subcommand' -a 'completion' -d 'Generate shell completion scripts'
|
||||||
|
|
||||||
|
# Global flags (main command uses single-dash -c and -v; words accept --)
|
||||||
|
complete -c kportal -s c -r -f -a '( __fish_complete_suffix .yaml )' -d 'Path to configuration file'
|
||||||
|
complete -c kportal -s v -d 'Enable verbose logging'
|
||||||
|
complete -c kportal -l version -d 'Show version and exit'
|
||||||
|
complete -c kportal -l update -d 'Check for updates'
|
||||||
|
complete -c kportal -l check -d 'Validate configuration'
|
||||||
|
complete -c kportal -l headless -d 'Run without UI'
|
||||||
|
complete -c kportal -l log-format -d 'Log format' -a 'text json' -f
|
||||||
|
complete -c kportal -l convert -r -f -a '( __fish_complete_suffix .json )' -d 'Convert kftray config'
|
||||||
|
complete -c kportal -l convert-output -r -f -a '( __fish_complete_suffix .yaml )' -d 'Output file'
|
||||||
|
|
||||||
|
# generate subcommand flags
|
||||||
|
complete -c kportal -n '__fish_seen_subcommand_from generate' -l context -d 'Kubernetes context' -a '(kubectl config get-contexts -o name 2>/dev/null)' -f
|
||||||
|
complete -c kportal -n '__fish_seen_subcommand_from generate' -l config -r -f -a '( __fish_complete_suffix .yaml )' -d 'Config file'
|
||||||
|
complete -c kportal -n '__fish_seen_subcommand_from generate' -l dry-run -d 'Print without saving'
|
||||||
|
|
||||||
|
# completion subcommand flags
|
||||||
|
complete -c kportal -n '__fish_seen_subcommand_from completion' -l install -d 'Install completions for the shell'
|
||||||
|
complete -c kportal -n '__fish_seen_subcommand_from completion' -l uninstall -d 'Remove installed completions'
|
||||||
|
complete -c kportal -n '__fish_seen_subcommand_from completion' -l shell -d 'Shell type' -a 'bash zsh fish' -f
|
||||||
|
|
||||||
|
# Aliases
|
||||||
|
complete -c kp -f
|
||||||
|
`)
|
||||||
|
|
||||||
|
return sb.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InstallCompletions installs completions for the specified shell
|
||||||
|
// Prints instructions for manual installation if auto-install fails
|
||||||
|
func InstallCompletions(shell Shell) error {
|
||||||
|
installer := NewInstaller(shell)
|
||||||
|
|
||||||
|
if err := installer.Install(); err != nil {
|
||||||
|
// Check if it's just "already exists" error
|
||||||
|
if strings.Contains(err.Error(), "already exists") {
|
||||||
|
fmt.Printf("Completion already installed at: %s/%s\n",
|
||||||
|
installer.prefixDir, installer.getCompletionFilename())
|
||||||
|
fmt.Println("Remove it first to reinstall, or source it manually:")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to print the script instead
|
||||||
|
fmt.Println("Could not auto-install completions. To install manually, run:")
|
||||||
|
fmt.Printf(" # %s\n", getInstallInstructions(shell))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("✅ Completions installed to: %s/%s\n",
|
||||||
|
installer.prefixDir, installer.getCompletionFilename())
|
||||||
|
fmt.Printf("\nTo enable %s completions, restart your shell or run:\n", shell)
|
||||||
|
fmt.Printf(" source ~/%s\n", getSourceInstruction(shell))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getInstallInstructions(shell Shell) string {
|
||||||
|
return fmt.Sprintf("kportal completion %s > <your-completion-dir>/_kportal", shell)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSourceInstruction(shell Shell) string {
|
||||||
|
switch shell {
|
||||||
|
case ShellBash:
|
||||||
|
return ".bashrc"
|
||||||
|
case ShellZsh:
|
||||||
|
return ".zshrc"
|
||||||
|
case ShellFish:
|
||||||
|
return ".config/fish/config.fish # completions load automatically"
|
||||||
|
}
|
||||||
|
return "shell config"
|
||||||
|
}
|
||||||
|
|
||||||
|
// AutoDetectShell detects the current shell
|
||||||
|
func AutoDetectShell() Shell {
|
||||||
|
shell := os.Getenv("SHELL")
|
||||||
|
if shell == "" {
|
||||||
|
return ShellBash // Default
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasSuffix(shell, "/bash") {
|
||||||
|
return ShellBash
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(shell, "/zsh") {
|
||||||
|
return ShellZsh
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(shell, "/fish") {
|
||||||
|
return ShellFish
|
||||||
|
}
|
||||||
|
|
||||||
|
return ShellBash
|
||||||
|
}
|
||||||
|
|
||||||
|
// pathExists reports whether path exists on disk.
|
||||||
|
// #nosec G703 -- callers pass fixed completion paths derived from the user's own
|
||||||
|
// HOME and constant subdirectory names, never external/untrusted input.
|
||||||
|
func pathExists(path string) bool {
|
||||||
|
_, err := os.Stat(path)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensureDir best-effort creates a completion directory. Errors are intentionally
|
||||||
|
// ignored: Install() surfaces any later write failure with a clear message.
|
||||||
|
// #nosec G703 -- path is derived from the user's own HOME and constant
|
||||||
|
// subdirectory names; 0o750 is appropriate for user-owned completion dirs.
|
||||||
|
func ensureDir(path string) {
|
||||||
|
_ = os.MkdirAll(path, 0o750)
|
||||||
|
}
|
||||||
@@ -0,0 +1,327 @@
|
|||||||
|
package complete
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGenerateBash(t *testing.T) {
|
||||||
|
script, err := Generate(ShellBash)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Generate(ShellBash) failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify script contains key elements
|
||||||
|
if !strings.Contains(script, "_kportal()") {
|
||||||
|
t.Error("Bash script missing _kportal function")
|
||||||
|
}
|
||||||
|
if !strings.Contains(script, "complete -F _kportal kportal") {
|
||||||
|
t.Error("Bash script missing completion registration")
|
||||||
|
}
|
||||||
|
if !strings.Contains(script, "--version") {
|
||||||
|
t.Error("Bash script missing --version flag")
|
||||||
|
}
|
||||||
|
if !strings.Contains(script, "generate") {
|
||||||
|
t.Error("Bash script missing generate subcommand")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateZsh(t *testing.T) {
|
||||||
|
script, err := Generate(ShellZsh)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Generate(ShellZsh) failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify script contains key elements
|
||||||
|
if !strings.Contains(script, "#compdef kportal") {
|
||||||
|
t.Error("Zsh script missing #compdef directive")
|
||||||
|
}
|
||||||
|
if !strings.Contains(script, "_kportal()") {
|
||||||
|
t.Error("Zsh script missing _kportal function")
|
||||||
|
}
|
||||||
|
if !strings.Contains(script, "'generate:") {
|
||||||
|
t.Error("Zsh script missing generate subcommand")
|
||||||
|
}
|
||||||
|
if !strings.Contains(script, "'--context[") {
|
||||||
|
t.Error("Zsh script missing --context flag")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateFish(t *testing.T) {
|
||||||
|
script, err := Generate(ShellFish)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Generate(ShellFish) failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify script contains key elements
|
||||||
|
if !strings.Contains(script, "complete -c kportal") {
|
||||||
|
t.Error("Fish script missing complete directive")
|
||||||
|
}
|
||||||
|
if !strings.Contains(script, "-n '__fish_use_subcommand'") {
|
||||||
|
t.Error("Fish script missing subcommand detection")
|
||||||
|
}
|
||||||
|
if !strings.Contains(script, "-l context") {
|
||||||
|
t.Error("Fish script missing --context flag")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateUnsupported(t *testing.T) {
|
||||||
|
_, err := Generate(Shell("unsupported"))
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error for unsupported shell")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAutoDetectShell(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
shellEnv string
|
||||||
|
expected Shell
|
||||||
|
}{
|
||||||
|
{"bash", "/bin/bash", ShellBash},
|
||||||
|
{"zsh", "/usr/bin/zsh", ShellZsh},
|
||||||
|
{"fish", "/usr/local/bin/fish", ShellFish},
|
||||||
|
{"tcsh", "/bin/tcsh", ShellBash}, // Falls back to bash
|
||||||
|
{"empty", "", ShellBash}, // Falls back to bash
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
detected := autoDetectShellFromEnv(tt.shellEnv)
|
||||||
|
if detected != tt.expected {
|
||||||
|
t.Errorf("AutoDetectShell() = %v, want %v", detected, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// autoDetectShellFromEnv is a test helper that simulates shell detection
|
||||||
|
func autoDetectShellFromEnv(shellEnv string) Shell {
|
||||||
|
if shellEnv == "" {
|
||||||
|
return ShellBash
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(shellEnv, "/bash") {
|
||||||
|
return ShellBash
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(shellEnv, "/zsh") {
|
||||||
|
return ShellZsh
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(shellEnv, "/fish") {
|
||||||
|
return ShellFish
|
||||||
|
}
|
||||||
|
return ShellBash
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInstaller(t *testing.T) {
|
||||||
|
// Test with bash
|
||||||
|
installer := NewInstaller(ShellBash)
|
||||||
|
if installer.prefixDir == "" {
|
||||||
|
t.Error("Installer should have a prefix directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test filename generation
|
||||||
|
t.Run("filename", func(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
shell Shell
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{ShellBash, "_kportal"},
|
||||||
|
{ShellZsh, "_kportal"},
|
||||||
|
{ShellFish, "kportal.fish"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
inst := NewInstaller(tt.shell)
|
||||||
|
got := inst.getCompletionFilename()
|
||||||
|
if got != tt.expected {
|
||||||
|
t.Errorf("getCompletionFilename() for %v = %v, want %v", tt.shell, got, tt.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInstallCompletion(t *testing.T) {
|
||||||
|
// Temp directory (auto-removed by the test framework)
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
|
||||||
|
// Test installation to temp directory
|
||||||
|
installer := &Installer{
|
||||||
|
shell: ShellBash,
|
||||||
|
prefixDir: tempDir,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should fail because file doesn't exist (doesn't matter, just test the method exists)
|
||||||
|
_ = installer.Install()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrint(t *testing.T) {
|
||||||
|
// Test that Print doesn't crash and outputs something
|
||||||
|
orig := os.Stdout
|
||||||
|
r, w, _ := os.Pipe()
|
||||||
|
os.Stdout = w
|
||||||
|
|
||||||
|
err := Print(ShellBash)
|
||||||
|
|
||||||
|
_ = w.Close()
|
||||||
|
os.Stdout = orig
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Print(ShellBash) failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read output
|
||||||
|
buf := make([]byte, 1024)
|
||||||
|
n, _ := r.Read(buf)
|
||||||
|
output := string(buf[:n])
|
||||||
|
|
||||||
|
if !strings.Contains(output, "_kportal") {
|
||||||
|
t.Error("Print output should contain _kportal")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetCompletionFilename(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
shell Shell
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{ShellBash, "_kportal"},
|
||||||
|
{ShellZsh, "_kportal"},
|
||||||
|
{ShellFish, "kportal.fish"},
|
||||||
|
{Shell("unknown"), "_kportal"}, // Falls back
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
inst := &Installer{shell: tt.shell}
|
||||||
|
got := inst.getCompletionFilename()
|
||||||
|
if got != tt.expected {
|
||||||
|
t.Errorf("getCompletionFilename() for %v = %v, want %v", tt.shell, got, tt.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBashCompletionIncludesContextCompletion(t *testing.T) {
|
||||||
|
script, err := Generate(ShellBash)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Generate(ShellBash) failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should include kubectl context completion
|
||||||
|
if !strings.Contains(script, "kubectl config get-contexts") {
|
||||||
|
t.Error("Bash completion should include kubectl context completion")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestZshCompletionIncludesContextCompletion(t *testing.T) {
|
||||||
|
script, err := Generate(ShellZsh)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Generate(ShellZsh) failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should include kubectl context completion
|
||||||
|
if !strings.Contains(script, "kubectl config get-contexts") {
|
||||||
|
t.Error("Zsh completion should include kubectl context completion")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFishCompletionIncludesContextCompletion(t *testing.T) {
|
||||||
|
script, err := Generate(ShellFish)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Generate(ShellFish) failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should include kubectl context completion
|
||||||
|
if !strings.Contains(script, "kubectl config get-contexts") {
|
||||||
|
t.Error("Fish completion should include kubectl context completion")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAllFlagsPresent(t *testing.T) {
|
||||||
|
script, err := Generate(ShellBash)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Generate(ShellBash) failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check all main flags are present. The main command exposes single-dash
|
||||||
|
// -c/-v (no --config/--verbose long forms); word flags use --.
|
||||||
|
flags := []string{
|
||||||
|
"-c",
|
||||||
|
"-v",
|
||||||
|
"--version",
|
||||||
|
"--update",
|
||||||
|
"--check",
|
||||||
|
"--headless",
|
||||||
|
"--log-format",
|
||||||
|
"--convert",
|
||||||
|
"--convert-output",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, flag := range flags {
|
||||||
|
if !strings.Contains(script, flag) {
|
||||||
|
t.Errorf("Bash completion missing flag: %s", flag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubcommandsPresent(t *testing.T) {
|
||||||
|
for _, shell := range []Shell{ShellBash, ShellZsh, ShellFish} {
|
||||||
|
script, err := Generate(shell)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Generate(%v) failed: %v", shell, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, sub := range []string{"generate", "completion"} {
|
||||||
|
if !strings.Contains(script, sub) {
|
||||||
|
t.Errorf("%s completion missing %q subcommand", shell, sub)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateFlagsPresent(t *testing.T) {
|
||||||
|
script, err := Generate(ShellBash)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Generate(ShellBash) failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
generateFlags := []string{
|
||||||
|
"--context",
|
||||||
|
"--config",
|
||||||
|
"--dry-run",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, flag := range generateFlags {
|
||||||
|
if !strings.Contains(script, flag) {
|
||||||
|
t.Errorf("Bash completion missing generate flag: %s", flag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompletionScriptPermissions(t *testing.T) {
|
||||||
|
// Create temp file and verify permissions handling
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
|
||||||
|
installer := &Installer{
|
||||||
|
shell: ShellBash,
|
||||||
|
prefixDir: tempDir,
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := filepath.Join(tempDir, installer.getCompletionFilename())
|
||||||
|
|
||||||
|
// Write a test file
|
||||||
|
err := os.WriteFile(filename, []byte("test"), 0o600)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to write test file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify file exists and is readable
|
||||||
|
data, err := os.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to read test file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(data) != "test" {
|
||||||
|
t.Error("File content mismatch")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -447,10 +447,10 @@ func FormatValidationErrors(errs []ValidationError) string {
|
|||||||
sb.WriteString(strings.Repeat("=", 50) + "\n\n")
|
sb.WriteString(strings.Repeat("=", 50) + "\n\n")
|
||||||
|
|
||||||
for i, err := range errs {
|
for i, err := range errs {
|
||||||
sb.WriteString(fmt.Sprintf("%d. %s\n", i+1, err.Message))
|
fmt.Fprintf(&sb, "%d. %s\n", i+1, err.Message)
|
||||||
if len(err.Context) > 0 {
|
if len(err.Context) > 0 {
|
||||||
for k, v := range err.Context {
|
for k, v := range err.Context {
|
||||||
sb.WriteString(fmt.Sprintf(" %s: %s\n", k, v))
|
fmt.Fprintf(&sb, " %s: %s\n", k, v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sb.WriteString("\n")
|
sb.WriteString("\n")
|
||||||
|
|||||||
@@ -67,6 +67,10 @@ func formatProcessList(processes []processInfo) string {
|
|||||||
|
|
||||||
// getProcessNameByPID retrieves the process name for a given PID on Unix systems
|
// getProcessNameByPID retrieves the process name for a given PID on Unix systems
|
||||||
func getProcessNameByPID(pid string) string {
|
func getProcessNameByPID(pid string) string {
|
||||||
|
if !isValidPID(pid) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
// #nosec G204 -- pid is validated by isValidPID() to contain only digits
|
||||||
cmd := exec.Command("ps", "-p", pid, "-o", "comm=")
|
cmd := exec.Command("ps", "-p", pid, "-o", "comm=")
|
||||||
output, err := cmd.Output()
|
output, err := cmd.Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -302,11 +306,11 @@ func FormatConflicts(conflicts []PortConflict) string {
|
|||||||
sb.WriteString(strings.Repeat("=", 50) + "\n\n")
|
sb.WriteString(strings.Repeat("=", 50) + "\n\n")
|
||||||
|
|
||||||
for _, conflict := range conflicts {
|
for _, conflict := range conflicts {
|
||||||
sb.WriteString(fmt.Sprintf("Port %d\n", conflict.Port))
|
fmt.Fprintf(&sb, "Port %d\n", conflict.Port)
|
||||||
if conflict.Resource != "" {
|
if conflict.Resource != "" {
|
||||||
sb.WriteString(fmt.Sprintf(" Needed for: %s\n", conflict.Resource))
|
fmt.Fprintf(&sb, " Needed for: %s\n", conflict.Resource)
|
||||||
}
|
}
|
||||||
sb.WriteString(fmt.Sprintf(" Currently used by: %s\n", conflict.UsedBy))
|
fmt.Fprintf(&sb, " Currently used by: %s\n", conflict.UsedBy)
|
||||||
sb.WriteString("\n")
|
sb.WriteString("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package forward
|
package forward
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -264,7 +265,7 @@ func (s *WatchdogTestSuite) TestConcurrentOperations() {
|
|||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func(id int) {
|
go func(id int) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
forwardID := string(rune('a' + id))
|
forwardID := fmt.Sprintf("worker-%d", id)
|
||||||
s.watchdog.RegisterWorker(forwardID, nil)
|
s.watchdog.RegisterWorker(forwardID, nil)
|
||||||
for j := 0; j < 10; j++ {
|
for j := 0; j < 10; j++ {
|
||||||
s.watchdog.Heartbeat(forwardID)
|
s.watchdog.Heartbeat(forwardID)
|
||||||
|
|||||||
+40
-15
@@ -448,6 +448,7 @@ type keyBinding struct {
|
|||||||
func mainViewKeyBindings() []keyBinding {
|
func mainViewKeyBindings() []keyBinding {
|
||||||
return []keyBinding{
|
return []keyBinding{
|
||||||
{"↑↓/jk", "Navigate"},
|
{"↑↓/jk", "Navigate"},
|
||||||
|
{"PgUp/Dn", "Page"},
|
||||||
{"Space", "Toggle"},
|
{"Space", "Toggle"},
|
||||||
{"n", "New"},
|
{"n", "New"},
|
||||||
{"e", "Edit"},
|
{"e", "Edit"},
|
||||||
@@ -480,7 +481,7 @@ func (m model) renderMainView() string {
|
|||||||
|
|
||||||
// Render error section if any errors exist
|
// Render error section if any errors exist
|
||||||
if len(m.ui.errors) > 0 {
|
if len(m.ui.errors) > 0 {
|
||||||
b.WriteString(m.renderErrorSection())
|
b.WriteString(m.renderErrorSection(termWidth))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render footer with proper spacing
|
// Render footer with proper spacing
|
||||||
@@ -527,10 +528,14 @@ func (m model) renderTitle(headerColor lipgloss.Color) string {
|
|||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// renderEmptyMessage renders the message shown when no forwards are configured
|
// renderEmptyMessage renders the message shown when no forwards are configured.
|
||||||
|
// It includes an actionable hint so a first-time user knows how to proceed.
|
||||||
func (m model) renderEmptyMessage(mutedColor lipgloss.Color) string {
|
func (m model) renderEmptyMessage(mutedColor lipgloss.Color) string {
|
||||||
disabledStyle := lipgloss.NewStyle().Foreground(mutedColor)
|
mutedStyle := lipgloss.NewStyle().Foreground(mutedColor)
|
||||||
return disabledStyle.Render("No forwards configured\n")
|
hintStyle := lipgloss.NewStyle().Foreground(highlightColor)
|
||||||
|
return mutedStyle.Render("No forwards configured") + "\n\n" +
|
||||||
|
hintStyle.Render(" Press ") + selectedStyle.Render("n") +
|
||||||
|
hintStyle.Render(" to add your first port forward.") + "\n"
|
||||||
}
|
}
|
||||||
|
|
||||||
// renderForwardsTable renders the forwards table with all styling
|
// renderForwardsTable renders the forwards table with all styling
|
||||||
@@ -655,10 +660,12 @@ func (m model) createTableStyleFunc(colors mainViewColors) func(row, col int) li
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// renderErrorSection renders the error display section
|
// renderErrorSection renders the error display section, sized to the terminal.
|
||||||
func (m model) renderErrorSection() string {
|
func (m model) renderErrorSection(termWidth int) string {
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
|
|
||||||
|
width := errorWidth(termWidth)
|
||||||
|
|
||||||
b.WriteString("\n\n")
|
b.WriteString("\n\n")
|
||||||
errorHeaderStyle := lipgloss.NewStyle().
|
errorHeaderStyle := lipgloss.NewStyle().
|
||||||
Bold(true).
|
Bold(true).
|
||||||
@@ -669,28 +676,44 @@ func (m model) renderErrorSection() string {
|
|||||||
|
|
||||||
errorLineStyle := lipgloss.NewStyle().
|
errorLineStyle := lipgloss.NewStyle().
|
||||||
Foreground(lipgloss.Color("196")).
|
Foreground(lipgloss.Color("196")).
|
||||||
Width(ErrorDisplayWidth).
|
Width(width).
|
||||||
MaxWidth(ErrorDisplayWidth)
|
MaxWidth(width)
|
||||||
|
|
||||||
for id, errMsg := range m.ui.errors {
|
for id, errMsg := range m.ui.errors {
|
||||||
// Find the forward to display its alias
|
// Find the forward to display its alias
|
||||||
if fwd, ok := m.ui.forwards[id]; ok {
|
if fwd, ok := m.ui.forwards[id]; ok {
|
||||||
b.WriteString(m.renderErrorLine(fwd.Alias, errMsg, errorLineStyle))
|
b.WriteString(m.renderErrorLine(fwd.Alias, errMsg, width, errorLineStyle))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// errorWidth clamps the error display to the terminal width, falling back to the
|
||||||
|
// default cap so errors never overflow narrow terminals nor sprawl on wide ones.
|
||||||
|
func errorWidth(termWidth int) int {
|
||||||
|
width := ErrorDisplayWidth
|
||||||
|
if termWidth > 0 && termWidth-2 < width {
|
||||||
|
width = termWidth - 2
|
||||||
|
}
|
||||||
|
if width < 20 {
|
||||||
|
width = 20
|
||||||
|
}
|
||||||
|
return width
|
||||||
|
}
|
||||||
|
|
||||||
// renderErrorLine renders a single error line with proper wrapping
|
// renderErrorLine renders a single error line with proper wrapping
|
||||||
func (m model) renderErrorLine(alias, errMsg string, style lipgloss.Style) string {
|
func (m model) renderErrorLine(alias, errMsg string, width int, style lipgloss.Style) string {
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
|
|
||||||
// Format: " • alias: error message"
|
// Format: " • alias: error message"
|
||||||
prefix := fmt.Sprintf(" • %s: ", alias)
|
prefix := fmt.Sprintf(" • %s: ", alias)
|
||||||
|
|
||||||
// Wrap the error message if it's too long
|
// Wrap the error message if it's too long
|
||||||
maxErrLen := ErrorDisplayWidth - len(prefix)
|
maxErrLen := width - len(prefix)
|
||||||
|
if maxErrLen < 1 {
|
||||||
|
maxErrLen = 1
|
||||||
|
}
|
||||||
wrappedMsg := wrapText(errMsg, maxErrLen)
|
wrappedMsg := wrapText(errMsg, maxErrLen)
|
||||||
|
|
||||||
// Render first line with prefix
|
// Render first line with prefix
|
||||||
@@ -750,9 +773,10 @@ func (m model) buildFooterLines(termWidth int) []string {
|
|||||||
var currentLine strings.Builder
|
var currentLine strings.Builder
|
||||||
currentLineVisualLen := 0
|
currentLineVisualLen := 0
|
||||||
|
|
||||||
// Calculate how much space we need for the total count suffix
|
// Calculate how much space we need for the total count suffix.
|
||||||
|
// Use lipgloss.Width for true display width (the "│" glyph is 3 bytes / 1 col).
|
||||||
totalSuffix := fmt.Sprintf(" │ Total: %d", len(m.ui.forwardOrder))
|
totalSuffix := fmt.Sprintf(" │ Total: %d", len(m.ui.forwardOrder))
|
||||||
totalSuffixLen := len(totalSuffix)
|
totalSuffixLen := lipgloss.Width(totalSuffix)
|
||||||
|
|
||||||
// Available width (account for some margin)
|
// Available width (account for some margin)
|
||||||
availableWidth := termWidth - 4
|
availableWidth := termWidth - 4
|
||||||
@@ -761,8 +785,9 @@ func (m model) buildFooterLines(termWidth int) []string {
|
|||||||
// Build this binding's text
|
// Build this binding's text
|
||||||
keyRendered := keyStyle.Render(binding.key)
|
keyRendered := keyStyle.Render(binding.key)
|
||||||
bindingText := keyRendered + ": " + binding.desc
|
bindingText := keyRendered + ": " + binding.desc
|
||||||
// Visual length without ANSI codes
|
// True display width: strips ANSI and counts wide/unicode glyphs (e.g. ↑↓)
|
||||||
bindingVisualLen := len(binding.key) + 2 + len(binding.desc)
|
// correctly, where len() would over-count multibyte runes and wrap early.
|
||||||
|
bindingVisualLen := lipgloss.Width(bindingText)
|
||||||
|
|
||||||
// Add separator if not first item on line
|
// Add separator if not first item on line
|
||||||
separator := ""
|
separator := ""
|
||||||
|
|||||||
@@ -43,3 +43,15 @@ const (
|
|||||||
// MaxPathWidth is the maximum width for displaying file paths
|
// MaxPathWidth is the maximum width for displaying file paths
|
||||||
MaxPathWidth = 48
|
MaxPathWidth = 48
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// HTTP log table layout
|
||||||
|
const (
|
||||||
|
// HTTPLogRowFormat is the column layout shared by the HTTP-log table header
|
||||||
|
// and its rows (TIME, METHOD, STATUS, LATENCY, PATH) so they stay aligned.
|
||||||
|
HTTPLogRowFormat = "%-10s %-7s %-6s %-8s %s"
|
||||||
|
|
||||||
|
// HTTPLogFixedCols is the width consumed by every column except PATH
|
||||||
|
// (prefix + the four fixed columns and their separators), used to size the
|
||||||
|
// remaining space for the path column responsively.
|
||||||
|
HTTPLogFixedCols = 48
|
||||||
|
)
|
||||||
|
|||||||
+11
-4
@@ -5,6 +5,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/lukaszraczylo/kportal/internal/config"
|
"github.com/lukaszraczylo/kportal/internal/config"
|
||||||
)
|
)
|
||||||
@@ -195,15 +196,21 @@ func hyperlink(url, text string) string {
|
|||||||
return fmt.Sprintf("\x1b]8;;%s\x1b\\%s\x1b]8;;\x1b\\", url, text)
|
return fmt.Sprintf("\x1b]8;;%s\x1b\\%s\x1b]8;;\x1b\\", url, text)
|
||||||
}
|
}
|
||||||
|
|
||||||
// truncate truncates a string to maxLen, adding "..." if needed
|
// truncate truncates a string to maxLen runes, adding "..." if needed.
|
||||||
|
// It counts and slices by rune (not byte) so multibyte aliases/resource names
|
||||||
|
// are never cut mid-rune into mojibake.
|
||||||
func truncate(s string, maxLen int) string {
|
func truncate(s string, maxLen int) string {
|
||||||
if len(s) <= maxLen {
|
if maxLen <= 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if utf8.RuneCountInString(s) <= maxLen {
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
r := []rune(s)
|
||||||
if maxLen <= 3 {
|
if maxLen <= 3 {
|
||||||
return s[:maxLen]
|
return string(r[:maxLen])
|
||||||
}
|
}
|
||||||
return s[:maxLen-3] + "..."
|
return string(r[:maxLen-3]) + "..."
|
||||||
}
|
}
|
||||||
|
|
||||||
// formatStatusWithIndicator adds color-coded indicator symbols to status
|
// formatStatusWithIndicator adds color-coded indicator symbols to status
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package ui
|
package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/lukaszraczylo/kportal/internal/config"
|
"github.com/lukaszraczylo/kportal/internal/config"
|
||||||
@@ -140,10 +141,14 @@ func TestTruncate(t *testing.T) {
|
|||||||
{"hi!", "hi", 2}, // maxLen <= 3 branch: no ellipsis
|
{"hi!", "hi", 2}, // maxLen <= 3 branch: no ellipsis
|
||||||
{"abcd", "abc", 3}, // maxLen <= 3 branch
|
{"abcd", "abc", 3}, // maxLen <= 3 branch
|
||||||
{"", "", 5},
|
{"", "", 5},
|
||||||
|
{"café-service", "café-...", 8}, // multibyte: count/slice by rune, no mojibake
|
||||||
|
{"日本語ポッド", "日本...", 5}, // CJK runes truncated cleanly
|
||||||
|
{"naïve", "naïve", 5}, // exactly maxLen runes (6 bytes) — unchanged
|
||||||
|
{"abc", "", 0}, // non-positive maxLen
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.input+"_"+string(rune('0'+tt.maxLen)), func(t *testing.T) {
|
t.Run(fmt.Sprintf("%s_%d", tt.input, tt.maxLen), func(t *testing.T) {
|
||||||
assert.Equal(t, tt.expected, truncate(tt.input, tt.maxLen))
|
assert.Equal(t, tt.expected, truncate(tt.input, tt.maxLen))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,6 +139,18 @@ func renderBreadcrumb(parts ...string) string {
|
|||||||
return breadcrumbStyle.Render(strings.Join(parts, " / "))
|
return breadcrumbStyle.Render(strings.Join(parts, " / "))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// scrollUpIndicator returns the styled "more items above" hint shown when a
|
||||||
|
// viewport is scrolled down past its first item.
|
||||||
|
func scrollUpIndicator() string {
|
||||||
|
return mutedStyle.Render(" ↑ More above ↑") + "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
// scrollDownIndicator returns the styled "more items below" hint shown when a
|
||||||
|
// viewport has items beyond its last visible row.
|
||||||
|
func scrollDownIndicator() string {
|
||||||
|
return mutedStyle.Render(" ↓ More below ↓") + "\n"
|
||||||
|
}
|
||||||
|
|
||||||
// renderList renders a list of items with cursor selection and viewport scrolling
|
// renderList renders a list of items with cursor selection and viewport scrolling
|
||||||
func renderList(items []string, cursor int, prefix string, scrollOffset int) string {
|
func renderList(items []string, cursor int, prefix string, scrollOffset int) string {
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
@@ -147,7 +159,7 @@ func renderList(items []string, cursor int, prefix string, scrollOffset int) str
|
|||||||
|
|
||||||
// Show scroll up indicator if there are items above the viewport
|
// Show scroll up indicator if there are items above the viewport
|
||||||
if scrollOffset > 0 {
|
if scrollOffset > 0 {
|
||||||
b.WriteString(mutedStyle.Render(" ↑ More above ↑") + "\n")
|
b.WriteString(scrollUpIndicator())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate visible range
|
// Calculate visible range
|
||||||
@@ -171,7 +183,7 @@ func renderList(items []string, cursor int, prefix string, scrollOffset int) str
|
|||||||
|
|
||||||
// Show scroll down indicator if there are items below the viewport
|
// Show scroll down indicator if there are items below the viewport
|
||||||
if end < totalItems {
|
if end < totalItems {
|
||||||
b.WriteString(mutedStyle.Render(" ↓ More below ↓") + "\n")
|
b.WriteString(scrollDownIndicator())
|
||||||
}
|
}
|
||||||
|
|
||||||
return b.String()
|
return b.String()
|
||||||
|
|||||||
+62
-93
@@ -9,8 +9,23 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/lukaszraczylo/kportal/internal/k8s"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// formatDetectedPort renders a discovered port for the port-selection list,
|
||||||
|
// e.g. "8080", "80 → 8000" (service target differs), optionally "(http)".
|
||||||
|
func formatDetectedPort(port k8s.PortInfo) string {
|
||||||
|
desc := fmt.Sprintf("%d", port.Port)
|
||||||
|
if port.TargetPort > 0 && port.TargetPort != port.Port {
|
||||||
|
desc = fmt.Sprintf("%d → %d", port.Port, port.TargetPort)
|
||||||
|
}
|
||||||
|
if port.Name != "" {
|
||||||
|
desc += fmt.Sprintf(" (%s)", port.Name)
|
||||||
|
}
|
||||||
|
return desc
|
||||||
|
}
|
||||||
|
|
||||||
// renderAddWizard renders the appropriate step of the add wizard
|
// renderAddWizard renders the appropriate step of the add wizard
|
||||||
func (m model) renderAddWizard() string {
|
func (m model) renderAddWizard() string {
|
||||||
if m.ui.addWizard == nil {
|
if m.ui.addWizard == nil {
|
||||||
@@ -61,24 +76,25 @@ func (m model) renderSelectContext() string {
|
|||||||
b.WriteString(spinnerStyle.Render("⣾ Loading contexts..."))
|
b.WriteString(spinnerStyle.Render("⣾ Loading contexts..."))
|
||||||
} else if wizard.error != nil {
|
} else if wizard.error != nil {
|
||||||
b.WriteString(errorStyle.Render(fmt.Sprintf("✗ Error: %v", wizard.error)))
|
b.WriteString(errorStyle.Render(fmt.Sprintf("✗ Error: %v", wizard.error)))
|
||||||
|
b.WriteString(mutedStyle.Render("\n\nCould not read kubeconfig. Press Esc to cancel."))
|
||||||
} else if len(wizard.contexts) == 0 {
|
} else if len(wizard.contexts) == 0 {
|
||||||
b.WriteString(mutedStyle.Render("No contexts found in kubeconfig"))
|
b.WriteString(mutedStyle.Render("No contexts found in kubeconfig"))
|
||||||
|
b.WriteString(mutedStyle.Render("\n\nAdd one with: kubectl config use-context <name>"))
|
||||||
} else {
|
} else {
|
||||||
filteredContexts := wizard.getFilteredContexts()
|
filteredContexts := wizard.getFilteredContexts()
|
||||||
if len(filteredContexts) == 0 {
|
if len(filteredContexts) == 0 {
|
||||||
b.WriteString(mutedStyle.Render("No matching contexts"))
|
b.WriteString(mutedStyle.Render("No matching contexts"))
|
||||||
} else {
|
} else {
|
||||||
const viewportHeight = 20
|
|
||||||
totalItems := len(filteredContexts)
|
totalItems := len(filteredContexts)
|
||||||
|
|
||||||
// Show scroll up indicator if there are items above the viewport
|
// Show scroll up indicator if there are items above the viewport
|
||||||
if wizard.scrollOffset > 0 {
|
if wizard.scrollOffset > 0 {
|
||||||
b.WriteString(mutedStyle.Render(" ↑ More above ↑") + "\n")
|
b.WriteString(scrollUpIndicator())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate visible range
|
// Calculate visible range
|
||||||
start := wizard.scrollOffset
|
start := wizard.scrollOffset
|
||||||
end := wizard.scrollOffset + viewportHeight
|
end := wizard.scrollOffset + ViewportHeight
|
||||||
if end > totalItems {
|
if end > totalItems {
|
||||||
end = totalItems
|
end = totalItems
|
||||||
}
|
}
|
||||||
@@ -103,7 +119,7 @@ func (m model) renderSelectContext() string {
|
|||||||
|
|
||||||
// Show scroll down indicator if there are items below the viewport
|
// Show scroll down indicator if there are items below the viewport
|
||||||
if end < totalItems {
|
if end < totalItems {
|
||||||
b.WriteString(mutedStyle.Render(" ↓ More below ↓") + "\n")
|
b.WriteString(scrollDownIndicator())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -124,7 +140,7 @@ func (m model) renderSelectNamespace() string {
|
|||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
|
|
||||||
b.WriteString(renderHeader("Add Port Forward", renderProgress(2, 7)))
|
b.WriteString(renderHeader("Add Port Forward", renderProgress(2, 7)))
|
||||||
b.WriteString(fmt.Sprintf("Context: %s\n\n", breadcrumbStyle.Render(wizard.selectedContext)))
|
fmt.Fprintf(&b, "Context: %s\n\n", breadcrumbStyle.Render(wizard.selectedContext))
|
||||||
|
|
||||||
b.WriteString("Select Namespace:\n\n")
|
b.WriteString("Select Namespace:\n\n")
|
||||||
|
|
||||||
@@ -342,39 +358,23 @@ func (m model) renderEnterRemotePort() string {
|
|||||||
b.WriteString("Select remote port:")
|
b.WriteString("Select remote port:")
|
||||||
b.WriteString("\n\n")
|
b.WriteString("\n\n")
|
||||||
|
|
||||||
const viewportHeight = 20
|
|
||||||
totalItems := len(wizard.detectedPorts) + 1 // +1 for manual entry option
|
totalItems := len(wizard.detectedPorts) + 1 // +1 for manual entry option
|
||||||
|
|
||||||
// Show scroll up indicator if there are items above the viewport
|
// Show scroll up indicator if there are items above the viewport
|
||||||
if wizard.scrollOffset > 0 {
|
if wizard.scrollOffset > 0 {
|
||||||
b.WriteString(mutedStyle.Render(" ↑ More above ↑") + "\n")
|
b.WriteString(scrollUpIndicator())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate visible range
|
// Calculate visible range
|
||||||
start := wizard.scrollOffset
|
start := wizard.scrollOffset
|
||||||
end := wizard.scrollOffset + viewportHeight
|
end := wizard.scrollOffset + ViewportHeight
|
||||||
if end > totalItems {
|
if end > totalItems {
|
||||||
end = totalItems
|
end = totalItems
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render detected ports within viewport
|
// Render detected ports within viewport
|
||||||
for i := start; i < end && i < len(wizard.detectedPorts); i++ {
|
for i := start; i < end && i < len(wizard.detectedPorts); i++ {
|
||||||
port := wizard.detectedPorts[i]
|
portDesc := formatDetectedPort(wizard.detectedPorts[i])
|
||||||
// For services, show both service port and target port if they differ
|
|
||||||
var portDesc string
|
|
||||||
if port.TargetPort > 0 && port.TargetPort != port.Port {
|
|
||||||
// Service with different target port: "80 → 8000 (http)"
|
|
||||||
portDesc = fmt.Sprintf("%d → %d", port.Port, port.TargetPort)
|
|
||||||
if port.Name != "" {
|
|
||||||
portDesc += fmt.Sprintf(" (%s)", port.Name)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Pod port or service with same port
|
|
||||||
portDesc = fmt.Sprintf("%d", port.Port)
|
|
||||||
if port.Name != "" {
|
|
||||||
portDesc += fmt.Sprintf(" (%s)", port.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
prefix := " "
|
prefix := " "
|
||||||
if i == wizard.cursor {
|
if i == wizard.cursor {
|
||||||
@@ -402,7 +402,7 @@ func (m model) renderEnterRemotePort() string {
|
|||||||
|
|
||||||
// Show scroll down indicator if there are items below the viewport
|
// Show scroll down indicator if there are items below the viewport
|
||||||
if end < totalItems {
|
if end < totalItems {
|
||||||
b.WriteString(mutedStyle.Render(" ↓ More below ↓") + "\n")
|
b.WriteString(scrollDownIndicator())
|
||||||
}
|
}
|
||||||
|
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
@@ -412,19 +412,7 @@ func (m model) renderEnterRemotePort() string {
|
|||||||
if len(wizard.detectedPorts) > 0 {
|
if len(wizard.detectedPorts) > 0 {
|
||||||
b.WriteString(mutedStyle.Render("Detected ports:\n"))
|
b.WriteString(mutedStyle.Render("Detected ports:\n"))
|
||||||
for _, port := range wizard.detectedPorts {
|
for _, port := range wizard.detectedPorts {
|
||||||
var portDesc string
|
b.WriteString(mutedStyle.Render(fmt.Sprintf(" • %s\n", formatDetectedPort(port))))
|
||||||
if port.TargetPort > 0 && port.TargetPort != port.Port {
|
|
||||||
portDesc = fmt.Sprintf("%d → %d", port.Port, port.TargetPort)
|
|
||||||
if port.Name != "" {
|
|
||||||
portDesc += fmt.Sprintf(" (%s)", port.Name)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
portDesc = fmt.Sprintf("%d", port.Port)
|
|
||||||
if port.Name != "" {
|
|
||||||
portDesc += fmt.Sprintf(" (%s)", port.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
b.WriteString(mutedStyle.Render(fmt.Sprintf(" • %s\n", portDesc)))
|
|
||||||
}
|
}
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
}
|
}
|
||||||
@@ -503,18 +491,18 @@ func (m model) renderConfirmation() string {
|
|||||||
resourceInfo = fmt.Sprintf("service/%s", wizard.resourceValue)
|
resourceInfo = fmt.Sprintf("service/%s", wizard.resourceValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
b.WriteString(fmt.Sprintf(" Context: %s\n", wizard.selectedContext))
|
fmt.Fprintf(&b, " Context: %s\n", wizard.selectedContext)
|
||||||
b.WriteString(fmt.Sprintf(" Namespace: %s\n", wizard.selectedNamespace))
|
fmt.Fprintf(&b, " Namespace: %s\n", wizard.selectedNamespace)
|
||||||
b.WriteString(fmt.Sprintf(" Resource: %s\n", resourceInfo))
|
fmt.Fprintf(&b, " Resource: %s\n", resourceInfo)
|
||||||
b.WriteString(fmt.Sprintf(" Remote Port: %d\n", wizard.remotePort))
|
fmt.Fprintf(&b, " Remote Port: %d\n", wizard.remotePort)
|
||||||
b.WriteString(fmt.Sprintf(" Local Port: %d\n", wizard.localPort))
|
fmt.Fprintf(&b, " Local Port: %d\n", wizard.localPort)
|
||||||
b.WriteString(" Protocol: tcp\n")
|
b.WriteString(" Protocol: tcp\n")
|
||||||
|
|
||||||
httpLogMark := "[ ] disabled"
|
httpLogMark := "[ ] disabled"
|
||||||
if wizard.httpLog {
|
if wizard.httpLog {
|
||||||
httpLogMark = "[x] enabled"
|
httpLogMark = "[x] enabled"
|
||||||
}
|
}
|
||||||
b.WriteString(fmt.Sprintf(" HTTP Log: %s\n", httpLogMark))
|
fmt.Fprintf(&b, " HTTP Log: %s\n", httpLogMark)
|
||||||
|
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
|
|
||||||
@@ -648,7 +636,7 @@ func (m model) renderRemoveSelection() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
selectedCount := wizard.getSelectedCount()
|
selectedCount := wizard.getSelectedCount()
|
||||||
b.WriteString(fmt.Sprintf("%d of %d selected\n\n", selectedCount, len(wizard.forwards)))
|
fmt.Fprintf(&b, "%d of %d selected\n\n", selectedCount, len(wizard.forwards))
|
||||||
|
|
||||||
b.WriteString(wrapHelpText("Space: Toggle a: All n: None Enter: Remove Esc: Cancel", wizardHelpWidth(m.termWidth)))
|
b.WriteString(wrapHelpText("Space: Toggle a: All n: None Enter: Remove Esc: Cancel", wizardHelpWidth(m.termWidth)))
|
||||||
|
|
||||||
@@ -663,7 +651,7 @@ func (m model) renderRemoveConfirmation() string {
|
|||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
|
|
||||||
selectedCount := wizard.getSelectedCount()
|
selectedCount := wizard.getSelectedCount()
|
||||||
b.WriteString(fmt.Sprintf("Remove %d port forward(s)?\n\n", selectedCount))
|
fmt.Fprintf(&b, "Remove %d port forward(s)?\n\n", selectedCount)
|
||||||
|
|
||||||
selectedForwards := wizard.getSelectedForwards()
|
selectedForwards := wizard.getSelectedForwards()
|
||||||
for _, fwd := range selectedForwards {
|
for _, fwd := range selectedForwards {
|
||||||
@@ -718,7 +706,7 @@ func (m model) renderBenchmarkConfig() string {
|
|||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
|
|
||||||
b.WriteString(renderHeader("HTTP Benchmark", ""))
|
b.WriteString(renderHeader("HTTP Benchmark", ""))
|
||||||
b.WriteString(fmt.Sprintf("Target: %s (localhost:%d)", breadcrumbStyle.Render(state.forwardAlias), state.localPort))
|
fmt.Fprintf(&b, "Target: %s (localhost:%d)", breadcrumbStyle.Render(state.forwardAlias), state.localPort)
|
||||||
b.WriteString("\n\n")
|
b.WriteString("\n\n")
|
||||||
|
|
||||||
b.WriteString("Configure benchmark parameters:")
|
b.WriteString("Configure benchmark parameters:")
|
||||||
@@ -741,7 +729,7 @@ func (m model) renderBenchmarkConfig() string {
|
|||||||
b.WriteString(selectedStyle.Render(fmt.Sprintf("%s%-12s", prefix, field.label+":")))
|
b.WriteString(selectedStyle.Render(fmt.Sprintf("%s%-12s", prefix, field.label+":")))
|
||||||
b.WriteString(validInputStyle.Render(field.value + "█"))
|
b.WriteString(validInputStyle.Render(field.value + "█"))
|
||||||
} else {
|
} else {
|
||||||
b.WriteString(fmt.Sprintf("%s%-12s %s", prefix, field.label+":", field.value))
|
fmt.Fprintf(&b, "%s%-12s %s", prefix, field.label+":", field.value)
|
||||||
}
|
}
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
}
|
}
|
||||||
@@ -759,7 +747,7 @@ func (m model) renderBenchmarkRunning() string {
|
|||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
|
|
||||||
b.WriteString(renderHeader("HTTP Benchmark", ""))
|
b.WriteString(renderHeader("HTTP Benchmark", ""))
|
||||||
b.WriteString(fmt.Sprintf("Target: %s", breadcrumbStyle.Render(state.forwardAlias)))
|
fmt.Fprintf(&b, "Target: %s", breadcrumbStyle.Render(state.forwardAlias))
|
||||||
b.WriteString("\n\n")
|
b.WriteString("\n\n")
|
||||||
|
|
||||||
// Progress bar
|
// Progress bar
|
||||||
@@ -779,7 +767,7 @@ func (m model) renderBenchmarkRunning() string {
|
|||||||
b.WriteString(spinnerStyle.Render("Running benchmark..."))
|
b.WriteString(spinnerStyle.Render("Running benchmark..."))
|
||||||
b.WriteString("\n\n")
|
b.WriteString("\n\n")
|
||||||
|
|
||||||
b.WriteString(fmt.Sprintf(" [%s] %d%%", successStyle.Render(bar), percent))
|
fmt.Fprintf(&b, " [%s] %d%%", successStyle.Render(bar), percent)
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
b.WriteString(mutedStyle.Render(fmt.Sprintf(" %d / %d requests completed", state.progress, state.total)))
|
b.WriteString(mutedStyle.Render(fmt.Sprintf(" %d / %d requests completed", state.progress, state.total)))
|
||||||
b.WriteString("\n\n")
|
b.WriteString("\n\n")
|
||||||
@@ -799,7 +787,7 @@ func (m model) renderBenchmarkResults() string {
|
|||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
|
|
||||||
b.WriteString(renderHeader("Benchmark Results", ""))
|
b.WriteString(renderHeader("Benchmark Results", ""))
|
||||||
b.WriteString(fmt.Sprintf("Target: %s", breadcrumbStyle.Render(state.forwardAlias)))
|
fmt.Fprintf(&b, "Target: %s", breadcrumbStyle.Render(state.forwardAlias))
|
||||||
b.WriteString("\n\n")
|
b.WriteString("\n\n")
|
||||||
|
|
||||||
if state.error != nil {
|
if state.error != nil {
|
||||||
@@ -824,43 +812,43 @@ func (m model) renderBenchmarkResults() string {
|
|||||||
successRate = 0
|
successRate = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
b.WriteString(fmt.Sprintf("Total Requests: %d", r.TotalRequests))
|
fmt.Fprintf(&b, "Total Requests: %d", r.TotalRequests)
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
if r.Failed == 0 {
|
if r.Failed == 0 {
|
||||||
b.WriteString(successStyle.Render(fmt.Sprintf("Successful: %d (%.1f%%)", r.Successful, successRate)))
|
b.WriteString(successStyle.Render(fmt.Sprintf("Successful: %d (%.1f%%)", r.Successful, successRate)))
|
||||||
} else {
|
} else {
|
||||||
b.WriteString(fmt.Sprintf("Successful: %d (%.1f%%)", r.Successful, successRate))
|
fmt.Fprintf(&b, "Successful: %d (%.1f%%)", r.Successful, successRate)
|
||||||
}
|
}
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
if r.Failed > 0 {
|
if r.Failed > 0 {
|
||||||
b.WriteString(errorStyle.Render(fmt.Sprintf("Failed: %d", r.Failed)))
|
b.WriteString(errorStyle.Render(fmt.Sprintf("Failed: %d", r.Failed)))
|
||||||
} else {
|
} else {
|
||||||
b.WriteString(fmt.Sprintf("Failed: %d", r.Failed))
|
fmt.Fprintf(&b, "Failed: %d", r.Failed)
|
||||||
}
|
}
|
||||||
b.WriteString("\n\n")
|
b.WriteString("\n\n")
|
||||||
|
|
||||||
// Latency stats
|
// Latency stats
|
||||||
b.WriteString(breadcrumbStyle.Render("Latency (ms)"))
|
b.WriteString(breadcrumbStyle.Render("Latency (ms)"))
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
b.WriteString(fmt.Sprintf(" Min: %.2f", r.MinLatency))
|
fmt.Fprintf(&b, " Min: %.2f", r.MinLatency)
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
b.WriteString(fmt.Sprintf(" Max: %.2f", r.MaxLatency))
|
fmt.Fprintf(&b, " Max: %.2f", r.MaxLatency)
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
b.WriteString(fmt.Sprintf(" Avg: %.2f", r.AvgLatency))
|
fmt.Fprintf(&b, " Avg: %.2f", r.AvgLatency)
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
b.WriteString(fmt.Sprintf(" P50: %.2f", r.P50Latency))
|
fmt.Fprintf(&b, " P50: %.2f", r.P50Latency)
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
b.WriteString(fmt.Sprintf(" P95: %.2f", r.P95Latency))
|
fmt.Fprintf(&b, " P95: %.2f", r.P95Latency)
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
b.WriteString(fmt.Sprintf(" P99: %.2f", r.P99Latency))
|
fmt.Fprintf(&b, " P99: %.2f", r.P99Latency)
|
||||||
b.WriteString("\n\n")
|
b.WriteString("\n\n")
|
||||||
|
|
||||||
// Throughput
|
// Throughput
|
||||||
b.WriteString(breadcrumbStyle.Render("Throughput"))
|
b.WriteString(breadcrumbStyle.Render("Throughput"))
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
b.WriteString(fmt.Sprintf(" Requests/sec: %.2f", r.Throughput))
|
fmt.Fprintf(&b, " Requests/sec: %.2f", r.Throughput)
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
b.WriteString(fmt.Sprintf(" Bytes read: %d", r.BytesRead))
|
fmt.Fprintf(&b, " Bytes read: %d", r.BytesRead)
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
|
|
||||||
// Status codes if interesting
|
// Status codes if interesting
|
||||||
@@ -874,7 +862,7 @@ func (m model) renderBenchmarkResults() string {
|
|||||||
} else if code >= 400 {
|
} else if code >= 400 {
|
||||||
b.WriteString(errorStyle.Render(fmt.Sprintf(" %d: %d", code, count)))
|
b.WriteString(errorStyle.Render(fmt.Sprintf(" %d: %d", code, count)))
|
||||||
} else {
|
} else {
|
||||||
b.WriteString(fmt.Sprintf(" %d: %d", code, count))
|
fmt.Fprintf(&b, " %d: %d", code, count)
|
||||||
}
|
}
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
}
|
}
|
||||||
@@ -966,7 +954,7 @@ func (m model) renderHTTPLog() string {
|
|||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
|
|
||||||
// Header
|
// Header
|
||||||
header := fmt.Sprintf(" %-10s %-7s %-6s %-8s %s",
|
header := " " + fmt.Sprintf(HTTPLogRowFormat,
|
||||||
"TIME", "METHOD", "STATUS", "LATENCY", "PATH")
|
"TIME", "METHOD", "STATUS", "LATENCY", "PATH")
|
||||||
b.WriteString(mutedStyle.Render(header))
|
b.WriteString(mutedStyle.Render(header))
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
@@ -1004,8 +992,8 @@ func (m model) renderHTTPLog() string {
|
|||||||
end = totalEntries
|
end = totalEntries
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate max path width
|
// Calculate max path width (remaining space after the fixed columns)
|
||||||
maxPathWidth := termWidth - 48
|
maxPathWidth := termWidth - HTTPLogFixedCols
|
||||||
if maxPathWidth < 10 {
|
if maxPathWidth < 10 {
|
||||||
maxPathWidth = 10
|
maxPathWidth = 10
|
||||||
}
|
}
|
||||||
@@ -1028,14 +1016,11 @@ func (m model) renderHTTPLog() string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Truncate path
|
// Truncate path (rune-aware, no mid-rune mojibake)
|
||||||
path := entry.Path
|
path := truncate(entry.Path, maxPathWidth)
|
||||||
if len(path) > maxPathWidth {
|
|
||||||
path = path[:maxPathWidth-3] + "..."
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build line
|
// Build line
|
||||||
line := fmt.Sprintf("%-10s %-7s %-6s %-8s %s",
|
line := fmt.Sprintf(HTTPLogRowFormat,
|
||||||
entry.Timestamp,
|
entry.Timestamp,
|
||||||
entry.Method,
|
entry.Method,
|
||||||
statusStr,
|
statusStr,
|
||||||
@@ -1079,7 +1064,7 @@ func (m model) renderHTTPLog() string {
|
|||||||
// Footer with entry count
|
// Footer with entry count
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
if totalEntries != totalUnfiltered {
|
if totalEntries != totalUnfiltered {
|
||||||
b.WriteString(mutedStyle.Render(fmt.Sprintf(" %d of %d entries (filtered from %d)", totalEntries, totalEntries, totalUnfiltered)))
|
b.WriteString(mutedStyle.Render(fmt.Sprintf(" %d of %d entries (filtered)", totalEntries, totalUnfiltered)))
|
||||||
} else {
|
} else {
|
||||||
b.WriteString(mutedStyle.Render(fmt.Sprintf(" %d entries", totalEntries)))
|
b.WriteString(mutedStyle.Render(fmt.Sprintf(" %d entries", totalEntries)))
|
||||||
}
|
}
|
||||||
@@ -1121,11 +1106,7 @@ func (m model) renderHTTPLogDetail(entry HTTPLogEntry, termWidth, termHeight int
|
|||||||
}
|
}
|
||||||
sort.Strings(headerKeys)
|
sort.Strings(headerKeys)
|
||||||
for _, k := range headerKeys {
|
for _, k := range headerKeys {
|
||||||
v := entry.RequestHeaders[k]
|
v := truncate(entry.RequestHeaders[k], termWidth-20)
|
||||||
// Truncate long header values
|
|
||||||
if len(v) > termWidth-20 {
|
|
||||||
v = v[:termWidth-23] + "..."
|
|
||||||
}
|
|
||||||
lines = append(lines, fmt.Sprintf(" %s: %s", mutedStyle.Render(k), v))
|
lines = append(lines, fmt.Sprintf(" %s: %s", mutedStyle.Render(k), v))
|
||||||
}
|
}
|
||||||
lines = append(lines, "")
|
lines = append(lines, "")
|
||||||
@@ -1146,11 +1127,7 @@ func (m model) renderHTTPLogDetail(entry HTTPLogEntry, termWidth, termHeight int
|
|||||||
reqBody = formatJSONContent(reqBody, entry.RequestHeaders)
|
reqBody = formatJSONContent(reqBody, entry.RequestHeaders)
|
||||||
bodyLines := strings.Split(reqBody, "\n")
|
bodyLines := strings.Split(reqBody, "\n")
|
||||||
for _, line := range bodyLines {
|
for _, line := range bodyLines {
|
||||||
// Truncate long lines
|
lines = append(lines, " "+truncate(line, termWidth-6))
|
||||||
if len(line) > termWidth-6 {
|
|
||||||
line = line[:termWidth-9] + "..."
|
|
||||||
}
|
|
||||||
lines = append(lines, " "+line)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lines = append(lines, "")
|
lines = append(lines, "")
|
||||||
@@ -1192,11 +1169,7 @@ func (m model) renderHTTPLogDetail(entry HTTPLogEntry, termWidth, termHeight int
|
|||||||
}
|
}
|
||||||
sort.Strings(headerKeys)
|
sort.Strings(headerKeys)
|
||||||
for _, k := range headerKeys {
|
for _, k := range headerKeys {
|
||||||
v := entry.ResponseHeaders[k]
|
v := truncate(entry.ResponseHeaders[k], termWidth-20)
|
||||||
// Truncate long header values
|
|
||||||
if len(v) > termWidth-20 {
|
|
||||||
v = v[:termWidth-23] + "..."
|
|
||||||
}
|
|
||||||
lines = append(lines, fmt.Sprintf(" %s: %s", mutedStyle.Render(k), v))
|
lines = append(lines, fmt.Sprintf(" %s: %s", mutedStyle.Render(k), v))
|
||||||
}
|
}
|
||||||
lines = append(lines, "")
|
lines = append(lines, "")
|
||||||
@@ -1217,11 +1190,7 @@ func (m model) renderHTTPLogDetail(entry HTTPLogEntry, termWidth, termHeight int
|
|||||||
respBody = formatJSONContent(respBody, entry.ResponseHeaders)
|
respBody = formatJSONContent(respBody, entry.ResponseHeaders)
|
||||||
bodyLines := strings.Split(respBody, "\n")
|
bodyLines := strings.Split(respBody, "\n")
|
||||||
for _, line := range bodyLines {
|
for _, line := range bodyLines {
|
||||||
// Truncate long lines
|
lines = append(lines, " "+truncate(line, termWidth-6))
|
||||||
if len(line) > termWidth-6 {
|
|
||||||
line = line[:termWidth-9] + "..."
|
|
||||||
}
|
|
||||||
lines = append(lines, " "+line)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lines = append(lines, "")
|
lines = append(lines, "")
|
||||||
|
|||||||
Reference in New Issue
Block a user