feat(cli): add update subcommand for in-place self-update

This commit is contained in:
2026-05-23 12:42:44 +01:00
parent 00267850ce
commit 941b1d86c3
3 changed files with 94 additions and 0 deletions
+54
View File
@@ -0,0 +1,54 @@
package cli
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"github.com/spf13/cobra"
)
const installURL = "https://raw.githubusercontent.com/lukaszraczylo/harness-sync/main/install.sh"
// NewUpdate returns the `update` subcommand. It downloads and installs the
// latest harness-sync release into the same directory the running binary
// lives in.
func NewUpdate() *cobra.Command {
var (
dryRun bool
dir string
)
cmd := &cobra.Command{
Use: "update",
Short: "Download and install the latest harness-sync release in place",
RunE: func(cmd *cobra.Command, _ []string) error {
if dir == "" {
exe, err := os.Executable()
if err != nil {
return fmt.Errorf("resolve current binary path: %w", err)
}
resolved, err := filepath.EvalSymlinks(exe)
if err == nil {
exe = resolved
}
dir = filepath.Dir(exe)
}
pipeline := fmt.Sprintf("curl -fsSL %q | INSTALL_DIR=%q bash", installURL, dir)
if _, err := fmt.Fprintf(cmd.OutOrStdout(), "Installing latest harness-sync into %s\n", dir); err != nil {
return err
}
if dryRun {
_, err := fmt.Fprintln(cmd.OutOrStdout(), pipeline)
return err
}
sh := exec.Command("/bin/bash", "-c", pipeline)
sh.Stdout = cmd.OutOrStdout()
sh.Stderr = cmd.ErrOrStderr()
return sh.Run()
},
}
cmd.Flags().BoolVar(&dryRun, "dry-run", false, "print the install command without executing it")
cmd.Flags().StringVar(&dir, "install-dir", "", "override install directory (default: dir of running binary)")
return cmd
}
+39
View File
@@ -0,0 +1,39 @@
package cli
import (
"bytes"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestUpdateDryRunPrintsCommand(t *testing.T) {
cmd := NewUpdate()
var buf bytes.Buffer
cmd.SetOut(&buf)
cmd.SetErr(&buf)
cmd.SetArgs([]string{"--dry-run", "--install-dir", "/tmp/test-dir"})
require.NoError(t, cmd.Execute())
out := buf.String()
assert.Contains(t, out, "Installing latest harness-sync into /tmp/test-dir")
assert.Contains(t, out, "curl -fsSL")
assert.Contains(t, out, "install.sh")
assert.Contains(t, out, "INSTALL_DIR=")
// Should NOT have run anything
assert.False(t, strings.Contains(out, "[INFO]"), "dry run must not invoke install.sh")
}
func TestUpdateResolvesCurrentBinaryDirByDefault(t *testing.T) {
cmd := NewUpdate()
var buf bytes.Buffer
cmd.SetOut(&buf)
cmd.SetArgs([]string{"--dry-run"})
require.NoError(t, cmd.Execute())
// Should mention SOME absolute path (the test binary's directory)
out := buf.String()
assert.Contains(t, out, "Installing latest harness-sync into /")
}