From 6b60b430b996e645c1480f035aadb3a2c89bb31a Mon Sep 17 00:00:00 2001 From: Ben Date: Thu, 5 Feb 2026 09:50:39 +0800 Subject: [PATCH 1/4] fix(cmd): update command uses platform-specific temp file extensions - Use .ps1 extension on Windows for PowerShell -File flag compatibility - Use .sh extension on Unix-like systems - Add error context in runInstallScript for better diagnostics - Add TestDownloadToTempFileExtension to verify platform-appropriate extensions Fixes silent failure of 'kairo update' on Windows where PowerShell -File flag could not execute files without .ps1 extension. --- cmd/update.go | 19 ++++++++++++++++--- cmd/update_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/cmd/update.go b/cmd/update.go index c2d8e30..3d075e4 100644 --- a/cmd/update.go +++ b/cmd/update.go @@ -119,7 +119,11 @@ func downloadToTempFile(url string) (string, error) { return "", fmt.Errorf("download failed with status %d", resp.StatusCode) } - tempFile, err := os.CreateTemp("", "kairo-install-*") + ext := ".sh" + if runtime.GOOS == "windows" { + ext = ".ps1" + } + tempFile, err := os.CreateTemp("", "kairo-install-*"+ext) if err != nil { return "", fmt.Errorf("failed to create temp file: %w", err) } @@ -145,7 +149,12 @@ func runInstallScript(scriptPath string) error { pwshCmd := exec.Command("powershell", "-ExecutionPolicy", "Bypass", "-File", scriptPath) pwshCmd.Stdout = os.Stdout pwshCmd.Stderr = os.Stderr - return pwshCmd.Run() + + if err := pwshCmd.Run(); err != nil { + return fmt.Errorf("powershell execution failed: %w", err) + } + + return nil } if err := os.Chmod(scriptPath, 0755); err != nil { @@ -155,7 +164,11 @@ func runInstallScript(scriptPath string) error { shCmd := exec.Command("/bin/sh", scriptPath) shCmd.Stdout = os.Stdout shCmd.Stderr = os.Stderr - return shCmd.Run() + if err := shCmd.Run(); err != nil { + return fmt.Errorf("shell execution failed: %w", err) + } + + return nil } var updateCmd = &cobra.Command{ diff --git a/cmd/update_test.go b/cmd/update_test.go index b4269c9..95d93c4 100644 --- a/cmd/update_test.go +++ b/cmd/update_test.go @@ -4,6 +4,8 @@ import ( "net/http" "net/http/httptest" "os" + "runtime" + "strings" "testing" "github.com/dkmnx/kairo/internal/version" @@ -672,3 +674,29 @@ func TestDownloadToTempFileErrorHandling(t *testing.T) { } }) } + +func TestDownloadToTempFileExtension(t *testing.T) { + t.Run("uses platform-appropriate extension", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("test content")) + })) + defer server.Close() + + tempFile, err := downloadToTempFile(server.URL) + if err != nil { + t.Fatalf("downloadToTempFile() error = %v", err) + } + defer os.Remove(tempFile) + + expectedExt := ".sh" + if runtime.GOOS == "windows" { + expectedExt = ".ps1" + } + + if !strings.HasSuffix(tempFile, expectedExt) { + t.Errorf("temp file %q should have %s extension on %s", tempFile, expectedExt, runtime.GOOS) + } + }) +} From f299ed7405ead251eed22666fd4d7929d0072e88 Mon Sep 17 00:00:00 2001 From: Ben Date: Thu, 5 Feb 2026 09:57:58 +0800 Subject: [PATCH 2/4] test(cmd): fix wrapper script execution on Windows - Use 'powershell -ExecutionPolicy Bypass -File' instead of 'cmd /c' - PowerShell scripts (.ps1) cannot be executed by cmd.exe directly - Fix comment check from 'REM' to '#' for PowerShell syntax Fixes TestWrapperScriptExecution failure on Windows where wrapper script was opening in Notepad instead of executing. --- cmd/switch_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cmd/switch_test.go b/cmd/switch_test.go index 3846962..a3f640c 100644 --- a/cmd/switch_test.go +++ b/cmd/switch_test.go @@ -479,15 +479,15 @@ echo "ANTHROPIC_AUTH_TOKEN=$ANTHROPIC_AUTH_TOKEN" t.Fatalf("Failed to create token file: %v", err) } - wrapperPath, useCmdExe, err := wrapper.GenerateWrapperScript(authDir, tokenPath, childScriptPath, []string{}) + wrapperPath, _, err := wrapper.GenerateWrapperScript(authDir, tokenPath, childScriptPath, []string{}) if err != nil { t.Fatalf("GenerateWrapperScript() error = %v", err) } var out bytes.Buffer var cmd *exec.Cmd - if useCmdExe { - cmd = exec.Command("cmd", "/c", wrapperPath) + if isWindows { + cmd = exec.Command("powershell", "-ExecutionPolicy", "Bypass", "-File", wrapperPath) } else { cmd = exec.Command(wrapperPath) } @@ -517,8 +517,8 @@ echo "ANTHROPIC_AUTH_TOKEN=$ANTHROPIC_AUTH_TOKEN" wrapperStr := string(wrapperContent) if isWindows { - if !strings.Contains(wrapperStr, "REM Generated by kairo") { - t.Error("Wrapper script missing REM comment") + if !strings.Contains(wrapperStr, "# Generated by kairo") { + t.Error("Wrapper script missing PowerShell comment") } } else { if !strings.Contains(wrapperStr, "# Generated by kairo - DO NOT EDIT") { From 99f8aac75ece3f91b2cc323e7d108312489810bb Mon Sep 17 00:00:00 2001 From: Ben Date: Thu, 5 Feb 2026 10:07:10 +0800 Subject: [PATCH 3/4] fix: close audit logger to prevent file lock on Windows - Add defer logger.Close() to auditCmd, auditListCmd, auditExportCmd - Set logger.f = nil after closing to prevent reuse of closed file - Add defer logger.Close() to audit tests - Skip permission test on Windows (chmod behaves differently) Fixes file lock errors in audit tests on Windows where audit.log cannot be deleted after test execution because file was left open. --- cmd/audit.go | 3 +++ cmd/audit_helpers_test.go | 6 ++++++ cmd/audit_test.go | 9 +++++++-- internal/audit/audit.go | 4 +++- 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/cmd/audit.go b/cmd/audit.go index a198be8..9f34146 100644 --- a/cmd/audit.go +++ b/cmd/audit.go @@ -38,6 +38,7 @@ var auditCmd = &cobra.Command{ ui.PrintError(fmt.Sprintf("Failed to open audit log: %v", err)) return } + defer logger.Close() entries, err := logger.LoadEntries() if err != nil { @@ -88,6 +89,7 @@ var auditListCmd = &cobra.Command{ ui.PrintError(fmt.Sprintf("Failed to open audit log: %v", err)) return } + defer logger.Close() entries, err := logger.LoadEntries() if err != nil { @@ -136,6 +138,7 @@ var auditExportCmd = &cobra.Command{ if err != nil { return err } + defer logger.Close() entries, err := logger.LoadEntries() if err != nil { diff --git a/cmd/audit_helpers_test.go b/cmd/audit_helpers_test.go index 0dd619f..d119207 100644 --- a/cmd/audit_helpers_test.go +++ b/cmd/audit_helpers_test.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "runtime" "strings" "testing" @@ -123,6 +124,11 @@ func TestLogAuditEvent_LoggerCreationFailure(t *testing.T) { }) t.Run("handles permission denied error", func(t *testing.T) { + // Skip this test on Windows as chmod behaves differently + if runtime.GOOS == "windows" { + t.Skip("chmod behaves differently on Windows, skipping permission test") + } + // Create a read-only directory tmpDir := t.TempDir() if err := os.Chmod(tmpDir, 0400); err != nil { diff --git a/cmd/audit_test.go b/cmd/audit_test.go index 53e0d4c..a4aba5f 100644 --- a/cmd/audit_test.go +++ b/cmd/audit_test.go @@ -63,6 +63,7 @@ func TestAuditCommandListWithEntries(t *testing.T) { if err != nil { t.Fatalf("NewLogger() error = %v", err) } + defer logger.Close() err = logger.LogSwitch("anthropic") if err != nil { @@ -107,6 +108,7 @@ func TestAuditCommandExportCSV(t *testing.T) { if err != nil { t.Fatalf("NewLogger() error = %v", err) } + defer logger.Close() err = logger.LogSwitch("anthropic") if err != nil { @@ -163,6 +165,7 @@ func TestAuditCommandExportJSON(t *testing.T) { if err != nil { t.Fatalf("NewLogger() error = %v", err) } + defer logger.Close() details := []audit.Change{ {Field: "api_key", New: "***"}, @@ -216,10 +219,11 @@ func TestAuditCommandInvalidFormat(t *testing.T) { } setConfigDir(tmpDir) - _, err := audit.NewLogger(tmpDir) + logger, err := audit.NewLogger(tmpDir) if err != nil { t.Fatalf("NewLogger() error = %v", err) } + defer logger.Close() tmpOutput := filepath.Join(t.TempDir(), "audit.xml") buf := new(bytes.Buffer) @@ -243,10 +247,11 @@ func TestAuditCommandMissingOutput(t *testing.T) { } setConfigDir(tmpDir) - _, err := audit.NewLogger(tmpDir) + logger, err := audit.NewLogger(tmpDir) if err != nil { t.Fatalf("NewLogger() error = %v", err) } + defer logger.Close() buf := new(bytes.Buffer) rootCmd.SetOut(buf) diff --git a/internal/audit/audit.go b/internal/audit/audit.go index 1e0235f..c5aa139 100644 --- a/internal/audit/audit.go +++ b/internal/audit/audit.go @@ -45,7 +45,9 @@ func (l *Logger) Close() error { l.mu.Lock() defer l.mu.Unlock() if l.f != nil { - return l.f.Close() + err := l.f.Close() + l.f = nil + return err } return nil } From 8383387835e89cf9e684570f7b4385b05a7b047e Mon Sep 17 00:00:00 2001 From: Ben Date: Fri, 6 Feb 2026 14:20:53 +0800 Subject: [PATCH 4/4] chore: update Go to 1.25.7 and add Windows-compatible pre-commit - ci: update Go version to 1.25.7 in all workflows (fixes GO-2026-4337) - pre-commit: use cmd /c for Windows-native hook execution - task: add pre-commit and pre-commit-install tasks to Taskfile.yml --- .github/workflows/ci.yml | 6 +++--- .github/workflows/release.yml | 2 +- .github/workflows/vulnerability-scan.yml | 2 +- .pre-commit-config.yaml | 4 ++-- Taskfile.yml | 12 ++++++++++++ 5 files changed, 19 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ea92fa1..2f4be52 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ permissions: checks: write env: - GO_VERSION: "1.25.5" + GO_VERSION: "1.25.7" GOLANGCI_LINT_VERSION: v1.62.0 jobs: @@ -81,9 +81,9 @@ jobs: strategy: fail-fast: false matrix: - go-version: ["1.25.6"] + go-version: ["1.25.7"] include: - - go-version: "1.25.6" + - go-version: "1.25.7" latest: true steps: - name: Checkout code diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b58a8bb..ed46187 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,7 +14,7 @@ permissions: id-token: write env: - GO_VERSION: '1.25.5' + GO_VERSION: '1.25.7' GORELEASER_VERSION: 'v2' jobs: diff --git a/.github/workflows/vulnerability-scan.yml b/.github/workflows/vulnerability-scan.yml index 24108b7..9fa13ec 100644 --- a/.github/workflows/vulnerability-scan.yml +++ b/.github/workflows/vulnerability-scan.yml @@ -16,7 +16,7 @@ permissions: actions: read env: - GO_VERSION: "1.25.6" + GO_VERSION: "1.25.7" jobs: vulncheck: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c87983e..c9e1cba 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,14 +25,14 @@ repos: - id: go-mod-tidy name: Check go.mod tidy - entry: bash -c "go mod tidy && git diff --exit-code go.mod go.sum" + entry: cmd /c "go mod tidy && git diff --exit-code go.mod go.sum" language: system files: go\.(mod|sum)$ pass_filenames: false - id: govulncheck name: Run vulnerability scan - entry: bash -c "command -v govulncheck >/dev/null 2>&1 || go install golang.org/x/vuln/cmd/govulncheck@latest && govulncheck ./..." + entry: cmd /c "where govulncheck >nul 2>&1 || go install golang.org/x/vuln/cmd/govulncheck@latest && govulncheck ./..." language: system files: go\.(mod|sum)$ pass_filenames: false diff --git a/Taskfile.yml b/Taskfile.yml index 381eecd..1b6e181 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -111,3 +111,15 @@ tasks: cmds: - echo "Running goreleaser (dry-run, no publish)..." - goreleaser release --clean --snapshot --skip=publish + + pre-commit: + desc: Run pre-commit hooks + cmds: + - python -m pre_commit run --all-files + - pre-commit run --all-files + + pre-commit-install: + desc: Install pre-commit git hooks + cmds: + - python -m pre_commit install + - pre-commit install