diff --git a/Dockerfile b/Dockerfile index 70a9dfe76e..9034defd85 100644 --- a/Dockerfile +++ b/Dockerfile @@ -95,7 +95,7 @@ RUN cd /usr/local/lib/kandev-cli && npm install --omit=dev && \ chmod +x bin/cli.js && \ ln -s /usr/local/lib/kandev-cli/bin/cli.js /usr/local/bin/kandev -# Data directory for SQLite and worktrees +# Kandev home directory (DB, worktrees, sessions, repos) VOLUME ["/data"] # Environment defaults for containerized operation diff --git a/apps/backend/cmd/kandev/agents.go b/apps/backend/cmd/kandev/agents.go index e6dbce7882..b6d6c4141f 100644 --- a/apps/backend/cmd/kandev/agents.go +++ b/apps/backend/cmd/kandev/agents.go @@ -95,7 +95,7 @@ func provideLifecycleManager( profileResolver, mcpService, lifecycle.ExecutorFallbackWarn, - cfg.ResolvedDataDir(), + cfg.ResolvedHomeDir(), log, ) diff --git a/apps/backend/cmd/kandev/main.go b/apps/backend/cmd/kandev/main.go index 7d441236a4..28ca38d9af 100644 --- a/apps/backend/cmd/kandev/main.go +++ b/apps/backend/cmd/kandev/main.go @@ -282,8 +282,8 @@ func startAgentInfrastructure( log.Info("Workspace info provider configured for session recovery") // Configure quick-chat workspace cleanup - if dataDir := cfg.ResolvedDataDir(); dataDir != "" { - quickChatDir := filepath.Join(dataDir, "quick-chat") + if homeDir := cfg.ResolvedHomeDir(); homeDir != "" { + quickChatDir := filepath.Join(homeDir, "quick-chat") services.Task.SetQuickChatDir(quickChatDir) log.Info("Quick-chat workspace cleanup configured", zap.String("quick_chat_dir", quickChatDir)) } @@ -293,7 +293,7 @@ func startAgentInfrastructure( // ============================================ repoCloner := repoclone.NewCloner(repoclone.Config{ BasePath: cfg.RepoClone.BasePath, - }, repoclone.DetectGitProtocol(), cfg.ResolvedDataDir(), log) + }, repoclone.DetectGitProtocol(), cfg.ResolvedHomeDir(), log) log.Info("Repository cloner configured", zap.String("base_path", cfg.RepoClone.BasePath)) @@ -349,7 +349,7 @@ func startGatewayAndServe( ctx, log, eventBus, services.Task, services.User, orchestratorSvc, lifecycleMgr, agentRegistry, repos.Notification, repos.Task, services.GitHub, - cfg.ResolvedDataDir(), + cfg.ResolvedHomeDir(), ) if err != nil { log.Error("Failed to initialize WebSocket gateway", zap.Error(err)) diff --git a/apps/backend/internal/common/config/config.go b/apps/backend/internal/common/config/config.go index b505917086..03ebd0caf7 100644 --- a/apps/backend/internal/common/config/config.go +++ b/apps/backend/internal/common/config/config.go @@ -29,9 +29,29 @@ type Config struct { RepoClone RepoCloneConfig `mapstructure:"repoClone"` } +// ResolvedHomeDir returns the Kandev home directory (~/.kandev for local dev). +// When KANDEV_DATA_DIR is explicitly set (e.g., /data in Docker), it returns that value +// so containers keep a flat layout. When unset, returns ~/.kandev — the parent of the data dir. +// Use this for workspace artifacts: worktrees, repos, sessions, quick-chat, lsp-servers. +func (c *Config) ResolvedHomeDir() string { + if c.DataDir != "" { + if strings.HasPrefix(c.DataDir, "~/") { + if home, err := os.UserHomeDir(); err == nil { + return filepath.Join(home, c.DataDir[2:]) + } + } + return c.DataDir + } + home, err := os.UserHomeDir() + if err != nil { + return ".kandev" + } + return filepath.Join(home, ".kandev") +} + // ResolvedDataDir returns the base data directory for Kandev. // Config.DataDir is populated by Viper from the config file or KANDEV_DATA_DIR env var. -// If empty, falls back to ~/.kandev/data. +// If empty, falls back to ~/.kandev/data. Use this for the database only. func (c *Config) ResolvedDataDir() string { if c.DataDir != "" { if strings.HasPrefix(c.DataDir, "~/") { diff --git a/apps/backend/internal/common/config/config_test.go b/apps/backend/internal/common/config/config_test.go index 12c2b194ae..64159971d4 100644 --- a/apps/backend/internal/common/config/config_test.go +++ b/apps/backend/internal/common/config/config_test.go @@ -6,6 +6,37 @@ import ( "testing" ) +func TestResolvedHomeDir_NoDataDir(t *testing.T) { + cfg := &Config{} + home, err := os.UserHomeDir() + if err != nil { + t.Skip("cannot determine home directory") + } + want := filepath.Join(home, ".kandev") + if got := cfg.ResolvedHomeDir(); got != want { + t.Errorf("ResolvedHomeDir() = %q, want %q", got, want) + } +} + +func TestResolvedHomeDir_WithDataDir(t *testing.T) { + cfg := &Config{DataDir: "/data"} + if got := cfg.ResolvedHomeDir(); got != "/data" { + t.Errorf("ResolvedHomeDir() = %q, want %q", got, "/data") + } +} + +func TestResolvedHomeDir_TildeExpansion(t *testing.T) { + home, err := os.UserHomeDir() + if err != nil { + t.Skip("cannot determine home directory") + } + cfg := &Config{DataDir: "~/mykandev"} + want := filepath.Join(home, "mykandev") + if got := cfg.ResolvedHomeDir(); got != want { + t.Errorf("ResolvedHomeDir() = %q, want %q", got, want) + } +} + func TestResolvedDataDir_ExplicitConfig(t *testing.T) { cfg := &Config{DataDir: "/custom/data"} if got := cfg.ResolvedDataDir(); got != "/custom/data" { diff --git a/apps/backend/internal/worktree/provider.go b/apps/backend/internal/worktree/provider.go index 23cb722f04..b9289e3068 100644 --- a/apps/backend/internal/worktree/provider.go +++ b/apps/backend/internal/worktree/provider.go @@ -20,7 +20,7 @@ func Provide(writer, reader *sqlx.DB, cfg *config.Config, log *logger.Logger) (* FetchTimeoutSeconds: cfg.Worktree.FetchTimeoutSeconds, PullTimeoutSeconds: cfg.Worktree.PullTimeoutSeconds, } - wtCfg.SetDataDirFallback(cfg.ResolvedDataDir()) + wtCfg.SetDataDirFallback(cfg.ResolvedHomeDir()) manager, err := NewManager(wtCfg, store, log) if err != nil { return nil, nil, err diff --git a/docs/docker.md b/docs/docker.md index b4cf115901..ca49691300 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -63,7 +63,7 @@ docker run -p 8080:8080 \ | Variable | Default | Description | |----------|---------|-------------| -| `KANDEV_DATA_DIR` | `/data` | Base directory for all data (DB, worktrees, sessions, etc.) | +| `KANDEV_DATA_DIR` | `/data` | Kandev home directory — contains DB, worktrees, sessions, repos, and LSP servers | | `KANDEV_DATABASE_DRIVER` | `sqlite` | Database driver (`sqlite` or `postgres`) | | `KANDEV_DATABASE_PATH` | `$KANDEV_DATA_DIR/kandev.db` | SQLite database file path (override) | | `KANDEV_LOG_LEVEL` | `info` | Log level: `debug`, `info`, `warn`, `error` | diff --git a/docs/k8s.md b/docs/k8s.md index ec576101d5..b2017b6530 100644 --- a/docs/k8s.md +++ b/docs/k8s.md @@ -85,7 +85,7 @@ Kandev reads configuration via `KANDEV_`-prefixed environment variables (Viper). | Env Var | Default | Description | |---------|---------|-------------| | `KANDEV_SERVER_PORT` | `8080` | Server port (API + WebSocket + Web UI) | -| `KANDEV_DATA_DIR` | `/data` | Base directory for all data (DB, worktrees, sessions, etc.) | +| `KANDEV_DATA_DIR` | `/data` | Kandev home directory — contains DB, worktrees, sessions, repos, and LSP servers | | `KANDEV_DATABASE_DRIVER` | `sqlite` | Database driver (`sqlite` or `postgres`) | | `KANDEV_DATABASE_PATH` | `$KANDEV_DATA_DIR/kandev.db` | SQLite database file path (override) | | `KANDEV_DOCKER_ENABLED` | `false` | Enable Docker runtime for agents (requires DinD) |