Skip to content

Add Windows support #11

Description

@torryt

Summary

Riff already builds for Windows via goreleaser and has a sysproc_windows.go build-tagged file, but the binary does not work properly on Windows due to several POSIX assumptions throughout the codebase. This issue tracks everything needed to make riff a fully functional Windows CLI.

Audit Results

Every .go file was evaluated. The codebase breaks down into three categories:

Already cross-platform (no changes needed)

Area Files Notes
Entry point / routing main.go Pure flag parsing, no OS-specific code
Colors internal/colors.go ANSI escapes work on Windows 10+ and Windows Terminal
TTY detection internal/prompt.go os.ModeCharDevice works on Windows
Project metadata internal/projects.go Uses filepath.Join, os.ReadDir, etc.
File copy / export cmd/export.go Uses filepath.Walk, os.Open, etc.
List / clean / config cmd/list.go, cmd/clean.go, cmd/config.go stdlib-only file I/O
Update docs cmd/update_docs.go No OS-specific code
LLM detection internal/describe.go exec.LookPath works cross-platform
Process detach internal/sysproc_windows.go Already handled with build tags
Data directory internal/config.go (InitPaths) os.UserHomeDir() + filepath.Join — correct on Windows

Needs changes

1. CRITICAL — sh -c for command execution

Files: cmd/new.go:116, cmd/new.go:129

cmd := exec.Command("sh", "-c", tmpl.Command)  // template commands
cmd := exec.Command("sh", "-c", runCmd)         // --run flag

Windows does not have sh. Both template execution and --run are completely broken.

Proposed fix: Create a build-tagged helper in internal/:

// shellcmd_unix.go
//go:build !windows

func ShellCommand(command string) *exec.Cmd {
    return exec.Command("sh", "-c", command)
}
// shellcmd_windows.go
//go:build windows

func ShellCommand(command string) *exec.Cmd {
    return exec.Command("cmd", "/C", command)
}

Then replace both call sites in cmd/new.go with internal.ShellCommand(...).


2. CRITICAL — riff init has no PowerShell/cmd support

File: cmd/init.go

The command only emits shell wrappers for bash, zsh, and fish. On Windows the primary shells are PowerShell (pwsh/powershell) and cmd.exe. Without a wrapper, the auto-cd-after-create feature is broken on Windows.

Proposed fix: Add a PowerShell wrapper:

function riff {
    $env:RIFF_WRAPPER = "1"
    & riff.exe @args
    $cdPath = Join-Path $env:USERPROFILE ".riff\.cd-path"
    if (Test-Path $cdPath) {
        $target = Get-Content $cdPath -Raw
        Remove-Item $cdPath
        if ($target -and (Test-Path $target)) {
            Set-Location $target.Trim()
        }
    }
}

And a cmd.exe wrapper (batch):

@echo off
set RIFF_WRAPPER=1
riff.exe %*
set "CD_PATH=%USERPROFILE%\.riff\.cd-path"
if exist "%CD_PATH%" (
    set /p TARGET=<"%CD_PATH%"
    del "%CD_PATH%"
    if defined TARGET cd /d "%TARGET%"
)

Update the switch in RunInit to handle "powershell", "pwsh", and "cmd".


3. CRITICAL — DetectShell() returns "" on Windows

File: internal/config.go:100-107

func DetectShell() string {
    shell := filepath.Base(os.Getenv("SHELL"))

Windows does not set the SHELL environment variable. This always returns "" on Windows, which causes EnsureShellWrapper to fall through to a generic message rather than auto-configuring.

Proposed fix: Add Windows-specific detection. Check for:

  • PSModulePath env var → PowerShell
  • COMSPEC env var → cmd.exe
  • Or inspect the parent process name

Example:

func DetectShell() string {
    // Unix: check SHELL
    shell := filepath.Base(os.Getenv("SHELL"))
    switch shell {
    case "bash", "zsh", "fish":
        return shell
    }

    // Windows: check for PowerShell, fall back to cmd
    if runtime.GOOS == "windows" {
        if os.Getenv("PSModulePath") != "" {
            return "powershell"
        }
        return "cmd"
    }

    return ""
}

4. CRITICAL — EnsureShellWrapper() only handles Unix config files

File: internal/config.go:111-178

This function writes eval "$(riff init)" to ~/.bashrc, ~/.zshrc, or ~/.config/fish/config.fish. None of these exist on Windows.

Proposed fix: Add cases for PowerShell profile paths:

  • $HOME\Documents\PowerShell\Microsoft.PowerShell_profile.ps1 (PowerShell 7+)
  • $HOME\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1 (Windows PowerShell 5.x)

The init line for PowerShell would be: . (riff init powershell) or Invoke-Expression (riff init powershell).

For cmd.exe, auto-injection isn't practical (AutoRun registry key is invasive), so print manual instructions instead.


5. MODERATE — Hardcoded / path separator in display string

File: internal/config.go:149

relPath := "~/" + strings.TrimPrefix(configFile, home+"/")

On Windows, home uses \ separators, so TrimPrefix with home+"/" will never match and the display path will be wrong.

Proposed fix:

relPath := "~" + string(filepath.Separator) + strings.TrimPrefix(configFile, home+string(filepath.Separator))

6. LOW RISK — Git post-commit hook is a POSIX shell script

File: cmd/new.go:145-156

hookContent := fmt.Sprintf("#!/bin/sh\n%s _update-single \"$(git rev-parse --show-toplevel)\" &\n", riffBin)

Git for Windows ships with MSYS2/MinGW and executes hooks through its own sh, so this likely works as-is. However, the & for backgrounding could behave unexpectedly.

Recommendation: Verify this works with Git for Windows. If not, conditionally write a .bat-style hook or use start /b for backgrounding.


7. LOW — CI lacks a Windows test runner

File: .github/workflows/ci.yml

CI only runs on ubuntu-latest. There are no Windows-specific tests to catch regressions.

Proposed fix: Add a matrix strategy:

strategy:
  matrix:
    os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}

Note: the test report step uses grep and shell constructs that would need adjustment on Windows, or could be skipped via if: runner.os != 'Windows'.


Implementation Checklist

  • Add internal/shellcmd_unix.go and internal/shellcmd_windows.go (build-tagged shell exec helper)
  • Update cmd/new.go to use internal.ShellCommand() instead of exec.Command("sh", "-c", ...)
  • Add PowerShell wrapper string to cmd/init.go
  • Add cmd.exe wrapper string to cmd/init.go
  • Update RunInit switch to handle powershell, pwsh, cmd
  • Update DetectShell() to detect PowerShell and cmd.exe on Windows
  • Update EnsureShellWrapper() with PowerShell profile path support
  • Fix hardcoded / separator in EnsureShellWrapper() display string
  • Verify git post-commit hook works with Git for Windows
  • Add windows-latest to CI matrix
  • Manual smoke test on Windows: riff new, riff new bun, riff new --run "echo hello", riff init powershell, riff list, riff open, riff clean, riff export

Labels

enhancement

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions