From 3179385113ea2c2c0df0e49375531e9acf4519ad Mon Sep 17 00:00:00 2001 From: lei Date: Tue, 7 Apr 2026 14:48:08 +0300 Subject: [PATCH 1/5] feat: add VerdaBinDir helper for ~/.verda/bin location Co-Authored-By: Claude Opus 4.6 (1M context) --- internal/verda-cli/options/paths.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/internal/verda-cli/options/paths.go b/internal/verda-cli/options/paths.go index f126605..7e13919 100644 --- a/internal/verda-cli/options/paths.go +++ b/internal/verda-cli/options/paths.go @@ -42,6 +42,27 @@ func EnsureVerdaDir() (string, error) { return dir, nil } +// VerdaBinDir returns the path to the Verda binary directory (~/.verda/bin). +func VerdaBinDir() (string, error) { + dir, err := VerdaDir() + if err != nil { + return "", err + } + return filepath.Join(dir, "bin"), nil +} + +// EnsureVerdaBinDir creates the Verda binary directory if it doesn't exist. +func EnsureVerdaBinDir() (string, error) { + dir, err := VerdaBinDir() + if err != nil { + return "", err + } + if err := mkdirSecure(dir); err != nil { + return "", fmt.Errorf("cannot create binary directory %s: %w", dir, err) + } + return dir, nil +} + // WriteSecureFile writes data to path with restrictive permissions. // On Unix, the file is created with 0600. On Windows, it inherits the // parent directory ACL (Go's os.WriteFile uses default security). From 0c26ae7aaab619367f2145f8c5e5434fa91c0d8f Mon Sep 17 00:00:00 2001 From: lei Date: Tue, 7 Apr 2026 14:49:56 +0300 Subject: [PATCH 2/5] feat: update command installs to ~/.verda/bin, warns about old binary Co-Authored-By: Claude Opus 4.6 (1M context) --- internal/verda-cli/cmd/update/update.go | 38 ++++++++++++++++++------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/internal/verda-cli/cmd/update/update.go b/internal/verda-cli/cmd/update/update.go index 9d233df..b875f32 100644 --- a/internal/verda-cli/cmd/update/update.go +++ b/internal/verda-cli/cmd/update/update.go @@ -21,6 +21,7 @@ import ( "github.com/verda-cloud/verdagostack/pkg/version" cmdutil "github/verda-cloud/verda-cli/internal/verda-cli/cmd/util" + "github/verda-cloud/verda-cli/internal/verda-cli/options" ) const ( @@ -78,6 +79,9 @@ func runList(ctx context.Context, ioStreams cmdutil.IOStreams) error { } current := version.Get().GitVersion + if !strings.HasPrefix(current, "v") { + current = "v" + current + } _, _ = fmt.Fprintf(ioStreams.Out, " Available versions (current: %s)\n\n", current) for _, v := range versions { marker := " " @@ -91,6 +95,9 @@ func runList(ctx context.Context, ioStreams cmdutil.IOStreams) error { func runUpdate(ctx context.Context, f cmdutil.Factory, ioStreams cmdutil.IOStreams, targetVersion string) error { current := version.Get().GitVersion + if !strings.HasPrefix(current, "v") { + current = "v" + current + } // Resolve target version. target := targetVersion @@ -132,24 +139,33 @@ func runUpdate(ctx context.Context, f cmdutil.Factory, ioStreams cmdutil.IOStrea return err } - // Replace current executable. - exe, err := resolveExecutable() + // Determine install destination: always ~/.verda/bin/verda. + binDir, err := options.EnsureVerdaBinDir() if err != nil { - return fmt.Errorf("resolving executable path: %w", err) + return fmt.Errorf("preparing install directory: %w", err) + } + binaryName := "verda" + if runtime.GOOS == osWindows { + binaryName = "verda.exe" } + dst := filepath.Join(binDir, binaryName) - if err := replaceBinary(exe, binary); err != nil { - if errors.Is(err, os.ErrPermission) { - hint := "sudo verda update" - if runtime.GOOS == osWindows { - hint = "running the command in an elevated (Administrator) terminal" - } - return fmt.Errorf("permission denied writing to %s\n\nTry: %s", filepath.Dir(exe), hint) - } + if err := replaceBinary(dst, binary); err != nil { return fmt.Errorf("replacing binary: %w", err) } _, _ = fmt.Fprintf(ioStreams.Out, "Updated to %s\n", target) + + // Warn if old binary exists in a system path. + oldExe, _ := resolveExecutable() + if oldExe != "" && oldExe != dst { + _, _ = fmt.Fprintf(ioStreams.ErrOut, + "\nNote: old binary still exists at %s\n"+ + " Remove it to avoid conflicts: sudo rm %s\n"+ + " Ensure %s is in your PATH.\n", + oldExe, oldExe, binDir) + } + return nil } From 545b80d00c86902ae9b20506d8ea7d8b4ea6c2cc Mon Sep 17 00:00:00 2001 From: lei Date: Tue, 7 Apr 2026 14:51:34 +0300 Subject: [PATCH 3/5] docs: update help text for new install location Co-Authored-By: Claude Opus 4.6 (1M context) --- internal/verda-cli/cmd/update/update.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/verda-cli/cmd/update/update.go b/internal/verda-cli/cmd/update/update.go index b875f32..4351e0c 100644 --- a/internal/verda-cli/cmd/update/update.go +++ b/internal/verda-cli/cmd/update/update.go @@ -43,6 +43,8 @@ func NewCmdUpdate(f cmdutil.Factory, ioStreams cmdutil.IOStreams) *cobra.Command Update the Verda CLI binary in-place by downloading from GitHub Releases. No Go installation required. + The binary is installed to ~/.verda/bin/ (no sudo required). + Without flags, updates to the latest version. Use --version to install a specific version (upgrade or downgrade). Use --list to show available versions. From 86beb17cd125ca4f7a9ed67150a6213e968b4768 Mon Sep 17 00:00:00 2001 From: lei Date: Tue, 7 Apr 2026 14:51:51 +0300 Subject: [PATCH 4/5] feat: install script defaults to ~/.verda/bin with PATH setup Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/install.sh | 62 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 53 insertions(+), 9 deletions(-) diff --git a/scripts/install.sh b/scripts/install.sh index 27d517c..e5b3127 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -3,14 +3,14 @@ # Usage: curl -sSL https://raw.githubusercontent.com/verda-cloud/verda-cli/main/scripts/install.sh | sh # # Environment variables: -# VERDA_INSTALL_DIR - Installation directory (default: /usr/local/bin) +# VERDA_INSTALL_DIR - Installation directory (default: ~/.verda/bin) # VERDA_VERSION - Specific version to install (default: latest) set -e REPO="verda-cloud/verda-cli" BINARY="verda" -INSTALL_DIR="${VERDA_INSTALL_DIR:-/usr/local/bin}" +INSTALL_DIR="${VERDA_INSTALL_DIR:-$HOME/.verda/bin}" # Detect OS OS="$(uname -s)" @@ -84,15 +84,51 @@ elif [ "$EXT" = "zip" ]; then fi # Install -if [ -w "$INSTALL_DIR" ]; then - mv "$BINARY" "$INSTALL_DIR/$BINARY" -else - echo "Elevating permissions to install to ${INSTALL_DIR}..." - sudo mv "$BINARY" "$INSTALL_DIR/$BINARY" -fi - +mkdir -p "$INSTALL_DIR" +mv "$BINARY" "$INSTALL_DIR/$BINARY" chmod +x "$INSTALL_DIR/$BINARY" +# Ensure ~/.verda/bin is in PATH +setup_path() { + case ":$PATH:" in + *":$INSTALL_DIR:"*) return ;; # already in PATH + esac + + SHELL_NAME="$(basename "$SHELL")" + case "$SHELL_NAME" in + zsh) RC_FILE="$HOME/.zshrc" ;; + bash) + if [ -f "$HOME/.bashrc" ]; then + RC_FILE="$HOME/.bashrc" + else + RC_FILE="$HOME/.bash_profile" + fi + ;; + fish) RC_FILE="$HOME/.config/fish/config.fish" ;; + *) RC_FILE="$HOME/.profile" ;; + esac + + PATH_LINE="export PATH=\"$INSTALL_DIR:\$PATH\"" + if [ "$SHELL_NAME" = "fish" ]; then + PATH_LINE="set -gx PATH $INSTALL_DIR \$PATH" + fi + + if [ -f "$RC_FILE" ] && grep -qF "$INSTALL_DIR" "$RC_FILE" 2>/dev/null; then + return # already configured + fi + + echo "" >> "$RC_FILE" + echo "# Added by Verda CLI installer" >> "$RC_FILE" + echo "$PATH_LINE" >> "$RC_FILE" + echo " Added $INSTALL_DIR to PATH in $RC_FILE" + echo " Run: source $RC_FILE (or open a new terminal)" +} + +# Only set up PATH if using the default location +if [ "$VERDA_INSTALL_DIR" = "" ]; then + setup_path +fi + echo "" echo "Verda CLI ${VERDA_VERSION} installed successfully!" echo "" @@ -100,3 +136,11 @@ echo "Get started:" echo " verda auth login # Configure credentials" echo " verda vm list # List VM instances" echo " verda --help # See all commands" + +# Warn about old binary in system path +OLD_BINARY="$(command -v verda 2>/dev/null || true)" +if [ -n "$OLD_BINARY" ] && [ "$OLD_BINARY" != "$INSTALL_DIR/$BINARY" ]; then + echo "" + echo "Warning: an older verda binary exists at $OLD_BINARY" + echo " Remove it to avoid conflicts: sudo rm $OLD_BINARY" +fi From d028633f505c1e54a0f23d3183d26f3288ad110b Mon Sep 17 00:00:00 2001 From: lei Date: Tue, 7 Apr 2026 15:00:13 +0300 Subject: [PATCH 5/5] feat: add ID column and fix table layout for images, vm list, and volume list - Add ID as the first column in images, vm list, and volume list tables - Truncate long names/hostnames to 30 chars with "..." to prevent layout breakage - Use fixed-width UUID column (36 chars) for consistent alignment Co-Authored-By: Claude Opus 4.6 (1M context) --- internal/verda-cli/cmd/images/images.go | 8 ++++---- internal/verda-cli/cmd/vm/list.go | 22 +++++++++------------- internal/verda-cli/cmd/volume/list.go | 12 ++++++++---- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/internal/verda-cli/cmd/images/images.go b/internal/verda-cli/cmd/images/images.go index 570757b..b849066 100644 --- a/internal/verda-cli/cmd/images/images.go +++ b/internal/verda-cli/cmd/images/images.go @@ -106,16 +106,16 @@ func runImages(cmd *cobra.Command, f cmdutil.Factory, ioStreams cmdutil.IOStream } _, _ = fmt.Fprintf(ioStreams.Out, " %d image(s) found\n\n", len(images)) - _, _ = fmt.Fprintf(ioStreams.Out, " %-45s %-12s %s\n", "NAME", "CATEGORY", "DETAILS") - _, _ = fmt.Fprintf(ioStreams.Out, " %-45s %-12s %s\n", "----", "--------", "-------") + _, _ = fmt.Fprintf(ioStreams.Out, " %-38s %-45s %-12s %s\n", "ID", "NAME", "CATEGORY", "DETAILS") + _, _ = fmt.Fprintf(ioStreams.Out, " %-38s %-45s %-12s %s\n", "--", "----", "--------", "-------") for i := range images { details := strings.Join(images[i].Details, ", ") def := "" if images[i].IsDefault { def = " *" } - _, _ = fmt.Fprintf(ioStreams.Out, " %-45s %-12s %s%s\n", - images[i].Name, images[i].Category, details, def) + _, _ = fmt.Fprintf(ioStreams.Out, " %-38s %-45s %-12s %s%s\n", + images[i].ID, images[i].Name, images[i].Category, details, def) } return nil } diff --git a/internal/verda-cli/cmd/vm/list.go b/internal/verda-cli/cmd/vm/list.go index 5bc7084..2389ae3 100644 --- a/internal/verda-cli/cmd/vm/list.go +++ b/internal/verda-cli/cmd/vm/list.go @@ -126,25 +126,21 @@ func fetchInstanceVolumes(ctx context.Context, client *verda.Client, inst *verda } func printInstanceTable(ioStreams cmdutil.IOStreams, instances []verda.Instance) error { - // Find max hostname length for dynamic column width. - nameW := 20 - for i := range instances { - if len(instances[i].Hostname) > nameW { - nameW = len(instances[i].Hostname) - } - } - _, _ = fmt.Fprintf(ioStreams.Out, " %d instance(s) found\n\n", len(instances)) - _, _ = fmt.Fprintf(ioStreams.Out, " %-*s %-13s %-18s %-8s %s\n", nameW, "HOSTNAME", "STATUS", "TYPE", "LOCATION", "IP") - _, _ = fmt.Fprintf(ioStreams.Out, " %-*s %-13s %-18s %-8s %s\n", nameW, "--------", "------", "----", "--------", "--") + _, _ = fmt.Fprintf(ioStreams.Out, " %-36s %-30s %-13s %-18s %-8s %s\n", "ID", "HOSTNAME", "STATUS", "TYPE", "LOCATION", "IP") + _, _ = fmt.Fprintf(ioStreams.Out, " %-36s %-30s %-13s %-18s %-8s %s\n", "--", "--------", "------", "----", "--------", "--") for i := range instances { ip := "" if instances[i].IP != nil && *instances[i].IP != "" { ip = *instances[i].IP } - _, _ = fmt.Fprintf(ioStreams.Out, " %-*s %-13s %-18s %-8s %s\n", - nameW, - instances[i].Hostname, + hostname := instances[i].Hostname + if len(hostname) > 30 { + hostname = hostname[:27] + "..." + } + _, _ = fmt.Fprintf(ioStreams.Out, " %-36s %-30s %-13s %-18s %-8s %s\n", + instances[i].ID, + hostname, instances[i].Status, instances[i].InstanceType, instances[i].Location, diff --git a/internal/verda-cli/cmd/volume/list.go b/internal/verda-cli/cmd/volume/list.go index 5cae727..92ccc65 100644 --- a/internal/verda-cli/cmd/volume/list.go +++ b/internal/verda-cli/cmd/volume/list.go @@ -82,11 +82,15 @@ func runList(cmd *cobra.Command, f cmdutil.Factory, ioStreams cmdutil.IOStreams, } _, _ = fmt.Fprintf(ioStreams.Out, " %d volume(s) found\n\n", len(volumes)) - _, _ = fmt.Fprintf(ioStreams.Out, " %-20s %-36s %6s %-10s %-10s %s\n", "NAME", "ID", "SIZE", "TYPE", "STATUS", "LOCATION") - _, _ = fmt.Fprintf(ioStreams.Out, " %-20s %-36s %6s %-10s %-10s %s\n", "----", "--", "----", "----", "------", "--------") + _, _ = fmt.Fprintf(ioStreams.Out, " %-36s %-30s %6s %-10s %-10s %s\n", "ID", "NAME", "SIZE", "TYPE", "STATUS", "LOCATION") + _, _ = fmt.Fprintf(ioStreams.Out, " %-36s %-30s %6s %-10s %-10s %s\n", "--", "----", "----", "----", "------", "--------") for i := range volumes { - _, _ = fmt.Fprintf(ioStreams.Out, " %-20s %-36s %4dGB %-10s %-10s %s\n", - volumes[i].Name, volumes[i].ID, volumes[i].Size, volumes[i].Type, volumes[i].Status, volumes[i].Location) + name := volumes[i].Name + if len(name) > 30 { + name = name[:27] + "..." + } + _, _ = fmt.Fprintf(ioStreams.Out, " %-36s %-30s %4dGB %-10s %-10s %s\n", + volumes[i].ID, name, volumes[i].Size, volumes[i].Type, volumes[i].Status, volumes[i].Location) } return nil }