feat: improve windows support (#225)
release / build-web (push) Has been cancelled
release / build-bundles (aarch64-linux-gnu-gcc, arm64, linux, ubuntu-latest, linux-arm64) (push) Has been cancelled
release / build-bundles (amd64, darwin, macos-13, macos-x64) (push) Has been cancelled
release / build-bundles (amd64, linux, ubuntu-latest, linux-x64) (push) Has been cancelled
release / build-bundles (amd64, windows, windows-latest, windows-x64) (push) Has been cancelled
release / build-bundles (arm64, darwin, macos-14, macos-arm64) (push) Has been cancelled

feat: windows support
This commit is contained in:
Carlos Florêncio
2026-02-15 18:16:21 +00:00
committed by GitHub
parent cf846606a5
commit cad2d783b3
26 changed files with 754 additions and 103 deletions
+28
View File
@@ -38,13 +38,30 @@ jobs:
include:
- os: ubuntu-latest
platform: linux-x64
goos: linux
goarch: amd64
- os: ubuntu-latest
platform: linux-arm64
goos: linux
goarch: arm64
cc: aarch64-linux-gnu-gcc
- os: macos-13
platform: macos-x64
goos: darwin
goarch: amd64
- os: macos-14
platform: macos-arm64
goos: darwin
goarch: arm64
- os: windows-latest
platform: windows-x64
goos: windows
goarch: amd64
runs-on: ${{ matrix.os }}
env:
CGO_ENABLED: 1
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v6
@@ -56,8 +73,16 @@ jobs:
name: web-bundle
path: dist/web
- name: Install cross-compilation tools (linux-arm64)
if: matrix.platform == 'linux-arm64'
run: |
sudo apt-get update
sudo apt-get install -y gcc-aarch64-linux-gnu
- name: Build backend binaries (unix)
if: runner.os != 'Windows'
env:
CC: ${{ matrix.cc || '' }}
run: |
cd apps/backend
mkdir -p bin
@@ -68,6 +93,9 @@ jobs:
if: runner.os == 'Windows'
shell: pwsh
run: |
# Ensure MSYS2 MinGW gcc is available for CGO
$env:Path = "C:\msys64\mingw64\bin;" + $env:Path
gcc --version
Set-Location apps/backend
New-Item -ItemType Directory -Force -Path bin | Out-Null
go build -o bin/kandev.exe ./cmd/kandev
+12 -3
View File
@@ -10,6 +10,15 @@ APPS_DIR := apps
PNPM := pnpm
MAKE := make
# Cross-platform commands
ifeq ($(OS),Windows_NT)
RM = cmd /c del /s /q
RMDIR = cmd /c rmdir /s /q
else
RM = rm -f
RMDIR = rm -rf
endif
# Colors for terminal output
RESET := \033[0m
BOLD := \033[1m
@@ -251,11 +260,11 @@ clean-backend:
.PHONY: clean-web
clean-web:
@printf "$(CYAN)Cleaning web artifacts...$(RESET)\n"
@rm -rf $(WEB_DIR)/.next $(APPS_DIR)/node_modules
@rm -rf $(APPS_DIR)/packages/*/node_modules
@$(RMDIR) $(WEB_DIR)/.next $(APPS_DIR)/node_modules
@$(RMDIR) $(APPS_DIR)/packages/*/node_modules
.PHONY: clean-db
clean-db:
@printf "$(CYAN)Removing local SQLite database...$(RESET)\n"
@rm -f kandev.db kandev.db-wal kandev.db-shm \
@$(RM) kandev.db kandev.db-wal kandev.db-shm \
$(BACKEND_DIR)/kandev.db $(BACKEND_DIR)/kandev.db-wal $(BACKEND_DIR)/kandev.db-shm
+17 -6
View File
@@ -7,10 +7,21 @@ GO := go
GOFLAGS := -v
LDFLAGS := -s -w
# Cross-platform commands
ifeq ($(OS),Windows_NT)
RM = cmd /c del /s /q
RMDIR = cmd /c rmdir /s /q
MKDIR = cmd /c mkdir
else
RM = rm -f
RMDIR = rm -rf
MKDIR = mkdir -p
endif
# Build info
VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
BUILD_TIME := $(shell date -u '+%Y-%m-%dT%H:%M:%SZ')
BUILD_TIME := $(shell date -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null || powershell -c "Get-Date -Format 'yyyy-MM-ddTHH:mm:ssZ'" 2>/dev/null || echo "unknown")
LDFLAGS += -X main.Version=$(VERSION) -X main.Commit=$(COMMIT) -X main.BuildTime=$(BUILD_TIME)
@@ -22,13 +33,13 @@ all: build
## Build the mock-agent binary for testing
build-mock-agent:
@echo "Building mock-agent..."
@mkdir -p $(BUILD_DIR)
@$(MKDIR) $(BUILD_DIR)
$(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/mock-agent ./cmd/mock-agent
## Build the unified kandev binary
build: build-agentctl build-mock-agent
@echo "Building $(BINARY_NAME)..."
@mkdir -p $(BUILD_DIR)
@$(MKDIR) $(BUILD_DIR)
$(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME) ./cmd/kandev
## Build all binaries (unified + individual services + agentctl)
@@ -39,13 +50,13 @@ build-all: build build-agentctl
## Build the agentctl sidecar binary (includes embedded MCP server)
build-agentctl:
@echo "Building agentctl..."
@mkdir -p $(BUILD_DIR)
@$(MKDIR) $(BUILD_DIR)
$(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/agentctl ./cmd/agentctl
## Clean build artifacts
clean:
@echo "Cleaning..."
@rm -rf $(BUILD_DIR)
@$(RMDIR) $(BUILD_DIR)
$(GO) clean
## Run the application
@@ -97,7 +108,7 @@ deps:
## Build for multiple platforms
build-cross:
@echo "Building for multiple platforms..."
@mkdir -p $(BUILD_DIR)
@$(MKDIR) $(BUILD_DIR)
GOOS=linux GOARCH=amd64 $(GO) build -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64 ./cmd/kandev
GOOS=darwin GOARCH=amd64 $(GO) build -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-darwin-amd64 ./cmd/kandev
GOOS=darwin GOARCH=arm64 $(GO) build -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-darwin-arm64 ./cmd/kandev
+1
View File
@@ -5,6 +5,7 @@ go 1.24.0
toolchain go1.24.11
require (
github.com/UserExistsError/conpty v0.1.4
github.com/coder/acp-go-sdk v0.6.3
github.com/creack/pty v1.1.21
github.com/docker/docker v28.5.2+incompatible
+3
View File
@@ -42,6 +42,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Microsoft/go-winio v0.4.21 h1:+6mVbXh4wPzUrl1COX9A+ZCvEpYsOBZ6/+kwDnvLyro=
github.com/Microsoft/go-winio v0.4.21/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
github.com/UserExistsError/conpty v0.1.4 h1:+3FhJhiqhyEJa+K5qaK3/w6w+sN3Nh9O9VbJyBS02to=
github.com/UserExistsError/conpty v0.1.4/go.mod h1:PDglKIkX3O/2xVk0MV9a6bCWxRmPVfxqZoTG/5sSd9I=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
@@ -479,6 +481,7 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -0,0 +1,75 @@
package process
import "strings"
// escapeArg rewrites a command-line argument following the MSDN
// CommandLineToArgvW parsing rules (same algorithm as Go's
// syscall.EscapeArg on Windows):
//
// - every backslash (\) is doubled, but only if immediately
// followed by a double quote (");
// - every double quote (") is escaped with a backslash (\);
// - finally, the argument is wrapped in double quotes only if
// it contains spaces or tabs.
// - an empty string becomes "" (two double-quote characters).
func escapeArg(s string) string {
if len(s) == 0 {
return `""`
}
needsBackslash := false
hasSpace := false
for i := 0; i < len(s); i++ {
switch s[i] {
case '"', '\\':
needsBackslash = true
case ' ', '\t':
hasSpace = true
}
}
if !needsBackslash && !hasSpace {
return s
}
if !needsBackslash {
return `"` + s + `"`
}
var b []byte
if hasSpace {
b = append(b, '"')
}
slashes := 0
for i := 0; i < len(s); i++ {
c := s[i]
switch c {
default:
slashes = 0
case '\\':
slashes++
case '"':
for ; slashes > 0; slashes-- {
b = append(b, '\\')
}
b = append(b, '\\')
}
b = append(b, c)
}
if hasSpace {
for ; slashes > 0; slashes-- {
b = append(b, '\\')
}
b = append(b, '"')
}
return string(b)
}
// buildCmdLine joins arguments into a single command-line string
// with proper quoting for Windows CreateProcess.
func buildCmdLine(args []string) string {
escaped := make([]string, len(args))
for i, arg := range args {
escaped[i] = escapeArg(arg)
}
return strings.Join(escaped, " ")
}
@@ -0,0 +1,52 @@
package process
import "testing"
func TestEscapeArg(t *testing.T) {
tests := []struct {
name string
in string
want string
}{
{"empty", "", `""`},
{"simple", "hello", "hello"},
{"with space", "hello world", `"hello world"`},
{"with tab", "hello\tworld", `"hello` + "\t" + `world"`},
{"with quote", `say "hi"`, `"say \"hi\""`},
{"backslash no quote", `a\b`, `a\b`},
{"backslash before quote", `a\"`, `a\\\"` },
{"trailing backslash with space", `a b\`, `"a b\\"`},
{"multiple trailing backslashes with space", `a b\\`, `"a b\\\\"`},
{"only backslashes", `\\`, `\\`},
{"backslash then quote", `\\"`, `\\\\\"` },
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := escapeArg(tt.in)
if got != tt.want {
t.Errorf("escapeArg(%q) = %q, want %q", tt.in, got, tt.want)
}
})
}
}
func TestBuildCmdLine(t *testing.T) {
tests := []struct {
name string
args []string
want string
}{
{"single", []string{"cmd.exe"}, "cmd.exe"},
{"simple args", []string{"git", "status"}, "git status"},
{"arg with space", []string{"C:\\Program Files\\app.exe", "-f"}, `"C:\Program Files\app.exe" -f`},
{"arg with quote", []string{"echo", `"hello"`}, `echo \"hello\"`},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := buildCmdLine(tt.args)
if got != tt.want {
t.Errorf("buildCmdLine(%v) = %q, want %q", tt.args, got, tt.want)
}
})
}
}
@@ -11,16 +11,13 @@ import (
"context"
"fmt"
"io"
"os"
"os/exec"
"regexp"
"sort"
"strings"
"sync"
"syscall"
"time"
"github.com/creack/pty"
"github.com/google/uuid"
"github.com/kandev/kandev/internal/agentctl/types"
"github.com/kandev/kandev/internal/common/logger"
@@ -70,7 +67,7 @@ type DirectOutputWriter interface {
type interactiveProcess struct {
info InteractiveProcessInfo
cmd *exec.Cmd
ptmx *os.File // PTY master file descriptor (using creack/pty)
ptmx PtyHandle // PTY handle (Unix: creack/pty, Windows: ConPTY)
buffer *ringBuffer
// Turn detection
@@ -330,12 +327,9 @@ func (r *InteractiveRunner) startProcess(proc *interactiveProcess, cols, rows in
// Note: Do NOT set Setpgid when using PTY - it conflicts with terminal control
// The PTY session handles process group management
// Start process in PTY with exact dimensions from frontend using creack/pty
// This is the battle-tested PTY library that properly handles resize/SIGWINCH
ptmx, err := pty.StartWithSize(cmd, &pty.Winsize{
Cols: uint16(cols),
Rows: uint16(rows),
})
// Start process in PTY with exact dimensions from frontend
// Unix: creack/pty, Windows: ConPTY
ptmx, err := startPTYWithSize(cmd, cols, rows)
if err != nil {
return fmt.Errorf("failed to start pty: %w", err)
}
@@ -473,7 +467,7 @@ func (r *InteractiveRunner) Stop(ctx context.Context, processID string) error {
// Terminate the process directly (PTY handles its own session management)
if proc.cmd != nil && proc.cmd.Process != nil {
_ = proc.cmd.Process.Signal(syscall.SIGTERM)
_ = terminateProcess(proc.cmd.Process)
// Wait for the wait() goroutine to finish (it calls cmd.Wait).
// If it doesn't exit in time, force-kill the process.
@@ -515,14 +509,18 @@ func (r *InteractiveRunner) GetBySession(sessionID string) (*InteractiveProcessI
}
// isProcessAlive checks if the underlying OS process is still running.
// Uses a non-blocking check on waitDone which is closed when cmd.Wait returns.
// Must be called with proc.mu held.
func (r *InteractiveRunner) isProcessAlive(proc *interactiveProcess) bool {
if proc.cmd != nil && proc.cmd.Process != nil {
// Signal 0 checks if process exists without sending a signal
err := proc.cmd.Process.Signal(syscall.Signal(0))
return err == nil
if proc.cmd == nil || proc.cmd.Process == nil {
return false
}
select {
case <-proc.waitDone:
return false
default:
return true
}
return false
}
// IsProcessRunning checks if a process with the given ID exists and is running.
@@ -605,13 +603,10 @@ func (r *InteractiveRunner) ResizeBySession(sessionID string, cols, rows uint16)
statusTracker := proc.statusTracker
proc.mu.Unlock()
// If process started, resize the PTY using creack/pty's Setsize
// This properly sets the window size and sends SIGWINCH to the process
// If process started, resize the PTY
// On Unix this sends SIGWINCH; on Windows it updates the ConPTY dimensions
if ptyInstance != nil {
if err := pty.Setsize(ptyInstance, &pty.Winsize{
Cols: cols,
Rows: rows,
}); err != nil {
if err := ptyInstance.Resize(cols, rows); err != nil {
return fmt.Errorf("failed to resize PTY: %w", err)
}
}
@@ -835,25 +830,14 @@ func (r *InteractiveRunner) handleStateChange(proc *interactiveProcess, state Ag
// 3. Adding a timeout here would leave the process unreachable and create leaks
func (r *InteractiveRunner) wait(proc *interactiveProcess) {
defer close(proc.waitDone)
err := proc.cmd.Wait()
exitCode := 0
proc.mu.Lock()
ptyHandle := proc.ptmx
proc.mu.Unlock()
exitCode, signalName, err := waitPtyProcess(proc.cmd, ptyHandle)
status := types.ProcessStatusExited
var signalName string
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
if waitStatus, ok := exitErr.Sys().(syscall.WaitStatus); ok {
if waitStatus.Signaled() {
signalName = waitStatus.Signal().String()
exitCode = 128 + int(waitStatus.Signal())
} else {
exitCode = waitStatus.ExitStatus()
}
} else {
exitCode = 1
}
} else {
exitCode = 1
}
status = types.ProcessStatusFailed
}
@@ -1354,18 +1338,9 @@ func (r *InteractiveRunner) StartUserShell(ctx context.Context, sessionID, termi
initialCommand = existingEntry.InitialCommand
}
// Determine shell to use
shell := preferredShell
if shell == "" {
shell = os.Getenv("SHELL")
}
if shell == "" {
shell = "/bin/sh"
}
req := InteractiveStartRequest{
SessionID: sessionID,
Command: []string{shell, "-l"}, // Login shell
Command: defaultShellCommand(preferredShell),
WorkingDir: workingDir,
InitialCommand: initialCommand,
DisableTurnDetection: true, // User shells must not trigger turn complete / MarkReady
@@ -1403,7 +1378,7 @@ func (r *InteractiveRunner) StartUserShell(ctx context.Context, sessionID, termi
zap.String("session_id", sessionID),
zap.String("terminal_id", terminalID),
zap.String("process_id", info.ID),
zap.String("shell", shell),
zap.String("shell", req.Command[0]),
zap.String("working_dir", workingDir),
zap.String("label", opts.Label),
zap.String("initial_command", opts.InitialCommand))
@@ -1544,10 +1519,7 @@ func (r *InteractiveRunner) ResizeUserShell(sessionID, terminalID string, cols,
proc.mu.Unlock()
if ptyInstance != nil {
if err := pty.Setsize(ptyInstance, &pty.Winsize{
Cols: cols,
Rows: rows,
}); err != nil {
if err := ptyInstance.Resize(cols, rows); err != nil {
return fmt.Errorf("failed to resize PTY: %w", err)
}
}
@@ -0,0 +1,35 @@
//go:build !windows
package process
import (
"os"
"os/exec"
"syscall"
)
// terminateProcess sends SIGTERM to the process for graceful shutdown.
func terminateProcess(p *os.Process) error {
return p.Signal(syscall.SIGTERM)
}
// waitPtyProcess waits for the PTY process to exit and returns exit info.
// On Unix, uses cmd.Wait() which inspects WaitStatus for signal information.
func waitPtyProcess(cmd *exec.Cmd, _ PtyHandle) (exitCode int, signalName string, err error) {
err = cmd.Wait()
if err == nil {
return 0, "", nil
}
exitErr, ok := err.(*exec.ExitError)
if !ok {
return 1, "", err
}
waitStatus, ok := exitErr.Sys().(syscall.WaitStatus)
if !ok {
return 1, "", err
}
if waitStatus.Signaled() {
return 128 + int(waitStatus.Signal()), waitStatus.Signal().String(), err
}
return waitStatus.ExitStatus(), "", err
}
@@ -0,0 +1,29 @@
//go:build windows
package process
import (
"os"
"os/exec"
)
// terminateProcess kills the process on Windows.
// Windows does not support SIGTERM; process termination is immediate.
func terminateProcess(p *os.Process) error {
return p.Kill()
}
// waitPtyProcess waits for the PTY process to exit and returns exit info.
// On Windows, uses cmd.Process.Wait() since the process may have been started
// via ConPTY rather than cmd.Start().
func waitPtyProcess(cmd *exec.Cmd, _ PtyHandle) (exitCode int, signalName string, err error) {
state, err := cmd.Process.Wait()
if err != nil {
return 1, "", err
}
code := state.ExitCode()
if code != 0 {
return code, "", &exec.ExitError{ProcessState: state}
}
return 0, "", nil
}
@@ -0,0 +1,12 @@
package process
import "io"
// PtyHandle abstracts PTY operations across Unix and Windows.
// On Unix, this wraps creack/pty (*os.File).
// On Windows, this wraps Windows ConPTY.
type PtyHandle interface {
io.ReadWriteCloser
// Resize changes the PTY window size.
Resize(cols, rows uint16) error
}
@@ -0,0 +1,36 @@
//go:build !windows
package process
import (
"os"
"os/exec"
"github.com/creack/pty"
)
// unixPTY wraps a Unix PTY master file descriptor.
type unixPTY struct {
f *os.File
}
func (p *unixPTY) Read(b []byte) (int, error) { return p.f.Read(b) }
func (p *unixPTY) Write(b []byte) (int, error) { return p.f.Write(b) }
func (p *unixPTY) Close() error { return p.f.Close() }
func (p *unixPTY) Resize(cols, rows uint16) error {
return pty.Setsize(p.f, &pty.Winsize{Cols: cols, Rows: rows})
}
// startPTYWithSize starts the command in a Unix PTY with the given dimensions.
// The command is started via pty.StartWithSize which calls cmd.Start() internally.
func startPTYWithSize(cmd *exec.Cmd, cols, rows int) (PtyHandle, error) {
f, err := pty.StartWithSize(cmd, &pty.Winsize{
Cols: uint16(cols),
Rows: uint16(rows),
})
if err != nil {
return nil, err
}
return &unixPTY{f: f}, nil
}
@@ -0,0 +1,64 @@
//go:build windows
package process
import (
"fmt"
"os"
"os/exec"
"github.com/UserExistsError/conpty"
)
// windowsPTY wraps a Windows ConPTY pseudo-console.
type windowsPTY struct {
cpty *conpty.ConPty
}
func (p *windowsPTY) Read(b []byte) (int, error) { return p.cpty.Read(b) }
func (p *windowsPTY) Write(b []byte) (int, error) { return p.cpty.Write(b) }
func (p *windowsPTY) Close() error { return p.cpty.Close() }
func (p *windowsPTY) Resize(cols, rows uint16) error {
return p.cpty.Resize(int(cols), int(rows))
}
// startPTYWithSize starts the command in a Windows ConPTY with the given dimensions.
// ConPTY manages process creation internally, so this builds a command line from
// the exec.Cmd and starts the process via ConPTY. After this call, cmd.Process
// is set so callers can manage the process lifecycle.
func startPTYWithSize(cmd *exec.Cmd, cols, rows int) (PtyHandle, error) {
cmdLine := buildCmdLine(cmd.Args)
if len(cmd.Args) == 0 {
cmdLine = escapeArg(cmd.Path)
}
opts := []conpty.ConPtyOption{
conpty.ConPtyDimensions(cols, rows),
}
if cmd.Dir != "" {
opts = append(opts, conpty.ConPtyWorkDir(cmd.Dir))
}
// Pass environment variables directly to the child process via ConPTY.
if cmd.Env != nil {
opts = append(opts, conpty.ConPtyEnv(cmd.Env))
}
cpty, err := conpty.Start(cmdLine, opts...)
if err != nil {
return nil, err
}
// Set cmd.Process so callers can use PID, Kill, Wait, etc.
pid := cpty.Pid()
proc, err := os.FindProcess(int(pid))
if err != nil {
_ = cpty.Close()
return nil, fmt.Errorf("failed to find ConPTY process %d: %w", pid, err)
}
cmd.Process = proc
return &windowsPTY{cpty: cpty}, nil
}
@@ -229,8 +229,9 @@ func (r *ProcessRunner) Start(ctx context.Context, req StartProcessRequest) (*Pr
id := uuid.New().String()
now := time.Now().UTC()
// Use "sh -lc" to execute command with login shell environment
cmd := exec.CommandContext(ctx, "sh", "-lc", req.Command)
// Execute command through the system shell (Unix: sh -lc, Windows: cmd /c)
prog, shellArgs := shellExecArgs(req.Command)
cmd := exec.CommandContext(ctx, prog, shellArgs...)
if req.WorkingDir != "" {
cmd.Dir = req.WorkingDir
}
@@ -0,0 +1,48 @@
package process
import (
"runtime"
"testing"
)
func TestDefaultShellCommand_CustomShell(t *testing.T) {
cmd := defaultShellCommand("/usr/local/bin/fish")
if cmd[0] != "/usr/local/bin/fish" {
t.Errorf("expected preferred shell as first element, got %q", cmd[0])
}
}
func TestDefaultShellCommand_EmptyPreferred(t *testing.T) {
cmd := defaultShellCommand("")
if len(cmd) == 0 {
t.Fatal("expected non-empty command")
}
// On Unix, should fall back to $SHELL or /bin/sh with -l flag
// On Windows, should fall back to %COMSPEC% or powershell.exe
if runtime.GOOS != "windows" {
if cmd[len(cmd)-1] != "-l" {
t.Errorf("expected login flag -l on Unix, got %v", cmd)
}
}
}
func TestShellExecArgs(t *testing.T) {
prog, args := shellExecArgs("echo hello")
if prog == "" {
t.Fatal("expected non-empty program")
}
if len(args) == 0 {
t.Fatal("expected non-empty args")
}
// The command string should appear in the args
found := false
for _, a := range args {
if a == "echo hello" {
found = true
break
}
}
if !found {
t.Errorf("expected command string in args, got prog=%q args=%v", prog, args)
}
}
@@ -0,0 +1,25 @@
//go:build !windows
package process
import "os"
// defaultShellCommand returns the command and args for starting an interactive login shell.
// On Unix, uses $SHELL (or /bin/sh) with the -l (login) flag.
func defaultShellCommand(preferredShell string) []string {
shell := preferredShell
if shell == "" {
shell = os.Getenv("SHELL")
}
if shell == "" {
shell = "/bin/sh"
}
return []string{shell, "-l"}
}
// shellExecArgs returns the program and arguments needed to execute a command
// string through the system shell.
// On Unix: sh -lc "command"
func shellExecArgs(command string) (prog string, args []string) {
return "sh", []string{"-lc", command}
}
@@ -0,0 +1,26 @@
//go:build windows
package process
import "os"
// defaultShellCommand returns the command and args for starting an interactive shell.
// On Windows, uses %COMSPEC% (typically cmd.exe) or falls back to powershell.exe.
// No -l flag since Windows shells don't use login shell mode.
func defaultShellCommand(preferredShell string) []string {
shell := preferredShell
if shell == "" {
shell = os.Getenv("COMSPEC")
}
if shell == "" {
shell = "powershell.exe"
}
return []string{shell}
}
// shellExecArgs returns the program and arguments needed to execute a command
// string through the system shell.
// On Windows: cmd /c "command"
func shellExecArgs(command string) (prog string, args []string) {
return "cmd", []string{"/c", command}
}
+29 -3
View File
@@ -5,6 +5,8 @@ package config
import (
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"time"
@@ -174,13 +176,13 @@ func setDefaults(v *viper.Viper) {
v.SetDefault("nats.clientId", "kandev-client")
v.SetDefault("nats.maxReconnects", 10)
// Docker defaults
// Docker defaults — platform-aware host and volume path
v.SetDefault("docker.enabled", true) // Docker runtime enabled by default if Docker is available
v.SetDefault("docker.host", "unix:///var/run/docker.sock")
v.SetDefault("docker.host", DefaultDockerHost())
v.SetDefault("docker.apiVersion", "1.41")
v.SetDefault("docker.tlsVerify", false)
v.SetDefault("docker.defaultNetwork", "kandev-network")
v.SetDefault("docker.volumeBasePath", "/var/lib/kandev/volumes")
v.SetDefault("docker.volumeBasePath", defaultDockerVolumePath())
// Agent defaults (runtime selection is now per-task based on executor type)
v.SetDefault("agent.standaloneHost", "localhost")
@@ -209,6 +211,30 @@ func setDefaults(v *viper.Viper) {
v.SetDefault("worktree.cleanupOnRemove", true)
}
// DefaultDockerHost returns the platform-appropriate Docker socket path.
// Respects DOCKER_HOST env var as override (standard Docker convention).
func DefaultDockerHost() string {
if host := os.Getenv("DOCKER_HOST"); host != "" {
return host
}
if runtime.GOOS == "windows" {
return "npipe:////./pipe/docker_engine"
}
return "unix:///var/run/docker.sock"
}
// defaultDockerVolumePath returns the platform-appropriate volume base path.
func defaultDockerVolumePath() string {
if runtime.GOOS == "windows" {
localAppData := os.Getenv("LOCALAPPDATA")
if localAppData == "" {
localAppData = filepath.Join(os.Getenv("USERPROFILE"), "AppData", "Local")
}
return filepath.Join(localAppData, "kandev", "volumes")
}
return "/var/lib/kandev/volumes"
}
// Load reads configuration from environment variables, config file, and defaults.
// Environment variables use the prefix KANDEV_ with snake_case naming.
// Config file should be named config.yaml and placed in the current directory or /etc/kandev/.
@@ -8,6 +8,7 @@ import (
"github.com/google/uuid"
"github.com/kandev/kandev/internal/common/config"
commonsqlite "github.com/kandev/kandev/internal/common/sqlite"
"github.com/kandev/kandev/internal/task/models"
)
@@ -137,7 +138,7 @@ func (r *Repository) ensureDefaultExecutorsAndEnvironments() error {
isSystem: false,
resumable: true,
config: map[string]string{
"docker_host": "unix:///var/run/docker.sock",
"docker_host": config.DefaultDockerHost(),
},
},
}
+2 -1
View File
@@ -141,5 +141,6 @@ scripts/release/publish-launcher.sh 0.1.0
| macOS (Apple Silicon) | `kandev-macos-arm64.zip` |
| macOS (Intel) | `kandev-macos-x64.zip` |
| Linux (x64) | `kandev-linux-x64.zip` |
| Linux (ARM64) | `kandev-linux-arm64.zip` |
| Windows (x64) | `kandev-windows-x64.zip` |
| Windows (ARM64) | `kandev-windows-arm64.zip` |
| Windows (ARM64) | Falls back to `windows-x64` (x64 emulation) |
+5 -3
View File
@@ -5,10 +5,10 @@ const isWindows = process.platform === "win32";
export type PlatformArch = "x64" | "arm64";
export type PlatformDir =
| "linux-x64"
| "linux-arm64"
| "macos-x64"
| "macos-arm64"
| "windows-x64"
| "windows-arm64";
| "windows-x64";
export function getBinaryName(base: string): string {
return isWindows ? `${base}.exe` : base;
@@ -40,9 +40,11 @@ export function getPlatformDir(): PlatformDir {
const platform = process.platform;
const arch = getEffectiveArch();
if (platform === "linux" && arch === "x64") return "linux-x64";
if (platform === "linux" && arch === "arm64") return "linux-arm64";
if (platform === "darwin" && arch === "x64") return "macos-x64";
if (platform === "darwin" && arch === "arm64") return "macos-arm64";
if (platform === "win32" && arch === "x64") return "windows-x64";
if (platform === "win32" && arch === "arm64") return "windows-arm64";
// Windows ARM64 runs x64 binaries via emulation — no native arm64 build yet
if (platform === "win32" && arch === "arm64") return "windows-x64";
throw new Error(`Unsupported platform: ${platform}-${arch}`);
}
+16 -8
View File
@@ -37,17 +37,23 @@ export function createProcessSupervisor(): {
const attachSignalHandlers = () => {
process.on("SIGINT", onSignal);
process.on("SIGTERM", onSignal);
// SIGTERM is not available on Windows — only attach where supported
if (process.platform !== "win32") {
process.on("SIGTERM", onSignal);
}
};
return { children, shutdown, attachSignalHandlers };
}
/**
* Send SIGTERM to a process and wait for it to exit.
* Falls back to SIGKILL if the process doesn't exit within the timeout.
* Terminate a process and wait for it to exit.
* On Unix: sends SIGTERM, falls back to SIGKILL after timeout.
* On Windows: tree-kill uses taskkill (no SIGTERM/SIGKILL distinction).
*/
function waitForProcessExit(child: ChildLike, timeoutMs: number): Promise<void> {
const isWindows = process.platform === "win32";
return new Promise<void>((resolve) => {
const pid = child.pid as number;
@@ -68,10 +74,12 @@ function waitForProcessExit(child: ChildLike, timeoutMs: number): Promise<void>
resolve();
};
// Set up timeout for fallback SIGKILL
// Set up timeout for force-kill fallback
const timeout = setTimeout(() => {
console.log(`[kandev] process ${pid} did not exit in time, sending SIGKILL`);
kill(pid, "SIGKILL", done);
console.log(`[kandev] process ${pid} did not exit in time, force killing`);
// On Windows, tree-kill always force-kills (no signal distinction)
// On Unix, escalate to SIGKILL
kill(pid, isWindows ? undefined : "SIGKILL", done);
}, timeoutMs);
// Listen for exit event if available
@@ -82,8 +90,8 @@ function waitForProcessExit(child: ChildLike, timeoutMs: number): Promise<void>
});
}
// Send SIGTERM
kill(pid, "SIGTERM", (err) => {
// Graceful termination: SIGTERM on Unix, default kill on Windows
kill(pid, isWindows ? undefined : "SIGTERM", (err) => {
// If kill fails (process already gone), we're done
if (err) {
clearTimeout(timeout);
+20 -6
View File
@@ -1,7 +1,22 @@
import { spawn, type ChildProcess, type StdioOptions } from "node:child_process";
import { readFileSync } from "node:fs";
import { createProcessSupervisor } from "./process";
let _isWSL: boolean | undefined;
function isWSL(): boolean {
if (_isWSL === undefined) {
try {
_isWSL = readFileSync("/proc/version", "utf8")
.toLowerCase()
.includes("microsoft");
} catch {
_isWSL = false;
}
}
return _isWSL;
}
export type WebLaunchOptions = {
command: string;
args: string[];
@@ -18,18 +33,17 @@ export function openBrowser(url: string) {
if (process.env.KANDEV_NO_BROWSER === "1") {
return;
}
const useCmd = process.platform === "win32" || isWSL();
const opener =
process.platform === "darwin"
? "open"
: process.platform === "win32"
? "cmd"
: useCmd
? "cmd.exe"
: "xdg-open";
const args =
process.platform === "win32"
? ["/c", "start", "", url]
: [url];
const args = useCmd ? ["/c", "start", "", url] : [url];
try {
const child = spawn(opener, args, { stdio: "ignore", detached: true });
child.on("error", () => {}); // ignore async spawn errors (e.g. xdg-open missing)
child.unref();
} catch {
// ignore browser launch errors
+73 -14
View File
@@ -367,7 +367,7 @@ importers:
version: 11.12.2
next:
specifier: 16.1.1
version: 16.1.1(@babel/core@7.28.6)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
version: 16.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
next-themes:
specifier: ^0.4.6
version: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
@@ -432,6 +432,9 @@ importers:
'@types/react-dom':
specifier: ^19
version: 19.2.3(@types/react@19.2.8)
cross-env:
specifier: ^7.0.3
version: 7.0.3
eslint:
specifier: ^9
version: 9.39.2(jiti@2.6.1)
@@ -1178,89 +1181,105 @@ packages:
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-arm@1.2.4':
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-ppc64@1.2.4':
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-riscv64@1.2.4':
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-s390x@1.2.4':
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-x64@1.2.4':
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-linux-arm64@0.34.5':
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-arm@0.34.5':
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-linux-ppc64@0.34.5':
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-riscv64@0.34.5':
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-s390x@0.34.5':
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-linux-x64@0.34.5':
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-linuxmusl-arm64@0.34.5':
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-linuxmusl-x64@0.34.5':
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-wasm32@0.34.5':
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
@@ -1521,24 +1540,28 @@ packages:
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@next/swc-linux-arm64-musl@16.1.1':
resolution: {integrity: sha512-MFHrgL4TXNQbBPzkKKur4Fb5ICEJa87HM7fczFs2+HWblM7mMLdco3dvyTI+QmLBU9xgns/EeeINSZD6Ar+oLg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@next/swc-linux-x64-gnu@16.1.1':
resolution: {integrity: sha512-20bYDfgOQAPUkkKBnyP9PTuHiJGM7HzNBbuqmD0jiFVZ0aOldz+VnJhbxzjcSabYsnNjMPsE0cyzEudpYxsrUQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@next/swc-linux-x64-musl@16.1.1':
resolution: {integrity: sha512-9pRbK3M4asAHQRkwaXwu601oPZHghuSC8IXNENgbBSyImHv/zY4K5udBusgdHkvJ/Tcr96jJwQYOll0qU8+fPA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@next/swc-win32-arm64-msvc@16.1.1':
resolution: {integrity: sha512-bdfQkggaLgnmYrFkSQfsHfOhk/mCYmjnrbRCGgkMcoOBZ4n+TRRSLmT/CU5SATzlBJ9TpioUyBW/vWFXTqQRiA==}
@@ -2373,66 +2396,79 @@ packages:
resolution: {integrity: sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.55.1':
resolution: {integrity: sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==}
cpu: [arm]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.55.1':
resolution: {integrity: sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.55.1':
resolution: {integrity: sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.55.1':
resolution: {integrity: sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==}
cpu: [loong64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-loong64-musl@4.55.1':
resolution: {integrity: sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==}
cpu: [loong64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-ppc64-gnu@4.55.1':
resolution: {integrity: sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-ppc64-musl@4.55.1':
resolution: {integrity: sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==}
cpu: [ppc64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-riscv64-gnu@4.55.1':
resolution: {integrity: sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.55.1':
resolution: {integrity: sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.55.1':
resolution: {integrity: sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.55.1':
resolution: {integrity: sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.55.1':
resolution: {integrity: sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==}
cpu: [x64]
os: [linux]
libc: [musl]
'@rollup/rollup-openbsd-x64@4.55.1':
resolution: {integrity: sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==}
@@ -2550,24 +2586,28 @@ packages:
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-arm64-musl@4.1.18':
resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@tailwindcss/oxide-linux-x64-gnu@4.1.18':
resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-x64-musl@4.1.18':
resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@tailwindcss/oxide-wasm32-wasi@4.1.18':
resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==}
@@ -3042,41 +3082,49 @@ packages:
resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-arm64-musl@1.11.1':
resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@unrs/resolver-binding-linux-ppc64-gnu@1.11.1':
resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-riscv64-gnu@1.11.1':
resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-riscv64-musl@1.11.1':
resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@unrs/resolver-binding-linux-s390x-gnu@1.11.1':
resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-x64-gnu@1.11.1':
resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-x64-musl@1.11.1':
resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==}
cpu: [x64]
os: [linux]
libc: [musl]
'@unrs/resolver-binding-wasm32-wasi@1.11.1':
resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==}
@@ -3499,6 +3547,11 @@ packages:
crelt@1.0.6:
resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
cross-env@7.0.3:
resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==}
engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'}
hasBin: true
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
@@ -4756,24 +4809,28 @@ packages:
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
lightningcss-linux-arm64-musl@1.30.2:
resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
lightningcss-linux-x64-gnu@1.30.2:
resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
lightningcss-linux-x64-musl@1.30.2:
resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
lightningcss-win32-arm64-msvc@1.30.2:
resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==}
@@ -9839,6 +9896,10 @@ snapshots:
crelt@1.0.6: {}
cross-env@7.0.3:
dependencies:
cross-spawn: 7.0.6
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
@@ -10375,8 +10436,8 @@ snapshots:
'@next/eslint-plugin-next': 16.1.1
eslint: 9.39.2(jiti@2.6.1)
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1))
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-react-hooks: 7.0.1(eslint@9.39.2(jiti@2.6.1))
@@ -10398,7 +10459,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)):
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)):
dependencies:
'@nolyfill/is-core-module': 1.0.39
debug: 4.4.3
@@ -10409,22 +10470,22 @@ snapshots:
tinyglobby: 0.2.15
unrs-resolver: 1.11.1
optionalDependencies:
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))
transitivePeerDependencies:
- supports-color
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)):
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)):
dependencies:
debug: 3.2.7
optionalDependencies:
'@typescript-eslint/parser': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
eslint: 9.39.2(jiti@2.6.1)
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1))
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))
transitivePeerDependencies:
- supports-color
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)):
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.9
@@ -10435,7 +10496,7 @@ snapshots:
doctrine: 2.1.0
eslint: 9.39.2(jiti@2.6.1)
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1))
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3
@@ -11877,7 +11938,7 @@ snapshots:
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
next@16.1.1(@babel/core@7.28.6)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
next@16.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
dependencies:
'@next/env': 16.1.1
'@swc/helpers': 0.5.15
@@ -11886,7 +11947,7 @@ snapshots:
postcss: 8.4.31
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
styled-jsx: 5.1.6(@babel/core@7.28.6)(react@19.2.3)
styled-jsx: 5.1.6(react@19.2.3)
optionalDependencies:
'@next/swc-darwin-arm64': 16.1.1
'@next/swc-darwin-x64': 16.1.1
@@ -13005,12 +13066,10 @@ snapshots:
dependencies:
inline-style-parser: 0.2.7
styled-jsx@5.1.6(@babel/core@7.28.6)(react@19.2.3):
styled-jsx@5.1.6(react@19.2.3):
dependencies:
client-only: 0.0.1
react: 19.2.3
optionalDependencies:
'@babel/core': 7.28.6
stylis@4.3.6: {}
+3 -2
View File
@@ -4,8 +4,8 @@
"private": true,
"scripts": {
"dev": "next dev --hostname 0.0.0.0",
"build": "NODE_ENV=production next build",
"start": "NODE_ENV=production next start",
"build": "cross-env NODE_ENV=production next build",
"start": "cross-env NODE_ENV=production next start",
"lint": "eslint",
"test": "vitest run"
},
@@ -104,6 +104,7 @@
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.1.1",
"cross-env": "^7.0.3",
"happy-dom": "^20.1.0",
"tailwindcss": "^4",
"typescript": "^5",
+112
View File
@@ -0,0 +1,112 @@
# Windows Support (WSL)
Kandev runs on Windows through WSL (Windows Subsystem for Linux).
## Prerequisites
- Windows 10 (version 2004+) or Windows 11
- WSL 2 with a Linux distribution installed (Ubuntu recommended)
## Setup
### 1. Install WSL
From PowerShell (as Administrator):
```powershell
wsl --install
```
Restart your machine if prompted, then open your Linux distribution from the Start menu.
### 2. Install system dependencies
```bash
sudo apt update
sudo apt install build-essential libatomic1 git
```
- `build-essential` provides `gcc`, required by `go-sqlite3` (CGO)
- `libatomic1` is required by Node.js
### 3. Install mise (runtime manager)
```bash
curl https://mise.run | sh
```
Add mise to your shell profile:
```bash
echo 'eval "$(~/.local/bin/mise activate bash)"' >> ~/.bashrc
source ~/.bashrc
```
### 4. Install runtimes
```bash
mise use -g node
mise use -g pnpm
mise use -g go
```
### 5. Clone and install
```bash
git clone <repo-url> kandev
cd kandev
make install
```
### 6. Run
```bash
make dev # development mode
make start # production mode
```
## Troubleshooting
### `libatomic.so.1: cannot open shared object file`
Node.js requires `libatomic1`. Install it:
```bash
sudo apt install libatomic1
```
### `pnpm: command not found`
Ensure pnpm is installed and activated via mise:
```bash
mise use -g pnpm
```
### `Binary was compiled with 'CGO_ENABLED=0', go-sqlite3 requires cgo`
The SQLite driver needs a C compiler. Install `build-essential`:
```bash
sudo apt install build-essential
```
### Browser doesn't auto-open
WSL interop must be enabled so the CLI can launch your Windows browser. Ensure `/etc/wsl.conf` contains:
```ini
[interop]
enabled=true
appendWindowsPath=true
```
Then restart WSL from PowerShell:
```powershell
wsl --shutdown
```
### `cd: can't cd to apps` during `make install`
This is a symptom of pnpm not being installed. Follow step 4 above.