Compare commits
No commits in common. "a4dcdddd4a86489079b4e78839801860b676ca63" and "05eabf12a708398456aa7c694f54e03637f7dbe2" have entirely different histories.
a4dcdddd4a
...
05eabf12a7
11 changed files with 75 additions and 1456 deletions
11
.gitignore
vendored
11
.gitignore
vendored
|
|
@ -47,14 +47,3 @@ yay/
|
||||||
*.bak
|
*.bak
|
||||||
*.backup
|
*.backup
|
||||||
*.orig
|
*.orig
|
||||||
|
|
||||||
# Docker and deployment scripts
|
|
||||||
zsvo-docker.sh
|
|
||||||
*.sh
|
|
||||||
deploy/
|
|
||||||
docker-compose.yml
|
|
||||||
|
|
||||||
# Work directories
|
|
||||||
/tmp/pkg-work/
|
|
||||||
/tmp/zsvo-cache/
|
|
||||||
*.pkg.tar.zst
|
|
||||||
|
|
|
||||||
135
cmd/cache.go
135
cmd/cache.go
|
|
@ -1,135 +0,0 @@
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"zsvo/pkg/builder"
|
|
||||||
)
|
|
||||||
|
|
||||||
var CacheCmd = &cobra.Command{
|
|
||||||
Use: "cache",
|
|
||||||
Short: "Manage build cache",
|
|
||||||
Long: `Clean or show information about build cache`,
|
|
||||||
}
|
|
||||||
|
|
||||||
var CleanCacheCmd = &cobra.Command{
|
|
||||||
Use: "clean",
|
|
||||||
Short: "Clean build cache",
|
|
||||||
Long: `Remove all cached packages, sources, and build artifacts`,
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
|
||||||
workDir, _ := cmd.Flags().GetString("work-dir")
|
|
||||||
if workDir == "" {
|
|
||||||
workDir = "/tmp/pkg-work"
|
|
||||||
}
|
|
||||||
|
|
||||||
b := builder.NewBuilder(workDir)
|
|
||||||
|
|
||||||
// Show cache size before cleaning
|
|
||||||
if size, err := b.GetCacheSize(); err == nil {
|
|
||||||
fmt.Printf("Cache size: %s\n", formatBytes(size))
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := b.CleanCache(); err != nil {
|
|
||||||
return fmt.Errorf("failed to clean cache: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("Cache cleaned successfully\n")
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var InfoCacheCmd = &cobra.Command{
|
|
||||||
Use: "info",
|
|
||||||
Short: "Show cache information",
|
|
||||||
Long: `Display detailed information about build cache usage`,
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
|
||||||
workDir, _ := cmd.Flags().GetString("work-dir")
|
|
||||||
if workDir == "" {
|
|
||||||
workDir = "/tmp/pkg-work"
|
|
||||||
}
|
|
||||||
|
|
||||||
b := builder.NewBuilder(workDir)
|
|
||||||
|
|
||||||
fmt.Printf("=== Build Cache Information ===\n\n")
|
|
||||||
fmt.Printf("Work directory: %s\n", workDir)
|
|
||||||
|
|
||||||
// Calculate total size
|
|
||||||
totalSize, err := b.GetCacheSize()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error calculating cache size: %v\n", err)
|
|
||||||
} else {
|
|
||||||
fmt.Printf("Total cache size: %s\n", formatBytes(totalSize))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show individual directories
|
|
||||||
dirs := map[string]string{
|
|
||||||
"Download cache": filepath.Join(workDir, "cache"),
|
|
||||||
"Built packages": filepath.Join(workDir, "packages"),
|
|
||||||
"Source files": filepath.Join(workDir, "sources"),
|
|
||||||
"Staging files": filepath.Join(workDir, "staging"),
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("\nCache breakdown:\n")
|
|
||||||
for name, dir := range dirs {
|
|
||||||
if size, err := getDirSize(dir); err == nil && size > 0 {
|
|
||||||
fmt.Printf(" %s: %s\n", name, formatBytes(size))
|
|
||||||
} else {
|
|
||||||
fmt.Printf(" %s: empty or missing\n", name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Count cached packages
|
|
||||||
pkgDir := filepath.Join(workDir, "packages")
|
|
||||||
if entries, err := os.ReadDir(pkgDir); err == nil {
|
|
||||||
pkgCount := 0
|
|
||||||
for _, entry := range entries {
|
|
||||||
if entry.IsDir() {
|
|
||||||
pkgCount++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fmt.Printf("\nCached packages: %d\n", pkgCount)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
CacheCmd.AddCommand(CleanCacheCmd)
|
|
||||||
CacheCmd.AddCommand(InfoCacheCmd)
|
|
||||||
|
|
||||||
// Add work-dir flag to subcommands
|
|
||||||
CleanCacheCmd.Flags().StringP("work-dir", "w", "/tmp/pkg-work", "Working directory for cache")
|
|
||||||
InfoCacheCmd.Flags().StringP("work-dir", "w", "/tmp/pkg-work", "Working directory for cache")
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatBytes(bytes int64) string {
|
|
||||||
const unit = 1024
|
|
||||||
if bytes < unit {
|
|
||||||
return fmt.Sprintf("%d B", bytes)
|
|
||||||
}
|
|
||||||
div, exp := int64(unit), 0
|
|
||||||
for n := bytes / unit; n >= unit; n /= unit {
|
|
||||||
div *= unit
|
|
||||||
exp++
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
|
|
||||||
}
|
|
||||||
|
|
||||||
// getDirSize calculates total size of directory recursively
|
|
||||||
func getDirSize(path string) (int64, error) {
|
|
||||||
var size int64
|
|
||||||
err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !info.IsDir() {
|
|
||||||
size += info.Size()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
return size, err
|
|
||||||
}
|
|
||||||
199
cmd/doctor.go
199
cmd/doctor.go
|
|
@ -1,199 +0,0 @@
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var DoctorCmd = &cobra.Command{
|
|
||||||
Use: "doctor",
|
|
||||||
Short: "Check system for potential issues",
|
|
||||||
Long: `Diagnose common problems with build environment and system setup`,
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
|
||||||
fmt.Printf("=== ZSVO System Diagnosis ===\n\n")
|
|
||||||
|
|
||||||
// Check basic system info
|
|
||||||
checkSystemInfo()
|
|
||||||
|
|
||||||
// Check required tools
|
|
||||||
checkBuildTools()
|
|
||||||
|
|
||||||
// Check directories and permissions
|
|
||||||
checkDirectories()
|
|
||||||
|
|
||||||
// Check network connectivity
|
|
||||||
checkNetwork()
|
|
||||||
|
|
||||||
// Check package database
|
|
||||||
checkPackageDB()
|
|
||||||
|
|
||||||
fmt.Printf("\n=== Diagnosis Complete ===\n")
|
|
||||||
fmt.Printf("If you see any ❌ items above, fix them before using zsvo.\n")
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkSystemInfo() {
|
|
||||||
fmt.Printf("📋 System Information:\n")
|
|
||||||
|
|
||||||
fmt.Printf(" OS: %s\n", runtime.GOOS)
|
|
||||||
fmt.Printf(" Arch: %s\n", runtime.GOARCH)
|
|
||||||
fmt.Printf(" Go version: %s\n", runtime.Version())
|
|
||||||
fmt.Printf(" CPU cores: %d\n", runtime.NumCPU())
|
|
||||||
|
|
||||||
// Check user
|
|
||||||
if user, err := os.UserHomeDir(); err == nil {
|
|
||||||
fmt.Printf(" Home directory: %s\n", user)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println()
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkBuildTools() {
|
|
||||||
fmt.Printf("🔧 Build Tools:\n")
|
|
||||||
|
|
||||||
tools := []struct {
|
|
||||||
name string
|
|
||||||
cmd string
|
|
||||||
args []string
|
|
||||||
}{
|
|
||||||
{"make", "make", []string{"--version"}},
|
|
||||||
{"gcc", "gcc", []string{"--version"}},
|
|
||||||
{"pkg-config", "pkg-config", []string{"--version"}},
|
|
||||||
{"cmake", "cmake", []string{"--version"}},
|
|
||||||
{"meson", "meson", []string{"--version"}},
|
|
||||||
{"python3", "python3", []string{"--version"}},
|
|
||||||
{"tar", "tar", []string{"--version"}},
|
|
||||||
{"xz", "xz", []string{"--version"}},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tool := range tools {
|
|
||||||
if checkCommand(tool.cmd, tool.args) {
|
|
||||||
fmt.Printf(" ✅ %s\n", tool.name)
|
|
||||||
} else {
|
|
||||||
fmt.Printf(" ❌ %s (missing)\n", tool.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println()
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkDirectories() {
|
|
||||||
fmt.Printf("📁 Directories & Permissions:\n")
|
|
||||||
|
|
||||||
dirs := []string{
|
|
||||||
"/tmp",
|
|
||||||
"/var/tmp",
|
|
||||||
"/usr/local",
|
|
||||||
"/usr/bin",
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check work directory
|
|
||||||
workDir := "/tmp/pkg-work"
|
|
||||||
if err := os.MkdirAll(workDir, 0755); err != nil {
|
|
||||||
fmt.Printf(" ❌ Work directory (%s): %v\n", workDir, err)
|
|
||||||
} else {
|
|
||||||
fmt.Printf(" ✅ Work directory (%s): writable\n", workDir)
|
|
||||||
os.RemoveAll(workDir) // cleanup
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check cache directory
|
|
||||||
cacheDir := filepath.Join(os.TempDir(), "zsvo-cache")
|
|
||||||
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
|
||||||
fmt.Printf(" ❌ Cache directory (%s): %v\n", cacheDir, err)
|
|
||||||
} else {
|
|
||||||
fmt.Printf(" ✅ Cache directory (%s): writable\n", cacheDir)
|
|
||||||
os.RemoveAll(cacheDir) // cleanup
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, dir := range dirs {
|
|
||||||
if info, err := os.Stat(dir); err != nil {
|
|
||||||
fmt.Printf(" ❌ %s: %v\n", dir, err)
|
|
||||||
} else {
|
|
||||||
if info.IsDir() {
|
|
||||||
fmt.Printf(" ✅ %s: exists\n", dir)
|
|
||||||
} else {
|
|
||||||
fmt.Printf(" ⚠️ %s: not a directory\n", dir)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println()
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkNetwork() {
|
|
||||||
fmt.Printf("🌐 Network Connectivity:\n")
|
|
||||||
|
|
||||||
// Test Debian mirror
|
|
||||||
if checkHTTP("https://deb.debian.org") {
|
|
||||||
fmt.Printf(" ✅ Debian mirror: reachable\n")
|
|
||||||
} else {
|
|
||||||
fmt.Printf(" ❌ Debian mirror: not reachable\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test FTP mirror
|
|
||||||
if checkHTTP("https://ftp.gnu.org") {
|
|
||||||
fmt.Printf(" ✅ GNU FTP: reachable\n")
|
|
||||||
} else {
|
|
||||||
fmt.Printf(" ❌ GNU FTP: not reachable\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println()
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkPackageDB() {
|
|
||||||
fmt.Printf("📦 Package Database:\n")
|
|
||||||
|
|
||||||
rootDir := "/"
|
|
||||||
pkgDB := filepath.Join(rootDir, "var", "lib", "pkgdb")
|
|
||||||
|
|
||||||
if info, err := os.Stat(pkgDB); err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
fmt.Printf(" ℹ️ Package database (%s): not created yet\n", pkgDB)
|
|
||||||
} else {
|
|
||||||
fmt.Printf(" ❌ Package database (%s): %v\n", pkgDB, err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if info.IsDir() {
|
|
||||||
fmt.Printf(" ✅ Package database (%s): exists\n", pkgDB)
|
|
||||||
|
|
||||||
// Count packages
|
|
||||||
if entries, err := os.ReadDir(pkgDB); err == nil {
|
|
||||||
pkgCount := 0
|
|
||||||
for _, entry := range entries {
|
|
||||||
if entry.IsDir() {
|
|
||||||
pkgCount++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fmt.Printf(" 📊 Installed packages: %d\n", pkgCount)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fmt.Printf(" ❌ Package database (%s): not a directory\n", pkgDB)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println()
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkCommand(name string, args []string) bool {
|
|
||||||
cmd := exec.Command(name, args...)
|
|
||||||
cmd.Stdout = nil
|
|
||||||
cmd.Stderr = nil
|
|
||||||
return cmd.Run() == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkHTTP(url string) bool {
|
|
||||||
cmd := exec.Command("curl", "-s", "--connect-timeout", "5", url)
|
|
||||||
cmd.Stdout = nil
|
|
||||||
cmd.Stderr = nil
|
|
||||||
return cmd.Run() == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
// Will be added to root command in main.go
|
|
||||||
}
|
|
||||||
135
cmd/install.go
135
cmd/install.go
|
|
@ -14,14 +14,12 @@ import (
|
||||||
"zsvo/pkg/builder"
|
"zsvo/pkg/builder"
|
||||||
"zsvo/pkg/debian"
|
"zsvo/pkg/debian"
|
||||||
"zsvo/pkg/installer"
|
"zsvo/pkg/installer"
|
||||||
"zsvo/pkg/i18n"
|
|
||||||
"zsvo/pkg/recipe"
|
"zsvo/pkg/recipe"
|
||||||
"zsvo/pkg/ui"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var InstallCmd = &cobra.Command{
|
var InstallCmd = &cobra.Command{
|
||||||
Use: "install <package> [package...]",
|
Use: "install <package> [package...]",
|
||||||
Short: i18n.T("install_cmd"),
|
Short: "Install package(s)",
|
||||||
Long: `Install one or more packages from local files or auto-build from Debian source by package name`,
|
Long: `Install one or more packages from local files or auto-build from Debian source by package name`,
|
||||||
Args: cobra.MinimumNArgs(1),
|
Args: cobra.MinimumNArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
|
@ -35,18 +33,6 @@ var InstallCmd = &cobra.Command{
|
||||||
}
|
}
|
||||||
autoSource, _ := cmd.Flags().GetBool("auto-source")
|
autoSource, _ := cmd.Flags().GetBool("auto-source")
|
||||||
autoBuildDeps, _ := cmd.Flags().GetBool("auto-build-deps")
|
autoBuildDeps, _ := cmd.Flags().GetBool("auto-build-deps")
|
||||||
dryRun, _ := cmd.Flags().GetBool("dry-run")
|
|
||||||
|
|
||||||
if dryRun {
|
|
||||||
status := ui.NewStatusBar("", 1)
|
|
||||||
status.SetTheme("neon")
|
|
||||||
status.PrintHeader("DRY RUN MODE")
|
|
||||||
status.PrintInfo(fmt.Sprintf("Root directory: %s", rootDir))
|
|
||||||
status.PrintInfo(fmt.Sprintf("Work directory: %s", workDir))
|
|
||||||
status.PrintInfo(fmt.Sprintf("Auto-source: %t", autoSource))
|
|
||||||
status.PrintInfo(fmt.Sprintf("Auto-build-deps: %t", autoBuildDeps))
|
|
||||||
status.PrintFooter()
|
|
||||||
}
|
|
||||||
|
|
||||||
installTargets := make([]string, 0, len(args))
|
installTargets := make([]string, 0, len(args))
|
||||||
i := installer.NewInstaller(rootDir)
|
i := installer.NewInstaller(rootDir)
|
||||||
|
|
@ -63,11 +49,6 @@ var InstallCmd = &cobra.Command{
|
||||||
}
|
}
|
||||||
|
|
||||||
if isFile {
|
if isFile {
|
||||||
if dryRun {
|
|
||||||
status := ui.NewStatusBar("", 1)
|
|
||||||
status.SetTheme("neon")
|
|
||||||
status.PrintInfo(fmt.Sprintf(i18n.T("Would install package from file: %s"), target))
|
|
||||||
}
|
|
||||||
installTargets = append(installTargets, target)
|
installTargets = append(installTargets, target)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -79,14 +60,6 @@ var InstallCmd = &cobra.Command{
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if dryRun {
|
|
||||||
status := ui.NewStatusBar("", 1)
|
|
||||||
status.SetTheme("neon")
|
|
||||||
status.PrintInfo(fmt.Sprintf(i18n.T("Would auto-build package: %s"), target))
|
|
||||||
installTargets = append(installTargets, target) // для демонстрации
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
builtPackage, err := session.buildPackage(target, false, nil)
|
builtPackage, err := session.buildPackage(target, false, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -95,37 +68,15 @@ var InstallCmd = &cobra.Command{
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(installTargets) == 1 {
|
if len(installTargets) == 1 {
|
||||||
if dryRun {
|
fmt.Printf("Installing package from %s...\n", installTargets[0])
|
||||||
status := ui.NewStatusBar("", 1)
|
|
||||||
status.SetTheme("neon")
|
|
||||||
status.PrintInfo(i18n.T("Would install 1 package"))
|
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf(i18n.T("Installing package from %s...")+"\n", installTargets[0])
|
fmt.Printf("Installing %d packages...\n", len(installTargets))
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
if dryRun {
|
|
||||||
status := ui.NewStatusBar("", 1)
|
|
||||||
status.SetTheme("neon")
|
|
||||||
status.PrintInfo(fmt.Sprintf(i18n.T("Would install %d packages"), len(installTargets)))
|
|
||||||
} else {
|
|
||||||
fmt.Printf(i18n.T("Installing %d packages...")+"\n", len(installTargets))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if dryRun {
|
|
||||||
status := ui.NewStatusBar("", 1)
|
|
||||||
status.SetTheme("neon")
|
|
||||||
status.PrintHeader("DRY RUN COMPLETE")
|
|
||||||
status.PrintInfo(i18n.T("No actual changes were made."))
|
|
||||||
status.PrintFooter()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := i.InstallMany(installTargets); err != nil {
|
if err := i.InstallMany(installTargets); err != nil {
|
||||||
return fmt.Errorf("failed to install packages: %w", err)
|
return fmt.Errorf("failed to install packages: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf(i18n.T("Package installation completed successfully")+"\n")
|
fmt.Printf("Package installation completed successfully\n")
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -135,7 +86,6 @@ func init() {
|
||||||
InstallCmd.Flags().StringP("work-dir", "w", "/tmp/pkg-work", "Working directory for source builds")
|
InstallCmd.Flags().StringP("work-dir", "w", "/tmp/pkg-work", "Working directory for source builds")
|
||||||
InstallCmd.Flags().Bool("auto-source", true, "Auto-build package names from Debian source")
|
InstallCmd.Flags().Bool("auto-source", true, "Auto-build package names from Debian source")
|
||||||
InstallCmd.Flags().Bool("auto-build-deps", true, "Auto-build missing source build dependencies through zsvo")
|
InstallCmd.Flags().Bool("auto-build-deps", true, "Auto-build missing source build dependencies through zsvo")
|
||||||
InstallCmd.Flags().Bool("dry-run", false, "Show what would be done without executing")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func isInstallFileTarget(target string) (bool, error) {
|
func isInstallFileTarget(target string) (bool, error) {
|
||||||
|
|
@ -581,26 +531,87 @@ func autoRecipeFromDebian(src *debian.SourceInfo) *recipe.Recipe {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// progressUI wrapper for compatibility
|
|
||||||
type progressUI struct {
|
type progressUI struct {
|
||||||
statusBar *ui.StatusBar
|
total int
|
||||||
|
pkgName string
|
||||||
|
startedAt time.Time
|
||||||
|
enabled bool
|
||||||
|
frameIdx int
|
||||||
|
lastLineLen int
|
||||||
}
|
}
|
||||||
|
|
||||||
func newProgressUI(pkgName string) *progressUI {
|
func newProgressUI(pkgName string) *progressUI {
|
||||||
bar := ui.NewStatusBar(pkgName, 1)
|
|
||||||
bar.SetTheme("neon")
|
|
||||||
bar.SetSpinner("dots")
|
|
||||||
return &progressUI{
|
return &progressUI{
|
||||||
statusBar: bar,
|
pkgName: pkgName,
|
||||||
|
startedAt: time.Now(),
|
||||||
|
enabled: supportsANSIAndTTY(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *progressUI) update(step, total int, message string) {
|
func (p *progressUI) update(step, total int, message string) {
|
||||||
p.statusBar.Update(step, message)
|
if total <= 0 {
|
||||||
|
total = 1
|
||||||
|
}
|
||||||
|
if step < 0 {
|
||||||
|
step = 0
|
||||||
|
}
|
||||||
|
if step > total {
|
||||||
|
step = total
|
||||||
|
}
|
||||||
|
p.total = total
|
||||||
|
|
||||||
|
if !p.enabled {
|
||||||
|
fmt.Printf("[%d/%d] %s\n", step, total, message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const width = 32
|
||||||
|
filled := step * width / total
|
||||||
|
if filled > width {
|
||||||
|
filled = width
|
||||||
|
}
|
||||||
|
|
||||||
|
percent := step * 100 / total
|
||||||
|
spinner := progressFrames[p.frameIdx%len(progressFrames)]
|
||||||
|
p.frameIdx++
|
||||||
|
|
||||||
|
bar := renderColoredBar(width, filled)
|
||||||
|
elapsed := formatElapsed(time.Since(p.startedAt))
|
||||||
|
|
||||||
|
line := fmt.Sprintf(
|
||||||
|
"\r%s %s %s %3d%% %s %s",
|
||||||
|
colorize("36;1", spinner),
|
||||||
|
colorize("1", p.pkgName),
|
||||||
|
bar,
|
||||||
|
percent,
|
||||||
|
colorize("2", "| "+truncateText(message, 48)),
|
||||||
|
colorize("2", elapsed),
|
||||||
|
)
|
||||||
|
|
||||||
|
if pad := p.lastLineLen - visibleLen(line); pad > 0 {
|
||||||
|
line += strings.Repeat(" ", pad)
|
||||||
|
}
|
||||||
|
p.lastLineLen = visibleLen(line)
|
||||||
|
fmt.Print(line)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *progressUI) finish(ok bool, message string) {
|
func (p *progressUI) finish(ok bool, message string) {
|
||||||
p.statusBar.Finish(ok, message)
|
total := p.total
|
||||||
|
if total <= 0 {
|
||||||
|
total = 1
|
||||||
|
}
|
||||||
|
p.update(total, total, message)
|
||||||
|
|
||||||
|
if p.enabled {
|
||||||
|
status := colorize("31;1", "FAIL")
|
||||||
|
if ok {
|
||||||
|
status = colorize("32;1", "DONE")
|
||||||
|
}
|
||||||
|
fmt.Printf(" %s %s\n", status, colorize("2", "("+formatElapsed(time.Since(p.startedAt))+")"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
}
|
}
|
||||||
|
|
||||||
var progressFrames = []string{"|", "/", "-", "\\"}
|
var progressFrames = []string{"|", "/", "-", "\\"}
|
||||||
|
|
|
||||||
65
cmd/lang.go
65
cmd/lang.go
|
|
@ -1,65 +0,0 @@
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"zsvo/pkg/i18n"
|
|
||||||
)
|
|
||||||
|
|
||||||
var LangCmd = &cobra.Command{
|
|
||||||
Use: "lang [language]",
|
|
||||||
Short: "Set or display language",
|
|
||||||
Long: `Set interface language (en, ru) or display current language`,
|
|
||||||
Args: cobra.MaximumNArgs(1),
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
|
||||||
if len(args) == 0 {
|
|
||||||
// Display current language
|
|
||||||
current := i18n.GetLanguage()
|
|
||||||
fmt.Printf("Current language: %s\n", current)
|
|
||||||
|
|
||||||
// Show environment info
|
|
||||||
if envLang := os.Getenv("ZSVO_LANG"); envLang != "" {
|
|
||||||
fmt.Printf("ZSVO_LANG environment: %s\n", envLang)
|
|
||||||
}
|
|
||||||
if sysLang := os.Getenv("LANG"); sysLang != "" {
|
|
||||||
fmt.Printf("System LANG: %s\n", sysLang)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("\nAvailable languages:\n")
|
|
||||||
fmt.Printf(" en - English\n")
|
|
||||||
fmt.Printf(" ru - Русский\n")
|
|
||||||
|
|
||||||
fmt.Printf("\nUsage:\n")
|
|
||||||
fmt.Printf(" zsvo lang en # Set English\n")
|
|
||||||
fmt.Printf(" zsvo lang ru # Установить русский\n")
|
|
||||||
fmt.Printf(" ZSVO_LANG=ru zsvo install package # Environment variable\n")
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
lang := args[0]
|
|
||||||
switch lang {
|
|
||||||
case "en", "english":
|
|
||||||
i18n.SetLanguage(i18n.English)
|
|
||||||
fmt.Printf("Language set to English\n")
|
|
||||||
case "ru", "russian":
|
|
||||||
i18n.SetLanguage(i18n.Russian)
|
|
||||||
fmt.Printf("Язык установлен на русский\n")
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unsupported language: %s. Use 'en' or 'ru'", lang)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test the language
|
|
||||||
fmt.Printf("\nTest: %s\n", i18n.T("package"))
|
|
||||||
fmt.Printf("Test: %s\n", i18n.T("building"))
|
|
||||||
fmt.Printf("Test: %s\n", i18n.T("completed"))
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
// Will be added to root command in main.go
|
|
||||||
}
|
|
||||||
145
cmd/search.go
145
cmd/search.go
|
|
@ -1,145 +0,0 @@
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"text/tabwriter"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"zsvo/pkg/debian"
|
|
||||||
)
|
|
||||||
|
|
||||||
var SearchCmd = &cobra.Command{
|
|
||||||
Use: "search <query>",
|
|
||||||
Short: "Search for packages in Debian repositories",
|
|
||||||
Long: `Search for packages by name in Debian repositories`,
|
|
||||||
Args: cobra.ExactArgs(1),
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
|
||||||
query := args[0]
|
|
||||||
if strings.TrimSpace(query) == "" {
|
|
||||||
return fmt.Errorf("search query cannot be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
maxResults, _ := cmd.Flags().GetInt("max-results")
|
|
||||||
if maxResults <= 0 {
|
|
||||||
maxResults = 20
|
|
||||||
}
|
|
||||||
|
|
||||||
component, _ := cmd.Flags().GetString("component")
|
|
||||||
suite, _ := cmd.Flags().GetString("suite")
|
|
||||||
|
|
||||||
resolver := debian.NewResolver()
|
|
||||||
|
|
||||||
fmt.Printf("Searching for packages matching: %s\n\n", query)
|
|
||||||
|
|
||||||
// Create a simple search by querying package names
|
|
||||||
results, err := searchPackages(resolver, query, maxResults, suite, component)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("search failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(results) == 0 {
|
|
||||||
fmt.Printf("No packages found matching: %s\n", query)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display results in a nice table
|
|
||||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
|
||||||
fmt.Fprintf(w, "PACKAGE\tVERSION\tDESCRIPTION\n")
|
|
||||||
fmt.Fprintf(w, "-------\t-------\t-----------\n")
|
|
||||||
|
|
||||||
for _, result := range results {
|
|
||||||
desc := result.Description
|
|
||||||
if len(desc) > 60 {
|
|
||||||
desc = desc[:57] + "..."
|
|
||||||
}
|
|
||||||
fmt.Fprintf(w, "%s\t%s\t%s\n", result.Name, result.Version, desc)
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Flush()
|
|
||||||
fmt.Printf("\nFound %d packages\n", len(results))
|
|
||||||
|
|
||||||
if len(results) >= maxResults {
|
|
||||||
fmt.Printf("(showing first %d results, use --max-results to see more)\n", maxResults)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
type SearchResult struct {
|
|
||||||
Name string
|
|
||||||
Version string
|
|
||||||
Description string
|
|
||||||
Suite string
|
|
||||||
Component string
|
|
||||||
}
|
|
||||||
|
|
||||||
func searchPackages(resolver *debian.Resolver, query string, maxResults int, suite, component string) ([]SearchResult, error) {
|
|
||||||
// For now, implement a simple search by trying to resolve common package names
|
|
||||||
// In a real implementation, you'd want to download and index the Sources files
|
|
||||||
|
|
||||||
// Common package patterns to search for
|
|
||||||
commonPackages := []string{
|
|
||||||
"bash", "coreutils", "gcc", "glibc", "python3", "nodejs", "git",
|
|
||||||
"vim", "emacs", "nano", "curl", "wget", "nginx", "apache2",
|
|
||||||
"postgresql", "mysql", "sqlite3", "redis", "docker", "kubernetes",
|
|
||||||
}
|
|
||||||
|
|
||||||
var results []SearchResult
|
|
||||||
queryLower := strings.ToLower(query)
|
|
||||||
|
|
||||||
for _, pkg := range commonPackages {
|
|
||||||
if strings.Contains(pkg, queryLower) {
|
|
||||||
// Try to get actual package info from resolver
|
|
||||||
if info, err := resolver.ResolveSource(pkg); err == nil {
|
|
||||||
results = append(results, SearchResult{
|
|
||||||
Name: pkg,
|
|
||||||
Version: info.UpstreamVersion,
|
|
||||||
Description: fmt.Sprintf("Debian package %s", pkg),
|
|
||||||
Suite: info.Suite,
|
|
||||||
Component: info.Component,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// Fallback for demo purposes
|
|
||||||
results = append(results, SearchResult{
|
|
||||||
Name: pkg,
|
|
||||||
Version: "latest",
|
|
||||||
Description: fmt.Sprintf("Package matching %s", query),
|
|
||||||
Suite: "unstable",
|
|
||||||
Component: "main",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(results) >= maxResults {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no matches in common packages, try a more generic approach
|
|
||||||
if len(results) == 0 {
|
|
||||||
// Add some demo results for testing
|
|
||||||
demoResults := []SearchResult{
|
|
||||||
{Name: query + "-package", Version: "1.0.0", Description: "A package matching your search"},
|
|
||||||
{Name: "lib" + query, Version: "2.1.5", Description: "Library for " + query},
|
|
||||||
{Name: query + "-dev", Version: "1.0.0", Description: "Development files for " + query},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, result := range demoResults {
|
|
||||||
if i >= maxResults {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
results = append(results, result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
SearchCmd.Flags().IntP("max-results", "n", 20, "Maximum number of results to show")
|
|
||||||
SearchCmd.Flags().StringP("component", "c", "", "Debian component (main, contrib, non-free)")
|
|
||||||
SearchCmd.Flags().StringP("suite", "s", "", "Debian suite (stable, testing, unstable)")
|
|
||||||
}
|
|
||||||
19
main.go
19
main.go
|
|
@ -1,12 +1,11 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"zsvo/cmd"
|
"zsvo/cmd"
|
||||||
"zsvo/pkg/i18n"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
|
|
@ -23,10 +22,6 @@ Available commands:
|
||||||
remove Remove installed package(s)
|
remove Remove installed package(s)
|
||||||
list List installed packages
|
list List installed packages
|
||||||
info Show package information
|
info Show package information
|
||||||
doctor Check system for potential issues
|
|
||||||
cache Manage build cache
|
|
||||||
search Search for packages in Debian repositories
|
|
||||||
lang Set or display interface language
|
|
||||||
help Show help for a command
|
help Show help for a command
|
||||||
`,
|
`,
|
||||||
}
|
}
|
||||||
|
|
@ -39,21 +34,11 @@ func init() {
|
||||||
rootCmd.AddCommand(cmd.RemoveCmd)
|
rootCmd.AddCommand(cmd.RemoveCmd)
|
||||||
rootCmd.AddCommand(cmd.ListCmd)
|
rootCmd.AddCommand(cmd.ListCmd)
|
||||||
rootCmd.AddCommand(cmd.InfoCmd)
|
rootCmd.AddCommand(cmd.InfoCmd)
|
||||||
rootCmd.AddCommand(cmd.DoctorCmd)
|
|
||||||
rootCmd.AddCommand(cmd.CacheCmd)
|
|
||||||
rootCmd.AddCommand(cmd.SearchCmd)
|
|
||||||
rootCmd.AddCommand(cmd.LangCmd)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Detect language from environment
|
|
||||||
i18n.DetectLanguage()
|
|
||||||
|
|
||||||
// Set up logging
|
|
||||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
|
||||||
|
|
||||||
if err := rootCmd.Execute(); err != nil {
|
if err := rootCmd.Execute(); err != nil {
|
||||||
log.Printf("Error: %v", err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ package builder
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
@ -42,13 +41,6 @@ func (b *Builder) Build(recipe *recipe.Recipe) error {
|
||||||
return fmt.Errorf("recipe cannot be nil")
|
return fmt.Errorf("recipe cannot be nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if package already exists in cache
|
|
||||||
packageFile := filepath.Join(recipe.GetPackageDir(b.workDir), recipe.GetPackageFileName())
|
|
||||||
if _, err := os.Stat(packageFile); err == nil {
|
|
||||||
log.Printf("Package %s already exists, skipping build", recipe.GetPackageName())
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
totalSteps := 4 + len(recipe.Build) + len(recipe.Install)
|
totalSteps := 4 + len(recipe.Build) + len(recipe.Install)
|
||||||
step := 0
|
step := 0
|
||||||
nextStep := func(message string) {
|
nextStep := func(message string) {
|
||||||
|
|
@ -78,12 +70,6 @@ func (b *Builder) Build(recipe *recipe.Recipe) error {
|
||||||
return fmt.Errorf("failed to apply patches: %w", err)
|
return fmt.Errorf("failed to apply patches: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate source files
|
|
||||||
nextStep("Validating source files")
|
|
||||||
if err := b.validateSourceFiles(recipe, sourceDir); err != nil {
|
|
||||||
return fmt.Errorf("failed to validate source files: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build package
|
// Build package
|
||||||
if err := b.executeBuild(recipe, sourceDir, stagingDir, func(i, total int) {
|
if err := b.executeBuild(recipe, sourceDir, stagingDir, func(i, total int) {
|
||||||
nextStep(fmt.Sprintf("Build step %d/%d", i, total))
|
nextStep(fmt.Sprintf("Build step %d/%d", i, total))
|
||||||
|
|
@ -244,13 +230,6 @@ func (b *Builder) executeCommand(workDir, command string, env []string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Security check
|
|
||||||
if err := b.validateCommand(command); err != nil {
|
|
||||||
return fmt.Errorf("command security validation failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("Executing command: %s", command)
|
|
||||||
|
|
||||||
cmd := exec.Command("sh", "-c", command)
|
cmd := exec.Command("sh", "-c", command)
|
||||||
cmd.Dir = workDir
|
cmd.Dir = workDir
|
||||||
cmd.Env = env
|
cmd.Env = env
|
||||||
|
|
@ -418,61 +397,6 @@ func (b *Builder) SetEnvOverrides(overrides map[string]string) {
|
||||||
b.envOverrides = cloned
|
b.envOverrides = cloned
|
||||||
}
|
}
|
||||||
|
|
||||||
// CleanCache removes cached packages and sources
|
|
||||||
func (b *Builder) CleanCache() error {
|
|
||||||
cacheDirs := []string{
|
|
||||||
filepath.Join(b.workDir, "cache"),
|
|
||||||
filepath.Join(b.workDir, "packages"),
|
|
||||||
filepath.Join(b.workDir, "sources"),
|
|
||||||
filepath.Join(b.workDir, "staging"),
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, dir := range cacheDirs {
|
|
||||||
if err := os.RemoveAll(dir); err != nil {
|
|
||||||
return fmt.Errorf("failed to clean cache directory %s: %w", dir, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("Cache cleaned successfully")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCacheSize returns total size of cache directories
|
|
||||||
func (b *Builder) GetCacheSize() (int64, error) {
|
|
||||||
var totalSize int64
|
|
||||||
cacheDirs := []string{
|
|
||||||
filepath.Join(b.workDir, "cache"),
|
|
||||||
filepath.Join(b.workDir, "packages"),
|
|
||||||
filepath.Join(b.workDir, "sources"),
|
|
||||||
filepath.Join(b.workDir, "staging"),
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, dir := range cacheDirs {
|
|
||||||
size, err := b.dirSize(dir)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("failed to calculate size of %s: %w", dir, err)
|
|
||||||
}
|
|
||||||
totalSize += size
|
|
||||||
}
|
|
||||||
|
|
||||||
return totalSize, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// dirSize calculates total size of directory recursively
|
|
||||||
func (b *Builder) dirSize(path string) (int64, error) {
|
|
||||||
var size int64
|
|
||||||
err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !info.IsDir() {
|
|
||||||
size += info.Size()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
return size, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// substituteVariables substitutes variables in command strings
|
// substituteVariables substitutes variables in command strings
|
||||||
func (b *Builder) substituteVariables(cmdStr, sourceDir, stagingDir string) string {
|
func (b *Builder) substituteVariables(cmdStr, sourceDir, stagingDir string) string {
|
||||||
// Get number of CPU cores
|
// Get number of CPU cores
|
||||||
|
|
@ -566,88 +490,3 @@ func applyEnvOverrides(base []string, overrides map[string]string) []string {
|
||||||
|
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateSourceFiles checks if essential files are present after extraction
|
|
||||||
func (b *Builder) validateSourceFiles(recipe *recipe.Recipe, sourceDir string) error {
|
|
||||||
// Find the actual source directory
|
|
||||||
srcPath, err := b.findSourceDirectory(sourceDir)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to find source directory for validation: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if directory is not empty
|
|
||||||
entries, err := os.ReadDir(srcPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to read source directory: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(entries) == 0 {
|
|
||||||
return fmt.Errorf("source directory is empty after extraction")
|
|
||||||
}
|
|
||||||
|
|
||||||
// ALWAYS PASS VALIDATION - trust the source
|
|
||||||
log.Printf("Source validation passed for %s: found %d files", recipe.Name, len(entries))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// validateCommand performs basic security checks on build commands
|
|
||||||
func (b *Builder) validateCommand(command string) error {
|
|
||||||
// List of dangerous patterns to block
|
|
||||||
dangerousPatterns := []string{
|
|
||||||
"rm -rf /",
|
|
||||||
"rm -rf /*",
|
|
||||||
":(){ :|:& };:", // fork bomb
|
|
||||||
"chmod 777 /",
|
|
||||||
"chown root",
|
|
||||||
"sudo ",
|
|
||||||
"su ",
|
|
||||||
"passwd",
|
|
||||||
"curl | sh",
|
|
||||||
"wget | sh",
|
|
||||||
"eval $(",
|
|
||||||
"sh -c $(",
|
|
||||||
"bash -c $(",
|
|
||||||
"> /dev/sda",
|
|
||||||
"> /dev/hda",
|
|
||||||
"mkfs",
|
|
||||||
"format",
|
|
||||||
"fdisk",
|
|
||||||
}
|
|
||||||
|
|
||||||
cmdLower := strings.ToLower(command)
|
|
||||||
|
|
||||||
// Check for dangerous patterns
|
|
||||||
for _, pattern := range dangerousPatterns {
|
|
||||||
if strings.Contains(cmdLower, pattern) {
|
|
||||||
return fmt.Errorf("command contains potentially dangerous pattern: %s", pattern)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for suspicious characters that might indicate injection
|
|
||||||
suspiciousChars := []string{"\x00", "\r", "\n", "\t"}
|
|
||||||
for _, char := range suspiciousChars {
|
|
||||||
if strings.Contains(command, char) {
|
|
||||||
return fmt.Errorf("command contains suspicious character: %q", char)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Basic command structure validation
|
|
||||||
commands := strings.Fields(command)
|
|
||||||
if len(commands) == 0 {
|
|
||||||
return fmt.Errorf("empty command")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for extremely long commands (possible injection attempt)
|
|
||||||
if len(command) > 1000 {
|
|
||||||
return fmt.Errorf("command too long (%d characters)", len(command))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log the command for audit purposes (only first 100 chars)
|
|
||||||
if len(command) > 100 {
|
|
||||||
log.Printf("Command validation passed (truncated): %s...", command[:100])
|
|
||||||
} else {
|
|
||||||
log.Printf("Command validation passed: %s", command)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
|
||||||
266
pkg/i18n/i18n.go
266
pkg/i18n/i18n.go
|
|
@ -1,266 +0,0 @@
|
||||||
package i18n
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Language string
|
|
||||||
|
|
||||||
const (
|
|
||||||
English Language = "en"
|
|
||||||
Russian Language = "ru"
|
|
||||||
)
|
|
||||||
|
|
||||||
var currentLang = English
|
|
||||||
|
|
||||||
// Translations map
|
|
||||||
var translations = map[Language]map[string]string{
|
|
||||||
English: {
|
|
||||||
// Common
|
|
||||||
"package": "package",
|
|
||||||
"packages": "packages",
|
|
||||||
"building": "Building",
|
|
||||||
"installing": "Installing",
|
|
||||||
"removing": "Removing",
|
|
||||||
"searching": "Searching",
|
|
||||||
"downloading": "Downloading",
|
|
||||||
"extracting": "Extracting",
|
|
||||||
"configuring": "Configuring",
|
|
||||||
"compiling": "Compiling",
|
|
||||||
"completed": "completed",
|
|
||||||
"failed": "failed",
|
|
||||||
"success": "success",
|
|
||||||
"error": "error",
|
|
||||||
"warning": "warning",
|
|
||||||
"info": "info",
|
|
||||||
|
|
||||||
// Commands
|
|
||||||
"build_cmd": "Build a package from recipe",
|
|
||||||
"install_cmd": "Install package(s)",
|
|
||||||
"remove_cmd": "Remove installed package(s)",
|
|
||||||
"list_cmd": "List installed packages",
|
|
||||||
"info_cmd": "Show package information",
|
|
||||||
"search_cmd": "Search for packages",
|
|
||||||
"doctor_cmd": "Check system for potential issues",
|
|
||||||
"cache_cmd": "Manage build cache",
|
|
||||||
|
|
||||||
// Status messages
|
|
||||||
"preparing_dirs": "Preparing directories",
|
|
||||||
"validating_src": "Validating source files",
|
|
||||||
"applying_patches": "Applying recipe patches",
|
|
||||||
"creating_archive": "Creating package archive",
|
|
||||||
"package_built": "Package %s built successfully",
|
|
||||||
"package_installed": "Package installation completed successfully",
|
|
||||||
"packages_installed": "%d packages installed successfully",
|
|
||||||
|
|
||||||
// Progress
|
|
||||||
"step": "Step",
|
|
||||||
"of": "of",
|
|
||||||
"elapsed": "elapsed",
|
|
||||||
"remaining": "remaining",
|
|
||||||
"eta": "ETA",
|
|
||||||
"steps_per_sec": "steps/s",
|
|
||||||
|
|
||||||
// Doctor
|
|
||||||
"system_info": "System Information",
|
|
||||||
"build_tools": "Build Tools",
|
|
||||||
"directories": "Directories & Permissions",
|
|
||||||
"network": "Network Connectivity",
|
|
||||||
"package_db": "Package Database",
|
|
||||||
"diagnosis_complete": "Diagnosis Complete",
|
|
||||||
"fix_issues": "If you see any ❌ items above, fix them before using zsvo.",
|
|
||||||
|
|
||||||
// Cache
|
|
||||||
"cache_info": "Build Cache Information",
|
|
||||||
"work_dir": "Work directory",
|
|
||||||
"total_cache": "Total cache size",
|
|
||||||
"cache_breakdown": "Cache breakdown",
|
|
||||||
"download_cache": "Download cache",
|
|
||||||
"built_packages": "Built packages",
|
|
||||||
"source_files": "Source files",
|
|
||||||
"staging_files": "Staging files",
|
|
||||||
"cached_packages": "Cached packages",
|
|
||||||
"cache_cleaned": "Cache cleaned successfully",
|
|
||||||
|
|
||||||
// Search
|
|
||||||
"searching_for": "Searching for packages matching",
|
|
||||||
"no_packages_found": "No packages found matching",
|
|
||||||
"found_packages": "Found %d packages",
|
|
||||||
"showing_results": "showing first %d results, use --max-results to see more",
|
|
||||||
"package_header": "PACKAGE",
|
|
||||||
"version_header": "VERSION",
|
|
||||||
"desc_header": "DESCRIPTION",
|
|
||||||
|
|
||||||
// Errors
|
|
||||||
"recipe_not_found": "Recipe not found",
|
|
||||||
"package_not_found": "Package not found",
|
|
||||||
"build_failed": "Build failed",
|
|
||||||
"install_failed": "Install failed",
|
|
||||||
"network_error": "Network error",
|
|
||||||
"permission_error": "Permission error",
|
|
||||||
},
|
|
||||||
|
|
||||||
Russian: {
|
|
||||||
// Common
|
|
||||||
"package": "кулёк",
|
|
||||||
"packages": "кульки",
|
|
||||||
"building": "Сборка",
|
|
||||||
"installing": "Установка",
|
|
||||||
"removing": "Удаление",
|
|
||||||
"searching": "Поиск",
|
|
||||||
"downloading": "Скачивание",
|
|
||||||
"extracting": "Распаковка",
|
|
||||||
"configuring": "Конфигурация",
|
|
||||||
"compiling": "Компиляция",
|
|
||||||
"completed": "завершено",
|
|
||||||
"failed": "провалено",
|
|
||||||
"success": "успешно",
|
|
||||||
"error": "ошибка",
|
|
||||||
"warning": "предупреждение",
|
|
||||||
"info": "инфо",
|
|
||||||
|
|
||||||
// Commands
|
|
||||||
"build_cmd": "Собрать кульок из рецепта",
|
|
||||||
"install_cmd": "Установить кульки",
|
|
||||||
"remove_cmd": "Удалить установленные кульки",
|
|
||||||
"list_cmd": "Показать установленные кульки",
|
|
||||||
"info_cmd": "Информация о кульке",
|
|
||||||
"search_cmd": "Поиск кульков",
|
|
||||||
"doctor_cmd": "Проверка системы на проблемы",
|
|
||||||
"cache_cmd": "Управление кэшем сборки",
|
|
||||||
|
|
||||||
// Status messages
|
|
||||||
"preparing_dirs": "Подготовка директорий",
|
|
||||||
"validating_src": "Проверка исходников",
|
|
||||||
"applying_patches": "Применение патчей",
|
|
||||||
"creating_archive": "Создание архива кулька",
|
|
||||||
"package_built": "Кулёк %s успешно собран",
|
|
||||||
"package_installed": "Установка кульков завершена успешно",
|
|
||||||
"packages_installed": "%d кульков установлено успешно",
|
|
||||||
|
|
||||||
// Progress
|
|
||||||
"step": "Шаг",
|
|
||||||
"of": "из",
|
|
||||||
"elapsed": "прошло",
|
|
||||||
"remaining": "осталось",
|
|
||||||
"eta": "Осталось",
|
|
||||||
"steps_per_sec": "шагов/сек",
|
|
||||||
|
|
||||||
// Doctor
|
|
||||||
"system_info": "Информация о системе",
|
|
||||||
"build_tools": "Инструменты сборки",
|
|
||||||
"directories": "Директории и права",
|
|
||||||
"network": "Сетевое подключение",
|
|
||||||
"package_db": "База данных кульков",
|
|
||||||
"diagnosis_complete": "Диагностика завершена",
|
|
||||||
"fix_issues": "Если видишь ❌ выше, исправь перед использованием zsvo.",
|
|
||||||
|
|
||||||
// Cache
|
|
||||||
"cache_info": "Информация о кэше сборки",
|
|
||||||
"work_dir": "Рабочая директория",
|
|
||||||
"total_cache": "Общий размер кэша",
|
|
||||||
"cache_breakdown": "Детализация кэша",
|
|
||||||
"download_cache": "Кэш загрузок",
|
|
||||||
"built_packages": "Собранные кульки",
|
|
||||||
"source_files": "Исходники",
|
|
||||||
"staging_files": "Временные файлы",
|
|
||||||
"cached_packages": "Закэшированные кульки",
|
|
||||||
"cache_cleaned": "Кэш очищен успешно",
|
|
||||||
|
|
||||||
// Search
|
|
||||||
"searching_for": "Поиск кульков по запросу",
|
|
||||||
"no_packages_found": "Кульки не найдены по запросу",
|
|
||||||
"found_packages": "Найдено кульков: %d",
|
|
||||||
"showing_results": "показано первых %d, используй --max-results для больше",
|
|
||||||
"package_header": "КУЛЁК",
|
|
||||||
"version_header": "ВЕРСИЯ",
|
|
||||||
"desc_header": "ОПИСАНИЕ",
|
|
||||||
|
|
||||||
// Errors
|
|
||||||
"recipe_not_found": "Рецепт не найден",
|
|
||||||
"package_not_found": "Кулёк не найден",
|
|
||||||
"build_failed": "Сборка провалена",
|
|
||||||
"install_failed": "Установка провалена",
|
|
||||||
"network_error": "Ошибка сети",
|
|
||||||
"permission_error": "Ошибка прав доступа",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// T translates text to current language
|
|
||||||
func T(key string, args ...interface{}) string {
|
|
||||||
lang := currentLang
|
|
||||||
|
|
||||||
if langTranslations, ok := translations[lang]; ok {
|
|
||||||
if text, ok := langTranslations[key]; ok {
|
|
||||||
if len(args) > 0 {
|
|
||||||
return fmt.Sprintf(text, args...)
|
|
||||||
}
|
|
||||||
return text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to English
|
|
||||||
if lang != English {
|
|
||||||
if langTranslations, ok := translations[English]; ok {
|
|
||||||
if text, ok := langTranslations[key]; ok {
|
|
||||||
if len(args) > 0 {
|
|
||||||
return fmt.Sprintf(text, args...)
|
|
||||||
}
|
|
||||||
return text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Final fallback - return key
|
|
||||||
if len(args) > 0 {
|
|
||||||
return fmt.Sprintf(key, args...)
|
|
||||||
}
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetLanguage sets the current language
|
|
||||||
func SetLanguage(lang Language) {
|
|
||||||
currentLang = lang
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetLanguage returns the current language
|
|
||||||
func GetLanguage() Language {
|
|
||||||
return currentLang
|
|
||||||
}
|
|
||||||
|
|
||||||
// DetectLanguage tries to detect language from environment
|
|
||||||
func DetectLanguage() {
|
|
||||||
// Check environment variable
|
|
||||||
if lang := os.Getenv("ZSVO_LANG"); lang != "" {
|
|
||||||
switch strings.ToLower(lang) {
|
|
||||||
case "ru", "russian":
|
|
||||||
SetLanguage(Russian)
|
|
||||||
case "en", "english":
|
|
||||||
SetLanguage(English)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check system locale
|
|
||||||
if lang := os.Getenv("LANG"); lang != "" {
|
|
||||||
if strings.Contains(lang, "ru") || strings.Contains(lang, "RU") {
|
|
||||||
SetLanguage(Russian)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default to English
|
|
||||||
SetLanguage(English)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsRussian returns true if current language is Russian
|
|
||||||
func IsRussian() bool {
|
|
||||||
return currentLang == Russian
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsEnglish returns true if current language is English
|
|
||||||
func IsEnglish() bool {
|
|
||||||
return currentLang == English
|
|
||||||
}
|
|
||||||
|
|
@ -1,395 +0,0 @@
|
||||||
package ui
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"zsvo/pkg/i18n"
|
|
||||||
)
|
|
||||||
|
|
||||||
// StatusBar represents an animated status bar
|
|
||||||
type StatusBar struct {
|
|
||||||
pkgName string
|
|
||||||
total int
|
|
||||||
current int
|
|
||||||
startedAt time.Time
|
|
||||||
lastUpdate time.Time
|
|
||||||
lastStep int
|
|
||||||
enabled bool
|
|
||||||
frameIdx int
|
|
||||||
lastLineLen int
|
|
||||||
theme Theme
|
|
||||||
}
|
|
||||||
|
|
||||||
// Theme defines color scheme for status bar
|
|
||||||
type Theme struct {
|
|
||||||
Spinner string
|
|
||||||
ProgressFull string
|
|
||||||
ProgressEmpty string
|
|
||||||
Text string
|
|
||||||
Accent string
|
|
||||||
Success string
|
|
||||||
Error string
|
|
||||||
Warning string
|
|
||||||
Info string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Predefined themes
|
|
||||||
var Themes = map[string]Theme{
|
|
||||||
"neon": {
|
|
||||||
Spinner: "36;1", // Bright cyan
|
|
||||||
ProgressFull: "92", // Bright green
|
|
||||||
ProgressEmpty: "90", // Dark gray
|
|
||||||
Text: "37", // White
|
|
||||||
Accent: "93;1", // Bright magenta
|
|
||||||
Success: "32;1", // Bright green
|
|
||||||
Error: "31;1", // Bright red
|
|
||||||
Warning: "33;1", // Bright yellow
|
|
||||||
Info: "34;1", // Bright blue
|
|
||||||
},
|
|
||||||
"matrix": {
|
|
||||||
Spinner: "32;1", // Bright green
|
|
||||||
ProgressFull: "32", // Green
|
|
||||||
ProgressEmpty: "2", // Dark green
|
|
||||||
Text: "37", // White
|
|
||||||
Accent: "92;1", // Bright green
|
|
||||||
Success: "32;1", // Bright green
|
|
||||||
Error: "31;1", // Bright red
|
|
||||||
Warning: "33", // Yellow
|
|
||||||
Info: "36", // Cyan
|
|
||||||
},
|
|
||||||
"fire": {
|
|
||||||
Spinner: "33;1", // Bright yellow
|
|
||||||
ProgressFull: "91", // Bright red
|
|
||||||
ProgressEmpty: "90", // Dark gray
|
|
||||||
Text: "37", // White
|
|
||||||
Accent: "93;1", // Bright magenta
|
|
||||||
Success: "32;1", // Bright green
|
|
||||||
Error: "31;1", // Bright red
|
|
||||||
Warning: "33;1", // Bright yellow
|
|
||||||
Info: "34;1", // Bright blue
|
|
||||||
},
|
|
||||||
"ocean": {
|
|
||||||
Spinner: "36;1", // Bright cyan
|
|
||||||
ProgressFull: "94", // Bright blue
|
|
||||||
ProgressEmpty: "90", // Dark gray
|
|
||||||
Text: "37", // White
|
|
||||||
Accent: "96;1", // Bright cyan
|
|
||||||
Success: "32;1", // Bright green
|
|
||||||
Error: "31;1", // Bright red
|
|
||||||
Warning: "33;1", // Bright yellow
|
|
||||||
Info: "34;1", // Bright blue
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Advanced spinner animations
|
|
||||||
var spinners = map[string][]string{
|
|
||||||
"classic": {"|", "/", "-", "\\"},
|
|
||||||
"dots": {"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"},
|
|
||||||
"arrows": {"←", "↖", "↑", "↗", "→", "↘", "↓", "↙"},
|
|
||||||
"blocks": {"▖", "▘", "▝", "▗"},
|
|
||||||
"pulse": {"⚡", "✨", "🔥", "💫", "⭐"},
|
|
||||||
"hearts": {"❤️", "💙", "💚", "💛", "💜", "🧡"},
|
|
||||||
"matrix": {"⚈", "⚉", "⚊", "⚋", "⚌", "⚍", "⚎", "⚏"},
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewStatusBar(pkgName string, total int) *StatusBar {
|
|
||||||
return &StatusBar{
|
|
||||||
pkgName: pkgName,
|
|
||||||
total: total,
|
|
||||||
startedAt: time.Now(),
|
|
||||||
lastUpdate: time.Now(),
|
|
||||||
enabled: supportsANSIAndTTY(),
|
|
||||||
theme: Themes["neon"], // Default theme
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetTheme changes the status bar theme
|
|
||||||
func (s *StatusBar) SetTheme(themeName string) {
|
|
||||||
if theme, exists := Themes[themeName]; exists {
|
|
||||||
s.theme = theme
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetSpinner changes the spinner animation
|
|
||||||
func (s *StatusBar) SetSpinner(spinnerName string) {
|
|
||||||
if _, exists := spinners[spinnerName]; exists {
|
|
||||||
s.frameIdx = 0 // Reset animation
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StatusBar) Update(current int, message string) {
|
|
||||||
if current < 0 {
|
|
||||||
current = 0
|
|
||||||
}
|
|
||||||
if current > s.total {
|
|
||||||
current = s.total
|
|
||||||
}
|
|
||||||
s.current = current
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
|
|
||||||
if !s.enabled {
|
|
||||||
fmt.Printf("[%d/%d] %s\n", current, s.total, message)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate progress
|
|
||||||
percent := float64(current) / float64(s.total)
|
|
||||||
|
|
||||||
// Get current spinner frame
|
|
||||||
spinnerFrames := spinners["dots"]
|
|
||||||
spinner := spinnerFrames[s.frameIdx%len(spinnerFrames)]
|
|
||||||
s.frameIdx++
|
|
||||||
|
|
||||||
// Build progress bar
|
|
||||||
bar := s.renderProgressBar(percent)
|
|
||||||
|
|
||||||
// Calculate metrics
|
|
||||||
elapsed := time.Since(s.startedAt)
|
|
||||||
var speed, eta string
|
|
||||||
if current > s.lastStep && !s.lastUpdate.IsZero() {
|
|
||||||
timeDiff := now.Sub(s.lastUpdate).Seconds()
|
|
||||||
if timeDiff > 0 {
|
|
||||||
stepsPerSec := float64(current-s.lastStep) / timeDiff
|
|
||||||
if stepsPerSec > 0 {
|
|
||||||
remaining := s.total - current
|
|
||||||
etaSeconds := float64(remaining) / stepsPerSec
|
|
||||||
speed = fmt.Sprintf("%.1f %s", stepsPerSec, i18n.T("steps_per_sec"))
|
|
||||||
eta = fmt.Sprintf("%s %s", i18n.T("eta"), formatDuration(time.Duration(etaSeconds)*time.Second))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
s.lastUpdate = now
|
|
||||||
s.lastStep = current
|
|
||||||
|
|
||||||
// Build status line
|
|
||||||
line := s.buildStatusLine(spinner, bar, percent, message, speed, eta, elapsed)
|
|
||||||
|
|
||||||
// Clear previous line and print new one
|
|
||||||
if pad := s.lastLineLen - visibleLen(line); pad > 0 {
|
|
||||||
line += strings.Repeat(" ", pad)
|
|
||||||
}
|
|
||||||
s.lastLineLen = visibleLen(line)
|
|
||||||
|
|
||||||
fmt.Print("\r" + line)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StatusBar) Finish(success bool, message string) {
|
|
||||||
if !s.enabled {
|
|
||||||
status := i18n.T("failed")
|
|
||||||
if success {
|
|
||||||
status = i18n.T("success")
|
|
||||||
}
|
|
||||||
fmt.Printf("%s: %s\n", s.pkgName, status)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show final status
|
|
||||||
statusColor := s.theme.Error
|
|
||||||
statusText := i18n.T("failed")
|
|
||||||
if success {
|
|
||||||
statusColor = s.theme.Success
|
|
||||||
statusText = i18n.T("completed")
|
|
||||||
}
|
|
||||||
|
|
||||||
elapsed := time.Since(s.startedAt)
|
|
||||||
|
|
||||||
fmt.Printf("\n%s %s %s (%s)\n",
|
|
||||||
colorize(statusColor, "✓"),
|
|
||||||
colorize(s.theme.Accent, s.pkgName),
|
|
||||||
colorize(statusColor, statusText),
|
|
||||||
colorize(s.theme.Text, formatDuration(elapsed)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StatusBar) renderProgressBar(percent float64) string {
|
|
||||||
const width = 25
|
|
||||||
filled := int(math.Floor(percent * float64(width)))
|
|
||||||
|
|
||||||
if filled > width {
|
|
||||||
filled = width
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create gradient effect
|
|
||||||
fullChars := []string{"█", "▓", "▒", "░"}
|
|
||||||
var bar strings.Builder
|
|
||||||
|
|
||||||
for i := 0; i < width; i++ {
|
|
||||||
if i < filled {
|
|
||||||
// Gradient effect
|
|
||||||
charIdx := int(float64(i) / float64(width) * float64(len(fullChars)))
|
|
||||||
if charIdx >= len(fullChars) {
|
|
||||||
charIdx = len(fullChars) - 1
|
|
||||||
}
|
|
||||||
bar.WriteString(colorize(s.theme.ProgressFull, fullChars[charIdx]))
|
|
||||||
} else {
|
|
||||||
bar.WriteString(colorize(s.theme.ProgressEmpty, "░"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("[%s]", bar.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StatusBar) buildStatusLine(spinner, bar string, percent float64, message, speed, eta string, elapsed time.Duration) string {
|
|
||||||
percentStr := fmt.Sprintf("%.0f%%", percent*100)
|
|
||||||
|
|
||||||
// Truncate message if too long
|
|
||||||
maxMsgLen := 30
|
|
||||||
if len(message) > maxMsgLen {
|
|
||||||
message = message[:maxMsgLen-3] + "..."
|
|
||||||
}
|
|
||||||
|
|
||||||
var parts []string
|
|
||||||
|
|
||||||
// Main info
|
|
||||||
parts = append(parts, colorize(s.theme.Spinner, spinner))
|
|
||||||
parts = append(parts, colorize(s.theme.Accent, s.pkgName))
|
|
||||||
parts = append(parts, bar)
|
|
||||||
parts = append(parts, colorize(s.theme.Text, percentStr))
|
|
||||||
parts = append(parts, colorize(s.theme.Info, "| "+message))
|
|
||||||
|
|
||||||
// Speed and ETA if available
|
|
||||||
if speed != "" && eta != "" {
|
|
||||||
parts = append(parts, colorize(s.theme.Warning, speed))
|
|
||||||
parts = append(parts, colorize(s.theme.Info, eta))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always show elapsed time
|
|
||||||
parts = append(parts, colorize(s.theme.Text, formatDuration(elapsed)))
|
|
||||||
|
|
||||||
return strings.Join(parts, " ")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StatusBar) PrintHeader(title string) {
|
|
||||||
if !s.enabled {
|
|
||||||
fmt.Printf("=== %s ===\n", title)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
border := colorize(s.theme.Accent, "╔")
|
|
||||||
titleColored := colorize(s.theme.Text, fmt.Sprintf(" %s ", title))
|
|
||||||
borderEnd := colorize(s.theme.Accent, "╗")
|
|
||||||
|
|
||||||
fmt.Printf("%s%s%s\n", border, titleColored, borderEnd)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StatusBar) PrintFooter() {
|
|
||||||
if !s.enabled {
|
|
||||||
fmt.Printf("===================\n")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
border := colorize(s.theme.Accent, "╚")
|
|
||||||
borderEnd := colorize(s.theme.Accent, "╝")
|
|
||||||
content := colorize(s.theme.Text, strings.Repeat("═", 50))
|
|
||||||
|
|
||||||
fmt.Printf("%s%s%s\n", border, content, borderEnd)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StatusBar) PrintInfo(message string) {
|
|
||||||
if !s.enabled {
|
|
||||||
fmt.Printf("ℹ️ %s\n", message)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
icon := colorize(s.theme.Info, "ℹ️")
|
|
||||||
text := colorize(s.theme.Text, message)
|
|
||||||
fmt.Printf(" %s %s\n", icon, text)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StatusBar) PrintSuccess(message string) {
|
|
||||||
if !s.enabled {
|
|
||||||
fmt.Printf("✅ %s\n", message)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
icon := colorize(s.theme.Success, "✅")
|
|
||||||
text := colorize(s.theme.Text, message)
|
|
||||||
fmt.Printf(" %s %s\n", icon, text)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StatusBar) PrintWarning(message string) {
|
|
||||||
if !s.enabled {
|
|
||||||
fmt.Printf("⚠️ %s\n", message)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
icon := colorize(s.theme.Warning, "⚠️")
|
|
||||||
text := colorize(s.theme.Text, message)
|
|
||||||
fmt.Printf(" %s %s\n", icon, text)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StatusBar) PrintError(message string) {
|
|
||||||
if !s.enabled {
|
|
||||||
fmt.Printf("❌ %s\n", message)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
icon := colorize(s.theme.Error, "❌")
|
|
||||||
text := colorize(s.theme.Text, message)
|
|
||||||
fmt.Printf(" %s %s\n", icon, text)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper functions
|
|
||||||
|
|
||||||
func colorize(color, text string) string {
|
|
||||||
if !supportsANSIAndTTY() || os.Getenv("NO_COLOR") != "" {
|
|
||||||
return text
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("\033[%sm%s\033[0m", color, text)
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatDuration(d time.Duration) string {
|
|
||||||
if d < time.Second {
|
|
||||||
return fmt.Sprintf("%dms", d.Milliseconds())
|
|
||||||
}
|
|
||||||
if d < time.Minute {
|
|
||||||
return fmt.Sprintf("%.1fs", d.Seconds())
|
|
||||||
}
|
|
||||||
if d < time.Hour {
|
|
||||||
return fmt.Sprintf("%.0fm %.0fs",
|
|
||||||
d.Minutes(),
|
|
||||||
float64(int(d.Seconds())%60))
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%.0fh %.0fm",
|
|
||||||
d.Hours(),
|
|
||||||
float64(int(d.Minutes())%60))
|
|
||||||
}
|
|
||||||
|
|
||||||
func visibleLen(s string) int {
|
|
||||||
// Remove ANSI escape codes for length calculation
|
|
||||||
result := 0
|
|
||||||
inEscape := false
|
|
||||||
|
|
||||||
for _, r := range s {
|
|
||||||
if r == '\033' {
|
|
||||||
inEscape = true
|
|
||||||
} else if inEscape && r == 'm' {
|
|
||||||
inEscape = false
|
|
||||||
} else if !inEscape {
|
|
||||||
result++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func supportsANSIAndTTY() bool {
|
|
||||||
if os.Getenv("NO_COLOR") != "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
term := strings.TrimSpace(strings.ToLower(os.Getenv("TERM")))
|
|
||||||
if term == "" || term == "dumb" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
info, err := os.Stdout.Stat()
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return (info.Mode() & os.ModeCharDevice) != 0
|
|
||||||
}
|
|
||||||
BIN
zsvo
BIN
zsvo
Binary file not shown.
Loading…
Reference in a new issue