Compare commits

...

5 Commits

Author SHA1 Message Date
lukaszraczylo 0f86f8e230 Update the tap link in README and github page 2025-11-23 18:35:49 +00:00
lukaszraczylo 0b07c99342 Remove semver misleading config. 2025-11-23 18:35:49 +00:00
lukaszraczylo 77b3f18a07 Base release - 0.2.x 2025-11-23 18:35:49 +00:00
lukaszraczylo 2e6db9ae2f Add kportal screenshot to documentation
- Add screenshot to README.md
- Add screenshot to GitHub Pages site with styling
- Include screenshot image in docs/ directory
2025-11-23 18:35:48 +00:00
lukaszraczylo 2ca8a2df69 Fix build and deployment issues
- Fix .gitignore to only ignore binary at root (/kportal)
- Add cmd/kportal/main.go to repository (was incorrectly ignored)
- Resolve merge conflict in static.yml workflow
- Ensure GitHub Pages workflow only triggers on docs/ changes
2025-11-23 18:35:37 +00:00
7 changed files with 294 additions and 20 deletions
-3
View File
@@ -5,11 +5,8 @@ on:
# Runs on pushes targeting the default branch
push:
branches: ["main"]
<<<<<<< HEAD
=======
paths:
- 'docs/**'
>>>>>>> b4f4c38 (Add github page.)
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
+1 -1
View File
@@ -1,5 +1,5 @@
CLAUDE.md
kportal
/kportal
DEPLOYMENT_SUMMARY.md
HOMEBREW_COMPLIANCE.md
RELEASE_SETUP.md
+2 -2
View File
@@ -8,7 +8,7 @@
kportal simplifies managing multiple Kubernetes port-forwards with an elegant, interactive terminal interface. Built with [Bubble Tea](https://github.com/charmbracelet/bubbletea), it provides real-time status updates, automatic reconnection, and hot-reload configuration support.
![kportal Demo](docs/images/demo.png)
![kportal Screenshot](docs/kportal-screenshot.png)
## ✨ Features
@@ -33,7 +33,7 @@ kportal simplifies managing multiple Kubernetes port-forwards with an elegant, i
### Homebrew (macOS/Linux)
```bash
brew install lukaszraczylo/tap/kportal
brew install lukaszraczylo/brew-taps/kportal
```
### Quick Install Script
+226
View File
@@ -0,0 +1,226 @@
package main
import (
"flag"
"fmt"
"io"
"log"
"os"
"os/signal"
"syscall"
"time"
"github.com/nvm/kportal/internal/config"
"github.com/nvm/kportal/internal/converter"
"github.com/nvm/kportal/internal/forward"
"github.com/nvm/kportal/internal/ui"
"k8s.io/klog/v2"
)
const (
defaultConfigFile = ".kportal.yaml"
)
var (
configFile = flag.String("c", defaultConfigFile, "Path to configuration file")
verbose = flag.Bool("v", false, "Enable verbose logging")
check = flag.Bool("check", false, "Validate configuration and exit")
showVersion = flag.Bool("version", false, "Show version and exit")
convertInput = flag.String("convert", "", "Convert kftray JSON config to kportal YAML (provide input file path)")
convertOutput = flag.String("convert-output", ".kportal.yaml", "Output file for converted configuration")
version = "0.1.0" // Set via ldflags during build
)
func main() {
flag.Parse()
if *showVersion {
fmt.Printf("kportal version %s\n", version)
os.Exit(0)
}
// Handle conversion mode
if *convertInput != "" {
if err := converter.ConvertKFTrayToKPortal(*convertInput, *convertOutput); err != nil {
fmt.Fprintf(os.Stderr, "Error converting configuration: %v\n", err)
os.Exit(1)
}
// Print summary
contextMap, totalForwards, err := converter.GetConversionSummary(*convertInput)
if err != nil {
fmt.Fprintf(os.Stderr, "Warning: Could not generate summary: %v\n", err)
} else {
fmt.Printf("Successfully converted %d forwards from %s to %s\n", totalForwards, *convertInput, *convertOutput)
fmt.Printf("Generated configuration with:\n")
for ctx, namespaces := range contextMap {
fmt.Printf(" - Context '%s':\n", ctx)
for ns, count := range namespaces {
fmt.Printf(" - Namespace '%s': %d forwards\n", ns, count)
}
}
}
os.Exit(0)
}
if !*verbose {
// In interactive mode, disable ALL logging to avoid interfering with bubbletea UI
log.SetOutput(io.Discard)
log.SetPrefix("")
log.SetFlags(0)
// Disable klog (used by kubernetes client-go)
klog.SetOutput(io.Discard)
klog.LogToStderr(false)
} else {
log.SetFlags(log.LstdFlags | log.Lshortfile)
}
// Load configuration
cfg, err := config.LoadConfig(*configFile)
if err != nil {
fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err)
os.Exit(1)
}
// Validate configuration
validator := config.NewValidator()
if errs := validator.ValidateConfig(cfg); len(errs) > 0 {
fmt.Fprint(os.Stderr, config.FormatValidationErrors(errs))
os.Exit(1)
}
if *check {
fmt.Println("Configuration is valid")
os.Exit(0)
}
// Only log startup messages in verbose mode
if *verbose {
log.Printf("kportal v%s", version)
log.Printf("Loading configuration from: %s", *configFile)
}
// Create forward manager
manager := forward.NewManager(*verbose)
// Create UI (bubbletea for interactive, simple table for verbose)
var bubbleTeaUI *ui.BubbleTeaUI
var tableUI *ui.TableUI
if !*verbose {
// Interactive mode with bubbletea
bubbleTeaUI = ui.NewBubbleTeaUI(func(id string, enable bool) {
if enable {
manager.EnableForward(id)
} else {
manager.DisableForward(id)
}
}, version)
manager.SetStatusUI(bubbleTeaUI)
} else {
// Verbose mode with simple table
tableUI = ui.NewTableUI(*verbose)
manager.SetStatusUI(tableUI)
}
// Start forwards
if err := manager.Start(cfg); err != nil {
fmt.Fprintf(os.Stderr, "Error starting forwards: %v\n", err)
os.Exit(1)
}
if *verbose {
// Verbose mode - use simple table with periodic updates
tableUI.RenderInitial()
// Setup signal handling
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP)
// Start table update loop
go func() {
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
for range ticker.C {
tableUI.Render()
}
}()
// Setup config watcher for hot-reload
watcher, err := config.NewWatcher(*configFile, func(newCfg *config.Config) error {
return manager.Reload(newCfg)
}, *verbose)
if err != nil {
log.Printf("Warning: Failed to setup config watcher: %v", err)
log.Printf("Hot-reload will not be available")
} else {
watcher.Start()
defer watcher.Stop()
}
log.Printf("Press Ctrl+C to stop")
// Wait for signals
for {
sig := <-sigChan
switch sig {
case syscall.SIGHUP:
log.Printf("Received SIGHUP, reloading configuration...")
newCfg, err := config.LoadConfig(*configFile)
if err != nil {
log.Printf("Failed to reload config: %v", err)
continue
}
if errs := validator.ValidateConfig(newCfg); len(errs) > 0 {
log.Printf("Config validation failed:")
log.Print(config.FormatValidationErrors(errs))
continue
}
if err := manager.Reload(newCfg); err != nil {
log.Printf("Failed to reload: %v", err)
}
case os.Interrupt, syscall.SIGTERM:
log.Printf("Received shutdown signal, stopping...")
manager.Stop()
os.Exit(0)
}
}
} else {
// Interactive mode with bubbletea
// Setup config watcher in background
watcher, err := config.NewWatcher(*configFile, func(newCfg *config.Config) error {
return manager.Reload(newCfg)
}, *verbose)
if err == nil {
watcher.Start()
defer watcher.Stop()
}
// Setup signal handler for clean shutdown
go func() {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
<-sigChan
bubbleTeaUI.Stop()
manager.Stop()
os.Exit(0)
}()
// Give a moment for initial forwards to be added
time.Sleep(100 * time.Millisecond)
// Start the bubbletea app (blocks until quit)
if err := bubbleTeaUI.Start(); err != nil {
fmt.Fprintf(os.Stderr, "Failed to start UI: %v\n", err)
manager.Stop()
os.Exit(1)
}
// Clean shutdown
manager.Stop()
}
}
+64 -12
View File
@@ -79,6 +79,10 @@
<img src="https://img.shields.io/github/license/lukaszraczylo/kportal" alt="License"/>
<img src="https://goreportcard.com/badge/github.com/lukaszraczylo/kportal" alt="Go Report"/>
</div>
<!-- Screenshot -->
<div class="mt-16 max-w-4xl mx-auto">
<img src="kportal-screenshot.png" alt="kportal terminal interface" class="rounded-lg shadow-2xl border border-gray-200 dark:border-gray-700"/>
</div>
</div>
</div>
</section>
@@ -158,9 +162,9 @@
<p class="text-gray-600 dark:text-gray-400 text-sm">macOS & Linux</p>
</div>
</div>
<div onclick="copyToClipboard('brew install lukaszraczylo/tap/kportal', this)"
<div onclick="copyToClipboard('brew install lukaszraczylo/brew-taps/kportal', this)"
class="bg-gray-900 dark:bg-gray-950 text-gray-100 p-4 rounded text-sm cursor-pointer hover:bg-gray-800 dark:hover:bg-black transition group relative">
<code class="block">brew install lukaszraczylo/tap/kportal</code>
<code class="block">brew install lukaszraczylo/brew-taps/kportal</code>
<i class="fas fa-copy absolute top-3 right-3 text-gray-500 group-hover:text-gray-300 text-xs"></i>
</div>
</div>
@@ -353,17 +357,65 @@
}
});
// Copy to clipboard function
// Copy to clipboard function with fallback
function copyToClipboard(text, button) {
navigator.clipboard.writeText(text).then(() => {
const originalHTML = button.innerHTML;
button.innerHTML = '<i class="fas fa-check text-green-500"></i>';
setTimeout(() => {
button.innerHTML = originalHTML;
}, 2000);
}).catch(err => {
console.error('Failed to copy:', err);
});
// Modern clipboard API (preferred)
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(() => {
showCopySuccess(button);
}).catch(err => {
console.error('Clipboard API failed:', err);
fallbackCopy(text, button);
});
} else {
// Fallback for older browsers or insecure contexts
fallbackCopy(text, button);
}
}
// Fallback copy method using execCommand
function fallbackCopy(text, button) {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.top = '0';
textarea.style.left = '0';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
try {
const successful = document.execCommand('copy');
if (successful) {
showCopySuccess(button);
} else {
showCopyError(button);
}
} catch (err) {
console.error('Fallback copy failed:', err);
showCopyError(button);
}
document.body.removeChild(textarea);
}
// Show success feedback
function showCopySuccess(button) {
const originalHTML = button.innerHTML;
button.innerHTML = '<i class="fas fa-check text-green-500"></i>';
setTimeout(() => {
button.innerHTML = originalHTML;
}, 2000);
}
// Show error feedback
function showCopyError(button) {
const originalHTML = button.innerHTML;
button.innerHTML = '<i class="fas fa-times text-red-500"></i>';
setTimeout(() => {
button.innerHTML = originalHTML;
}, 2000);
}
// Smooth scrolling
Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

+1 -2
View File
@@ -2,7 +2,7 @@ version: 1
force:
major: 0
minor: 1
minor: 2
patch: 0
blacklist:
@@ -19,5 +19,4 @@ wording:
- "major"
- "BREAKING CHANGE"
release:
- "rc"
- "release candidate"