Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,22 @@ jobs:
run: make cover
- name: Vet
run: make lint

test-windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: '1.25.8'
cache: true
- name: Build
run: go build ./cmd/san/
- name: Test (cmd/san only — integration tests are Unix-only)
run: go test -count=1 ./cmd/san/...
- name: Smoke test — zip package
run: |
go build -o san.exe ./cmd/san/
Compress-Archive -Path san.exe -DestinationPath san_windows_amd64.zip -Force
Expand-Archive -Path san_windows_amd64.zip -DestinationPath extracted -Force
.\extracted\san.exe version
40 changes: 40 additions & 0 deletions cmd/san/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,17 @@ func init() {
rootCmd.AddCommand(helpCmd)
rootCmd.SetHelpCommand(helpCmd)
rootCmd.AddCommand(mcpCmd)
rootCmd.AddCommand(updateCmd)
}

func main() {
defer func() { _ = log.Sync() }()

// Clean up any stale backup file from a previous self-update.
// On Windows, os.Remove on a running executable's renamed backup
// fails, so we clean it on the next launch instead.
cleanupUpdateBackup()

if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
Expand Down Expand Up @@ -116,6 +122,20 @@ Non-interactive mode:
},
}

// cleanupUpdateBackup removes any stale .bak file from a previous self-update.
// On Windows, the running process cannot delete the renamed backup of itself,
// so we defer cleanup to the next launch.
func cleanupUpdateBackup() {
exe, err := os.Executable()
if err != nil {
return
}
backupPath := exe + ".bak"
if _, err := os.Stat(backupPath); err == nil {
_ = os.Remove(backupPath)
}
}

// readStdin returns piped stdin data, or empty string if stdin is a terminal.
func readStdin() string {
stat, _ := os.Stdin.Stat()
Expand All @@ -137,6 +157,25 @@ var versionCmd = &cobra.Command{
},
}

var updateCmd = &cobra.Command{
Use: "update",
Short: "Check for san updates and install if available",
Long: `Check for available san version updates and install the latest version.

Checks the latest release on GitHub and upgrades the san binary if a newer
version is available.

The current installed version is read from the binary itself. If a newer
release is found, the binary is automatically downloaded and replaced.

Example:
san update`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return runSelfUpdate(cmd.Context())
},
}

var helpCmd = &cobra.Command{
Use: "help",
Short: "Show help information",
Expand Down Expand Up @@ -174,6 +213,7 @@ Session:
Commands:
version Print the version number
agent run Run a headless agent
update Check for san updates and install if available
help Show this help message

Keybindings:
Expand Down
Loading