fix(cli): isolate dev mode state when running inside a kandev task (#617)
This commit is contained in:
@@ -94,7 +94,7 @@ Start the backend:
|
|||||||
```bash
|
```bash
|
||||||
E2E_TMP=$(mktemp -d) && mkdir -p "$E2E_TMP/.kandev" && \
|
E2E_TMP=$(mktemp -d) && mkdir -p "$E2E_TMP/.kandev" && \
|
||||||
printf '[user]\n name = E2E Test\n email = e2e@test.local\n[commit]\n gpgsign = false\n' > "$E2E_TMP/.gitconfig" && \
|
printf '[user]\n name = E2E Test\n email = e2e@test.local\n[commit]\n gpgsign = false\n' > "$E2E_TMP/.gitconfig" && \
|
||||||
HOME="$E2E_TMP" KANDEV_DATA_DIR="$E2E_TMP/.kandev" KANDEV_SERVER_PORT=$BACKEND_PORT \
|
HOME="$E2E_TMP" KANDEV_HOME_DIR="$E2E_TMP/.kandev" KANDEV_SERVER_PORT=$BACKEND_PORT \
|
||||||
KANDEV_DATABASE_PATH="$E2E_TMP/kandev.db" KANDEV_MOCK_AGENT=only \
|
KANDEV_DATABASE_PATH="$E2E_TMP/kandev.db" KANDEV_MOCK_AGENT=only \
|
||||||
KANDEV_MOCK_GITHUB=true KANDEV_DOCKER_ENABLED=false KANDEV_WORKTREE_ENABLED=false \
|
KANDEV_MOCK_GITHUB=true KANDEV_DOCKER_ENABLED=false KANDEV_WORKTREE_ENABLED=false \
|
||||||
KANDEV_LOG_LEVEL=warn apps/backend/bin/kandev &
|
KANDEV_LOG_LEVEL=warn apps/backend/bin/kandev &
|
||||||
|
|||||||
@@ -49,6 +49,11 @@ temp/
|
|||||||
# Agent session files (local development)
|
# Agent session files (local development)
|
||||||
.augment/
|
.augment/
|
||||||
|
|
||||||
|
# Isolated kandev home used by `make dev` (see apps/cli/src/dev.ts).
|
||||||
|
# Keeps dev backend state (db, tasks, worktrees, repos) out of the user's
|
||||||
|
# production ~/.kandev when developing kandev using kandev.
|
||||||
|
/.kandev-dev/
|
||||||
|
|
||||||
# Build artifacts
|
# Build artifacts
|
||||||
dist/
|
dist/
|
||||||
build/
|
build/
|
||||||
|
|||||||
+1
-1
@@ -100,7 +100,7 @@ VOLUME ["/data"]
|
|||||||
|
|
||||||
# Environment defaults for containerized operation
|
# Environment defaults for containerized operation
|
||||||
ENV KANDEV_NO_BROWSER=1 \
|
ENV KANDEV_NO_BROWSER=1 \
|
||||||
KANDEV_DATA_DIR=/data \
|
KANDEV_HOME_DIR=/data \
|
||||||
KANDEV_DOCKER_ENABLED=false \
|
KANDEV_DOCKER_ENABLED=false \
|
||||||
HOSTNAME=0.0.0.0 \
|
HOSTNAME=0.0.0.0 \
|
||||||
NODE_ENV=production
|
NODE_ENV=production
|
||||||
|
|||||||
@@ -13,9 +13,19 @@ import (
|
|||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// kandevHomeSubdir is the hidden directory name used for the Kandev root
|
||||||
|
// under the user's home directory (e.g. ~/.kandev). This is the single
|
||||||
|
// source of truth for the dotdir name; derived paths go through
|
||||||
|
// ResolvedHomeDir / ResolvedDataDir rather than re-constructing it.
|
||||||
|
const kandevHomeSubdir = ".kandev"
|
||||||
|
|
||||||
// Config holds all configuration sections for Kandev.
|
// Config holds all configuration sections for Kandev.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
DataDir string `mapstructure:"dataDir"`
|
// HomeDir is the root Kandev directory (e.g. ~/.kandev in prod, or
|
||||||
|
// <repo>/.kandev-dev during local development). When empty, falls back
|
||||||
|
// to ~/.kandev. All workspace artifacts (data, tasks, worktrees, repos)
|
||||||
|
// live under this root.
|
||||||
|
HomeDir string `mapstructure:"homeDir"`
|
||||||
Server ServerConfig `mapstructure:"server"`
|
Server ServerConfig `mapstructure:"server"`
|
||||||
Database DatabaseConfig `mapstructure:"database"`
|
Database DatabaseConfig `mapstructure:"database"`
|
||||||
NATS NATSConfig `mapstructure:"nats"`
|
NATS NATSConfig `mapstructure:"nats"`
|
||||||
@@ -30,43 +40,41 @@ type Config struct {
|
|||||||
Debug DebugConfig `mapstructure:"debug"`
|
Debug DebugConfig `mapstructure:"debug"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResolvedHomeDir returns the Kandev home directory (~/.kandev for local dev).
|
// expandTilde expands a leading "~/" to the user's home directory.
|
||||||
// When KANDEV_DATA_DIR is explicitly set (e.g., /data in Docker), it returns that value
|
// Returns the input unchanged if expansion is unnecessary or fails.
|
||||||
// so containers keep a flat layout. When unset, returns ~/.kandev — the parent of the data dir.
|
func expandTilde(p string) string {
|
||||||
// Use this for workspace artifacts: worktrees, repos, sessions, quick-chat, lsp-servers.
|
if !strings.HasPrefix(p, "~/") {
|
||||||
func (c *Config) ResolvedHomeDir() string {
|
return p
|
||||||
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()
|
home, err := os.UserHomeDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ".kandev"
|
return p
|
||||||
}
|
}
|
||||||
return filepath.Join(home, ".kandev")
|
return filepath.Join(home, p[2:])
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResolvedDataDir returns the base data directory for Kandev.
|
// ResolvedHomeDir returns the Kandev root directory — the parent of data,
|
||||||
// Config.DataDir is populated by Viper from the config file or KANDEV_DATA_DIR env var.
|
// tasks, worktrees, repos, sessions, quick-chat and lsp-servers.
|
||||||
// If empty, falls back to ~/.kandev/data. Use this for the database only.
|
//
|
||||||
func (c *Config) ResolvedDataDir() string {
|
// Resolution order:
|
||||||
if c.DataDir != "" {
|
// 1. KANDEV_HOME_DIR (explicit override — e.g. /data in Docker, /kandev in K8s,
|
||||||
if strings.HasPrefix(c.DataDir, "~/") {
|
// or <repo>/.kandev-dev during local development).
|
||||||
if home, err := os.UserHomeDir(); err == nil {
|
// 2. ~/.kandev (default).
|
||||||
return filepath.Join(home, c.DataDir[2:])
|
func (c *Config) ResolvedHomeDir() string {
|
||||||
}
|
if c.HomeDir != "" {
|
||||||
}
|
return expandTilde(c.HomeDir)
|
||||||
return c.DataDir
|
|
||||||
}
|
}
|
||||||
home, err := os.UserHomeDir()
|
home, err := os.UserHomeDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return filepath.Join(".kandev", "data")
|
return kandevHomeSubdir
|
||||||
}
|
}
|
||||||
return filepath.Join(home, ".kandev", "data")
|
return filepath.Join(home, kandevHomeSubdir)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolvedDataDir returns the base data directory (where the SQLite DB lives).
|
||||||
|
// Always <ResolvedHomeDir>/data — relocate via KANDEV_HOME_DIR, not a separate knob.
|
||||||
|
func (c *Config) ResolvedDataDir() string {
|
||||||
|
return filepath.Join(c.ResolvedHomeDir(), "data")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServerConfig holds HTTP server configuration.
|
// ServerConfig holds HTTP server configuration.
|
||||||
@@ -228,8 +236,8 @@ func setDefaults(v *viper.Viper) {
|
|||||||
v.SetDefault("server.writeTimeout", 30)
|
v.SetDefault("server.writeTimeout", 30)
|
||||||
v.SetDefault("server.webInternalUrl", "")
|
v.SetDefault("server.webInternalUrl", "")
|
||||||
|
|
||||||
// DataDir default — empty means resolve from KANDEV_DATA_DIR env or ~/.kandev/data
|
// HomeDir default — empty means resolve from KANDEV_HOME_DIR env or ~/.kandev
|
||||||
v.SetDefault("dataDir", "")
|
v.SetDefault("homeDir", "")
|
||||||
|
|
||||||
// Database defaults
|
// Database defaults
|
||||||
v.SetDefault("database.driver", "sqlite")
|
v.SetDefault("database.driver", "sqlite")
|
||||||
@@ -346,7 +354,7 @@ func LoadWithPath(configPath string) (*Config, error) {
|
|||||||
_ = v.BindEnv("agent.mcpServerPort", "KANDEV_AGENT_MCP_SERVER_PORT")
|
_ = v.BindEnv("agent.mcpServerPort", "KANDEV_AGENT_MCP_SERVER_PORT")
|
||||||
_ = v.BindEnv("agent.mcpServerUrl", "KANDEV_AGENT_MCP_SERVER_URL")
|
_ = v.BindEnv("agent.mcpServerUrl", "KANDEV_AGENT_MCP_SERVER_URL")
|
||||||
_ = v.BindEnv("server.webInternalUrl", "KANDEV_WEB_INTERNAL_URL")
|
_ = v.BindEnv("server.webInternalUrl", "KANDEV_WEB_INTERNAL_URL")
|
||||||
_ = v.BindEnv("dataDir", "KANDEV_DATA_DIR")
|
_ = v.BindEnv("homeDir", "KANDEV_HOME_DIR")
|
||||||
_ = v.BindEnv("logging.level", "KANDEV_LOG_LEVEL")
|
_ = v.BindEnv("logging.level", "KANDEV_LOG_LEVEL")
|
||||||
_ = v.BindEnv("events.namespace", "KANDEV_EVENTS_NAMESPACE")
|
_ = v.BindEnv("events.namespace", "KANDEV_EVENTS_NAMESPACE")
|
||||||
_ = v.BindEnv("debug.pprofEnabled", "KANDEV_DEBUG_PPROF_ENABLED")
|
_ = v.BindEnv("debug.pprofEnabled", "KANDEV_DEBUG_PPROF_ENABLED")
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestResolvedHomeDir_NoDataDir(t *testing.T) {
|
func TestResolvedHomeDir_Default(t *testing.T) {
|
||||||
cfg := &Config{}
|
cfg := &Config{}
|
||||||
home, err := os.UserHomeDir()
|
home, err := os.UserHomeDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -18,10 +18,10 @@ func TestResolvedHomeDir_NoDataDir(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestResolvedHomeDir_WithDataDir(t *testing.T) {
|
func TestResolvedHomeDir_WithHomeDir(t *testing.T) {
|
||||||
cfg := &Config{DataDir: "/data"}
|
cfg := &Config{HomeDir: "/custom/kandev"}
|
||||||
if got := cfg.ResolvedHomeDir(); got != "/data" {
|
if got := cfg.ResolvedHomeDir(); got != "/custom/kandev" {
|
||||||
t.Errorf("ResolvedHomeDir() = %q, want %q", got, "/data")
|
t.Errorf("ResolvedHomeDir() = %q, want %q", got, "/custom/kandev")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,21 +30,14 @@ func TestResolvedHomeDir_TildeExpansion(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Skip("cannot determine home directory")
|
t.Skip("cannot determine home directory")
|
||||||
}
|
}
|
||||||
cfg := &Config{DataDir: "~/mykandev"}
|
cfg := &Config{HomeDir: "~/.kandev-dev"}
|
||||||
want := filepath.Join(home, "mykandev")
|
want := filepath.Join(home, ".kandev-dev")
|
||||||
if got := cfg.ResolvedHomeDir(); got != want {
|
if got := cfg.ResolvedHomeDir(); got != want {
|
||||||
t.Errorf("ResolvedHomeDir() = %q, want %q", got, want)
|
t.Errorf("ResolvedHomeDir() = %q, want %q", got, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestResolvedDataDir_ExplicitConfig(t *testing.T) {
|
func TestResolvedDataDir_Default(t *testing.T) {
|
||||||
cfg := &Config{DataDir: "/custom/data"}
|
|
||||||
if got := cfg.ResolvedDataDir(); got != "/custom/data" {
|
|
||||||
t.Errorf("ResolvedDataDir() = %q, want %q", got, "/custom/data")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResolvedDataDir_FallbackToHome(t *testing.T) {
|
|
||||||
cfg := &Config{}
|
cfg := &Config{}
|
||||||
home, err := os.UserHomeDir()
|
home, err := os.UserHomeDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -56,22 +49,10 @@ func TestResolvedDataDir_FallbackToHome(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestResolvedDataDir_ConfigTakesPrecedence(t *testing.T) {
|
func TestResolvedDataDir_DerivedFromHomeDir(t *testing.T) {
|
||||||
t.Setenv("KANDEV_DATA_DIR", "/env/data")
|
// Data always lives under <HomeDir>/data. No independent override.
|
||||||
// Even with env set, explicit Config.DataDir wins
|
cfg := &Config{HomeDir: "/custom/kandev"}
|
||||||
cfg := &Config{DataDir: "/explicit/data"}
|
want := filepath.Join("/custom/kandev", "data")
|
||||||
if got := cfg.ResolvedDataDir(); got != "/explicit/data" {
|
|
||||||
t.Errorf("ResolvedDataDir() = %q, want %q", got, "/explicit/data")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResolvedDataDir_TildeExpansion(t *testing.T) {
|
|
||||||
home, err := os.UserHomeDir()
|
|
||||||
if err != nil {
|
|
||||||
t.Skip("cannot determine home directory")
|
|
||||||
}
|
|
||||||
cfg := &Config{DataDir: "~/kandev"}
|
|
||||||
want := filepath.Join(home, "kandev")
|
|
||||||
if got := cfg.ResolvedDataDir(); got != want {
|
if got := cfg.ResolvedDataDir(); got != want {
|
||||||
t.Errorf("ResolvedDataDir() = %q, want %q", got, want)
|
t.Errorf("ResolvedDataDir() = %q, want %q", got, want)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package persistence
|
package persistence
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
@@ -36,6 +39,9 @@ func provideSQLite(cfg *config.Config, log *logger.Logger) (*db.Pool, func() err
|
|||||||
dbPath := cfg.Database.Path
|
dbPath := cfg.Database.Path
|
||||||
if dbPath == "" {
|
if dbPath == "" {
|
||||||
dbPath = filepath.Join(cfg.ResolvedDataDir(), "kandev.db")
|
dbPath = filepath.Join(cfg.ResolvedDataDir(), "kandev.db")
|
||||||
|
if err := migrateLegacyDBPath(cfg, dbPath, log); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("migrate legacy DB: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Writer: single connection, owns WAL/journal_mode setup.
|
// Writer: single connection, owns WAL/journal_mode setup.
|
||||||
@@ -71,6 +77,50 @@ func provideSQLite(cfg *config.Config, log *logger.Logger) (*db.Pool, func() err
|
|||||||
return pool, cleanup, nil
|
return pool, cleanup, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// migrateLegacyDBPath moves a pre-KANDEV_HOME_DIR SQLite DB from
|
||||||
|
// <HomeDir>/kandev.db into the new derived location at <HomeDir>/data/kandev.db
|
||||||
|
// on first boot after an upgrade, so `docker pull && docker restart` doesn't
|
||||||
|
// silently start against an empty DB.
|
||||||
|
//
|
||||||
|
// Runs only when:
|
||||||
|
// - KANDEV_DATABASE_PATH is not explicitly set (caller checks this).
|
||||||
|
// - The new derived path does not exist yet.
|
||||||
|
// - A legacy file exists at <HomeDir>/kandev.db.
|
||||||
|
// - The legacy path differs from the new path (skip when HomeDir == DataDir).
|
||||||
|
//
|
||||||
|
// Only the main .db file is moved — SQLite recreates -wal/-shm on open, and a
|
||||||
|
// cleanly-shut-down DB (the expected state on container restart) has empty WAL.
|
||||||
|
func migrateLegacyDBPath(cfg *config.Config, newPath string, log *logger.Logger) error {
|
||||||
|
if _, err := os.Stat(newPath); !errors.Is(err, fs.ErrNotExist) {
|
||||||
|
return nil // new path exists (or stat failed) — nothing to migrate
|
||||||
|
}
|
||||||
|
legacyPath := filepath.Join(cfg.ResolvedHomeDir(), "kandev.db")
|
||||||
|
if legacyPath == newPath {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(legacyPath); err != nil {
|
||||||
|
return nil // no legacy file to migrate
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(filepath.Dir(newPath), 0o755); err != nil {
|
||||||
|
return fmt.Errorf("create data dir %s: %w", filepath.Dir(newPath), err)
|
||||||
|
}
|
||||||
|
if err := os.Rename(legacyPath, newPath); err != nil {
|
||||||
|
return fmt.Errorf("move %s -> %s: %w", legacyPath, newPath, err)
|
||||||
|
}
|
||||||
|
// Also move -wal / -shm if present. These are transient (SQLite recreates
|
||||||
|
// them on open) but moving them avoids orphaned files on the volume.
|
||||||
|
for _, suffix := range []string{"-wal", "-shm"} {
|
||||||
|
_ = os.Rename(legacyPath+suffix, newPath+suffix)
|
||||||
|
}
|
||||||
|
if log != nil {
|
||||||
|
log.Info("Migrated SQLite database from pre-KANDEV_HOME_DIR location",
|
||||||
|
zap.String("legacy_path", legacyPath),
|
||||||
|
zap.String("new_path", newPath),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func providePostgres(cfg *config.Config, log *logger.Logger) (*db.Pool, func() error, error) {
|
func providePostgres(cfg *config.Config, log *logger.Logger) (*db.Pool, func() error, error) {
|
||||||
dsn := cfg.Database.DSN()
|
dsn := cfg.Database.DSN()
|
||||||
dbConn, err := db.OpenPostgres(dsn, cfg.Database.MaxConns, cfg.Database.MinConns)
|
dbConn, err := db.OpenPostgres(dsn, cfg.Database.MaxConns, cfg.Database.MinConns)
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
package persistence
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/kandev/kandev/internal/common/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMigrateLegacyDBPath_MovesLegacyDB(t *testing.T) {
|
||||||
|
homeDir := t.TempDir()
|
||||||
|
legacyPath := filepath.Join(homeDir, "kandev.db")
|
||||||
|
newPath := filepath.Join(homeDir, "data", "kandev.db")
|
||||||
|
|
||||||
|
if err := os.WriteFile(legacyPath, []byte("legacy-db-content"), 0o600); err != nil {
|
||||||
|
t.Fatalf("seed legacy db: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &config.Config{HomeDir: homeDir}
|
||||||
|
if err := migrateLegacyDBPath(cfg, newPath, nil); err != nil {
|
||||||
|
t.Fatalf("migrateLegacyDBPath: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(legacyPath); !os.IsNotExist(err) {
|
||||||
|
t.Errorf("legacy file still exists: %v", err)
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(newPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read migrated db: %v", err)
|
||||||
|
}
|
||||||
|
if string(data) != "legacy-db-content" {
|
||||||
|
t.Errorf("migrated content = %q, want %q", data, "legacy-db-content")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMigrateLegacyDBPath_MovesWalAndShm(t *testing.T) {
|
||||||
|
homeDir := t.TempDir()
|
||||||
|
legacyPath := filepath.Join(homeDir, "kandev.db")
|
||||||
|
newPath := filepath.Join(homeDir, "data", "kandev.db")
|
||||||
|
|
||||||
|
for _, suffix := range []string{"", "-wal", "-shm"} {
|
||||||
|
if err := os.WriteFile(legacyPath+suffix, []byte("x"+suffix), 0o600); err != nil {
|
||||||
|
t.Fatalf("seed %s: %v", legacyPath+suffix, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &config.Config{HomeDir: homeDir}
|
||||||
|
if err := migrateLegacyDBPath(cfg, newPath, nil); err != nil {
|
||||||
|
t.Fatalf("migrateLegacyDBPath: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, suffix := range []string{"", "-wal", "-shm"} {
|
||||||
|
if _, err := os.Stat(newPath + suffix); err != nil {
|
||||||
|
t.Errorf("missing new file %s: %v", newPath+suffix, err)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(legacyPath + suffix); !os.IsNotExist(err) {
|
||||||
|
t.Errorf("legacy file %s still exists", legacyPath+suffix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMigrateLegacyDBPath_NoLegacyFile(t *testing.T) {
|
||||||
|
homeDir := t.TempDir()
|
||||||
|
newPath := filepath.Join(homeDir, "data", "kandev.db")
|
||||||
|
|
||||||
|
cfg := &config.Config{HomeDir: homeDir}
|
||||||
|
if err := migrateLegacyDBPath(cfg, newPath, nil); err != nil {
|
||||||
|
t.Fatalf("migrateLegacyDBPath: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should be a no-op; the data dir should NOT be created if nothing to migrate.
|
||||||
|
if _, err := os.Stat(filepath.Dir(newPath)); !os.IsNotExist(err) {
|
||||||
|
t.Errorf("data dir was created unexpectedly")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMigrateLegacyDBPath_NewPathExists_SkipsMigration(t *testing.T) {
|
||||||
|
homeDir := t.TempDir()
|
||||||
|
legacyPath := filepath.Join(homeDir, "kandev.db")
|
||||||
|
newPath := filepath.Join(homeDir, "data", "kandev.db")
|
||||||
|
|
||||||
|
if err := os.MkdirAll(filepath.Dir(newPath), 0o755); err != nil {
|
||||||
|
t.Fatalf("mkdir: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(legacyPath, []byte("legacy"), 0o600); err != nil {
|
||||||
|
t.Fatalf("seed legacy: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(newPath, []byte("new"), 0o600); err != nil {
|
||||||
|
t.Fatalf("seed new: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &config.Config{HomeDir: homeDir}
|
||||||
|
if err := migrateLegacyDBPath(cfg, newPath, nil); err != nil {
|
||||||
|
t.Fatalf("migrateLegacyDBPath: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// New path untouched.
|
||||||
|
got, err := os.ReadFile(newPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read new: %v", err)
|
||||||
|
}
|
||||||
|
if string(got) != "new" {
|
||||||
|
t.Errorf("new path was overwritten: got %q", got)
|
||||||
|
}
|
||||||
|
// Legacy untouched.
|
||||||
|
if _, err := os.Stat(legacyPath); err != nil {
|
||||||
|
t.Errorf("legacy file removed despite new existing: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMigrateLegacyDBPath_LegacyEqualsNew_NoOp(t *testing.T) {
|
||||||
|
// If HomeDir somehow resolves so that legacy == new (shouldn't happen with
|
||||||
|
// the current resolver, but guard against it), we must not rename a file
|
||||||
|
// onto itself.
|
||||||
|
tmp := t.TempDir()
|
||||||
|
sameFile := filepath.Join(tmp, "kandev.db")
|
||||||
|
if err := os.WriteFile(sameFile, []byte("x"), 0o600); err != nil {
|
||||||
|
t.Fatalf("seed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &config.Config{HomeDir: tmp}
|
||||||
|
// Pretend the derived path is the same as the legacy path.
|
||||||
|
if err := migrateLegacyDBPath(cfg, sameFile, nil); err != nil {
|
||||||
|
t.Fatalf("migrateLegacyDBPath: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(sameFile); err != nil {
|
||||||
|
t.Errorf("file disappeared: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,21 @@ export const RANDOM_PORT_RETRIES = 10;
|
|||||||
export const HEALTH_TIMEOUT_MS_RELEASE = 45000;
|
export const HEALTH_TIMEOUT_MS_RELEASE = 45000;
|
||||||
export const HEALTH_TIMEOUT_MS_DEV = 600000;
|
export const HEALTH_TIMEOUT_MS_DEV = 600000;
|
||||||
|
|
||||||
|
// Kandev root directory. Single source of truth for the dotdir name and
|
||||||
|
// everything derived from it (data, tasks, bin). Dev mode uses a separate
|
||||||
|
// root under the repo (see DEV_KANDEV_DOTDIR).
|
||||||
|
export const KANDEV_DOTDIR = ".kandev";
|
||||||
|
export const KANDEV_HOME_DIR = path.join(os.homedir(), KANDEV_DOTDIR);
|
||||||
|
export const KANDEV_TASKS_DIR = path.join(KANDEV_HOME_DIR, "tasks");
|
||||||
|
|
||||||
// Local user cache/data directories for release bundles and DB.
|
// Local user cache/data directories for release bundles and DB.
|
||||||
export const CACHE_DIR = path.join(os.homedir(), ".kandev", "bin");
|
export const CACHE_DIR = path.join(KANDEV_HOME_DIR, "bin");
|
||||||
export const DATA_DIR = path.join(os.homedir(), ".kandev", "data");
|
export const DATA_DIR = path.join(KANDEV_HOME_DIR, "data");
|
||||||
|
|
||||||
|
// Dev-mode root: an isolated kandev home inside the repo so that running
|
||||||
|
// `make dev` from inside a kandev-spawned task workspace does not touch the
|
||||||
|
// user's production state.
|
||||||
|
export const DEV_KANDEV_DOTDIR = ".kandev-dev";
|
||||||
|
export function devKandevHome(repoRoot: string): string {
|
||||||
|
return path.join(repoRoot, DEV_KANDEV_DOTDIR);
|
||||||
|
}
|
||||||
|
|||||||
+44
-9
@@ -1,8 +1,9 @@
|
|||||||
import { spawn } from "node:child_process";
|
import { spawn } from "node:child_process";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
|
||||||
import { HEALTH_TIMEOUT_MS_DEV } from "./constants";
|
import { devKandevHome, HEALTH_TIMEOUT_MS_DEV } from "./constants";
|
||||||
import { resolveHealthTimeoutMs, waitForHealth, waitForUrlReady } from "./health";
|
import { resolveHealthTimeoutMs, waitForHealth, waitForUrlReady } from "./health";
|
||||||
|
import { isInsideKandevTask } from "./kandev-env";
|
||||||
import { createProcessSupervisor } from "./process";
|
import { createProcessSupervisor } from "./process";
|
||||||
import {
|
import {
|
||||||
attachBackendExitHandler,
|
attachBackendExitHandler,
|
||||||
@@ -21,14 +22,7 @@ export type DevOptions = {
|
|||||||
|
|
||||||
export async function runDev({ repoRoot, backendPort, webPort }: DevOptions): Promise<void> {
|
export async function runDev({ repoRoot, backendPort, webPort }: DevOptions): Promise<void> {
|
||||||
const ports = await pickPorts(backendPort, webPort);
|
const ports = await pickPorts(backendPort, webPort);
|
||||||
const dbPath =
|
const { dbPath, extra } = resolveDevBackendEnv(repoRoot);
|
||||||
process.env.KANDEV_DATABASE_PATH || path.join(repoRoot, "apps", "backend", "kandev.db");
|
|
||||||
|
|
||||||
const extra: Record<string, string> = {
|
|
||||||
KANDEV_DATABASE_PATH: dbPath,
|
|
||||||
KANDEV_MOCK_AGENT: process.env.KANDEV_MOCK_AGENT || "true",
|
|
||||||
KANDEV_DEBUG_PPROF_ENABLED: "true",
|
|
||||||
};
|
|
||||||
const backendEnv = buildBackendEnv({ ports, extra });
|
const backendEnv = buildBackendEnv({ ports, extra });
|
||||||
const webEnv = buildWebEnv({ ports, debug: true });
|
const webEnv = buildWebEnv({ ports, debug: true });
|
||||||
const logLevel =
|
const logLevel =
|
||||||
@@ -72,3 +66,44 @@ export async function runDev({ repoRoot, backendPort, webPort }: DevOptions): Pr
|
|||||||
console.log(`[kandev] web ready at ${webUrl}`);
|
console.log(`[kandev] web ready at ${webUrl}`);
|
||||||
openBrowser(webUrl);
|
openBrowser(webUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DevBackendEnv = {
|
||||||
|
dbPath: string;
|
||||||
|
extra: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Computes the dev-mode backend env. When invoked from inside a kandev task
|
||||||
|
// workspace, force-relocates the entire kandev root to <repo>/.kandev-dev so
|
||||||
|
// dev state is isolated from the user's production ~/.kandev. In a normal
|
||||||
|
// shell, preserves the existing override-friendly behavior (honor an
|
||||||
|
// explicit KANDEV_DATABASE_PATH, otherwise default to a repo-local db).
|
||||||
|
function resolveDevBackendEnv(repoRoot: string): DevBackendEnv {
|
||||||
|
const baseExtra: Record<string, string> = {
|
||||||
|
KANDEV_MOCK_AGENT: process.env.KANDEV_MOCK_AGENT || "true",
|
||||||
|
KANDEV_DEBUG_PPROF_ENABLED: "true",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isInsideKandevTask(repoRoot)) {
|
||||||
|
const devHome = devKandevHome(repoRoot);
|
||||||
|
// Display only; the backend derives its own DB path from KANDEV_HOME_DIR
|
||||||
|
// via ResolvedDataDir(). Both resolve to the same location.
|
||||||
|
const dbPath = path.join(devHome, "data", "kandev.db");
|
||||||
|
console.log("[kandev] task workspace detected → using local dev state");
|
||||||
|
return {
|
||||||
|
dbPath,
|
||||||
|
extra: {
|
||||||
|
...baseExtra,
|
||||||
|
KANDEV_HOME_DIR: devHome,
|
||||||
|
// Clear a parent-leaked DB path so the backend uses the HomeDir-derived default.
|
||||||
|
KANDEV_DATABASE_PATH: "",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const dbPath =
|
||||||
|
process.env.KANDEV_DATABASE_PATH || path.join(repoRoot, "apps", "backend", "kandev.db");
|
||||||
|
return {
|
||||||
|
dbPath,
|
||||||
|
extra: { ...baseExtra, KANDEV_DATABASE_PATH: dbPath },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { KANDEV_TASKS_DIR } from "./constants";
|
||||||
|
import { isInsideKandevTask } from "./kandev-env";
|
||||||
|
|
||||||
|
describe("isInsideKandevTask", () => {
|
||||||
|
const originalTaskId = process.env.KANDEV_TASK_ID;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
delete process.env.KANDEV_TASK_ID;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (originalTaskId === undefined) {
|
||||||
|
delete process.env.KANDEV_TASK_ID;
|
||||||
|
} else {
|
||||||
|
process.env.KANDEV_TASK_ID = originalTaskId;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns true when KANDEV_TASK_ID is set regardless of repo path", () => {
|
||||||
|
process.env.KANDEV_TASK_ID = "some-id";
|
||||||
|
expect(isInsideKandevTask("/any/path")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns true for a repoRoot under the kandev tasks dir", () => {
|
||||||
|
const repoRoot = path.join(KANDEV_TASKS_DIR, "task-123", "kandev");
|
||||||
|
expect(isInsideKandevTask(repoRoot)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false for a repoRoot outside the kandev tasks dir", () => {
|
||||||
|
expect(isInsideKandevTask("/home/user/projects/kandev")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not match a sibling directory that shares the 'tasks' prefix", () => {
|
||||||
|
// Boundary: ~/.kandev/tasks-extra/... must NOT be treated as a task workspace.
|
||||||
|
// The `path.sep` suffix on KANDEV_TASKS_DIR is the only guard against this.
|
||||||
|
const adjacent = path.join(os.homedir(), ".kandev", "tasks-extra", "proj");
|
||||||
|
expect(isInsideKandevTask(adjacent)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not match the tasks dir itself without a subdirectory", () => {
|
||||||
|
expect(isInsideKandevTask(KANDEV_TASKS_DIR)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
import { KANDEV_TASKS_DIR } from "./constants";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true when the current process looks like it was spawned inside a
|
||||||
|
* kandev-created task workspace. Two signals:
|
||||||
|
* 1. The parent kandev backend exports KANDEV_TASK_ID into every task shell.
|
||||||
|
* 2. Task worktrees live under ~/.kandev/tasks/ (see KANDEV_TASKS_DIR).
|
||||||
|
*
|
||||||
|
* Used by dev mode to auto-isolate the backend onto a local dev root so that
|
||||||
|
* `make dev` never mutates the user's production state.
|
||||||
|
*
|
||||||
|
* Note: the path-prefix fallback is a defensive secondary signal for nested
|
||||||
|
* shells where KANDEV_TASK_ID was stripped. It is case-sensitive and does not
|
||||||
|
* resolve symlinks, so a realpath'd repoRoot may miss a symlinked HOME on
|
||||||
|
* macOS / Windows. KANDEV_TASK_ID remains the primary guarantee.
|
||||||
|
*/
|
||||||
|
export function isInsideKandevTask(repoRoot: string): boolean {
|
||||||
|
if (process.env.KANDEV_TASK_ID) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return repoRoot.startsWith(KANDEV_TASKS_DIR + path.sep);
|
||||||
|
}
|
||||||
@@ -167,12 +167,12 @@ export const backendFixture = base.extend<object, { backend: BackendContext }>({
|
|||||||
const tmpDir = fs.mkdtempSync(
|
const tmpDir = fs.mkdtempSync(
|
||||||
path.join(os.tmpdir(), `kandev-e2e-${workerInfo.workerIndex}-`),
|
path.join(os.tmpdir(), `kandev-e2e-${workerInfo.workerIndex}-`),
|
||||||
);
|
);
|
||||||
const dataDir = path.join(tmpDir, ".kandev");
|
const homeDir = path.join(tmpDir, ".kandev");
|
||||||
const dbPath = path.join(tmpDir, "kandev.db");
|
const dbPath = path.join(tmpDir, "kandev.db");
|
||||||
const worktreeBase = path.join(tmpDir, "worktrees");
|
const worktreeBase = path.join(tmpDir, "worktrees");
|
||||||
const repoCloneBase = path.join(tmpDir, "repos");
|
const repoCloneBase = path.join(tmpDir, "repos");
|
||||||
|
|
||||||
fs.mkdirSync(dataDir, { recursive: true });
|
fs.mkdirSync(homeDir, { recursive: true });
|
||||||
fs.mkdirSync(worktreeBase, { recursive: true });
|
fs.mkdirSync(worktreeBase, { recursive: true });
|
||||||
fs.mkdirSync(repoCloneBase, { recursive: true });
|
fs.mkdirSync(repoCloneBase, { recursive: true });
|
||||||
|
|
||||||
@@ -231,7 +231,7 @@ exec git "$@"
|
|||||||
KANDEV_E2E_ORIGINAL_PATH: originalPath,
|
KANDEV_E2E_ORIGINAL_PATH: originalPath,
|
||||||
KANDEV_E2E_GIT_DELAY_FILE: shimDelayFile,
|
KANDEV_E2E_GIT_DELAY_FILE: shimDelayFile,
|
||||||
HOME: tmpDir,
|
HOME: tmpDir,
|
||||||
KANDEV_DATA_DIR: dataDir,
|
KANDEV_HOME_DIR: homeDir,
|
||||||
KANDEV_SERVER_PORT: String(backendPort),
|
KANDEV_SERVER_PORT: String(backendPort),
|
||||||
KANDEV_DATABASE_PATH: dbPath,
|
KANDEV_DATABASE_PATH: dbPath,
|
||||||
KANDEV_MOCK_AGENT: "only",
|
KANDEV_MOCK_AGENT: "only",
|
||||||
|
|||||||
+4
-4
@@ -10,17 +10,17 @@ SQLite requires no external services and works out of the box.
|
|||||||
|
|
||||||
| Variable | Default | Description |
|
| Variable | Default | Description |
|
||||||
|----------|---------|-------------|
|
|----------|---------|-------------|
|
||||||
| `KANDEV_DATA_DIR` | `~/.kandev/data` | Base directory for all data (DB, worktrees, sessions, etc.) |
|
| `KANDEV_HOME_DIR` | `~/.kandev` | Kandev root directory — contains `data/` (DB), `tasks/`, `worktrees/`, `repos/`, `sessions/`, and `lsp-servers/` |
|
||||||
| `KANDEV_DATABASE_DRIVER` | `sqlite` | Database driver |
|
| `KANDEV_DATABASE_DRIVER` | `sqlite` | Database driver |
|
||||||
| `KANDEV_DATABASE_PATH` | `$KANDEV_DATA_DIR/kandev.db` | Path to the SQLite database file (override) |
|
| `KANDEV_DATABASE_PATH` | `$KANDEV_HOME_DIR/data/kandev.db` | Path to the SQLite database file (override) |
|
||||||
|
|
||||||
### Config file (`config.yaml`)
|
### Config file (`config.yaml`)
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
dataDir: "" # empty = resolve from KANDEV_DATA_DIR or ~/.kandev/data
|
homeDir: "" # empty = resolve from KANDEV_HOME_DIR or ~/.kandev
|
||||||
database:
|
database:
|
||||||
driver: sqlite
|
driver: sqlite
|
||||||
path: "" # empty = $dataDir/kandev.db
|
path: "" # empty = $homeDir/data/kandev.db
|
||||||
```
|
```
|
||||||
|
|
||||||
No additional setup is needed - the database file is created automatically on first run.
|
No additional setup is needed - the database file is created automatically on first run.
|
||||||
|
|||||||
+4
-2
@@ -63,13 +63,15 @@ docker run -p 8080:8080 \
|
|||||||
|
|
||||||
| Variable | Default | Description |
|
| Variable | Default | Description |
|
||||||
|----------|---------|-------------|
|
|----------|---------|-------------|
|
||||||
| `KANDEV_DATA_DIR` | `/data` | Kandev home directory — contains DB, worktrees, sessions, repos, and LSP servers |
|
| `KANDEV_HOME_DIR` | `/data` | Kandev home directory — contains `data/` (DB), `tasks/`, `worktrees/`, `repos/`, `sessions/`, and `lsp-servers/` |
|
||||||
| `KANDEV_DATABASE_DRIVER` | `sqlite` | Database driver (`sqlite` or `postgres`) |
|
| `KANDEV_DATABASE_DRIVER` | `sqlite` | Database driver (`sqlite` or `postgres`) |
|
||||||
| `KANDEV_DATABASE_PATH` | `$KANDEV_DATA_DIR/kandev.db` | SQLite database file path (override) |
|
| `KANDEV_DATABASE_PATH` | `$KANDEV_HOME_DIR/data/kandev.db` | SQLite database file path (override) |
|
||||||
| `KANDEV_LOG_LEVEL` | `info` | Log level: `debug`, `info`, `warn`, `error` |
|
| `KANDEV_LOG_LEVEL` | `info` | Log level: `debug`, `info`, `warn`, `error` |
|
||||||
| `KANDEV_LOGGING_FORMAT` | `text` | Log format: `text` or `json` |
|
| `KANDEV_LOGGING_FORMAT` | `text` | Log format: `text` or `json` |
|
||||||
| `KANDEV_DOCKER_ENABLED` | `false` | Enable Docker runtime for agents (see below) |
|
| `KANDEV_DOCKER_ENABLED` | `false` | Enable Docker runtime for agents (see below) |
|
||||||
|
|
||||||
|
> **Upgrading from a pre-`KANDEV_HOME_DIR` image?** The SQLite DB path moved from `/data/kandev.db` to `/data/data/kandev.db`. The backend auto-migrates the legacy `kandev.db` (plus any `-wal`/`-shm` files) on first boot — look for `Migrated SQLite database from pre-KANDEV_HOME_DIR location` in the logs. If you prefer to pin the old location instead, set `-e KANDEV_DATABASE_PATH=/data/kandev.db`. If you previously set `KANDEV_DATA_DIR`, replace it with `KANDEV_HOME_DIR`.
|
||||||
|
|
||||||
### PostgreSQL
|
### PostgreSQL
|
||||||
|
|
||||||
To use PostgreSQL instead of SQLite:
|
To use PostgreSQL instead of SQLite:
|
||||||
|
|||||||
+7
-5
@@ -85,13 +85,15 @@ Kandev reads configuration via `KANDEV_`-prefixed environment variables (Viper).
|
|||||||
| Env Var | Default | Description |
|
| Env Var | Default | Description |
|
||||||
|---------|---------|-------------|
|
|---------|---------|-------------|
|
||||||
| `KANDEV_SERVER_PORT` | `8080` | Server port (API + WebSocket + Web UI) |
|
| `KANDEV_SERVER_PORT` | `8080` | Server port (API + WebSocket + Web UI) |
|
||||||
| `KANDEV_DATA_DIR` | `/data` | Kandev home directory — contains DB, worktrees, sessions, repos, and LSP servers |
|
| `KANDEV_HOME_DIR` | `/data` | Kandev home directory — contains `data/` (DB), `tasks/`, `worktrees/`, `repos/`, `sessions/`, and `lsp-servers/` |
|
||||||
| `KANDEV_DATABASE_DRIVER` | `sqlite` | Database driver (`sqlite` or `postgres`) |
|
| `KANDEV_DATABASE_DRIVER` | `sqlite` | Database driver (`sqlite` or `postgres`) |
|
||||||
| `KANDEV_DATABASE_PATH` | `$KANDEV_DATA_DIR/kandev.db` | SQLite database file path (override) |
|
| `KANDEV_DATABASE_PATH` | `$KANDEV_HOME_DIR/data/kandev.db` | SQLite database file path (override) |
|
||||||
| `KANDEV_DOCKER_ENABLED` | `false` | Enable Docker runtime for agents (requires DinD) |
|
| `KANDEV_DOCKER_ENABLED` | `false` | Enable Docker runtime for agents (requires DinD) |
|
||||||
| `KANDEV_LOG_LEVEL` | `info` | Log level: `debug`, `info`, `warn`, `error` |
|
| `KANDEV_LOG_LEVEL` | `info` | Log level: `debug`, `info`, `warn`, `error` |
|
||||||
| `KANDEV_LOGGING_FORMAT` | auto | Log format: `json` (auto-detected in K8s) or `text` |
|
| `KANDEV_LOGGING_FORMAT` | auto | Log format: `json` (auto-detected in K8s) or `text` |
|
||||||
|
|
||||||
|
> **Upgrading from a pre-`KANDEV_HOME_DIR` deployment?** The SQLite DB path moved from `/data/kandev.db` to `/data/data/kandev.db`, and `KANDEV_DATA_DIR` is gone — point `KANDEV_HOME_DIR` at the same volume mount (`/data`) instead. (`KANDEV_WORKTREE_BASEPATH` still works as an explicit override if you want to keep worktrees outside the home dir.) The backend auto-migrates the legacy `kandev.db` (plus any `-wal`/`-shm` files) on first boot — look for `Migrated SQLite database from pre-KANDEV_HOME_DIR location` in the pod logs. If you'd rather pin the old path, set `KANDEV_DATABASE_PATH=/data/kandev.db` in the ConfigMap.
|
||||||
|
|
||||||
### PostgreSQL Settings (when `KANDEV_DATABASE_DRIVER=postgres`)
|
### PostgreSQL Settings (when `KANDEV_DATABASE_DRIVER=postgres`)
|
||||||
|
|
||||||
| Env Var | Default | Description |
|
| Env Var | Default | Description |
|
||||||
@@ -108,7 +110,7 @@ Kandev reads configuration via `KANDEV_`-prefixed environment variables (Viper).
|
|||||||
### SQLite (default)
|
### SQLite (default)
|
||||||
|
|
||||||
- Zero-config, works out of the box
|
- Zero-config, works out of the box
|
||||||
- Database stored at `/data/kandev.db` on the PV
|
- Database stored at `/data/data/kandev.db` on the PV (derived from `KANDEV_HOME_DIR=/data`)
|
||||||
- **Single replica only** (SQLite is single-writer)
|
- **Single replica only** (SQLite is single-writer)
|
||||||
- Deployment strategy is `Recreate` to prevent concurrent writes
|
- Deployment strategy is `Recreate` to prevent concurrent writes
|
||||||
- Good for small teams / personal use
|
- Good for small teams / personal use
|
||||||
@@ -135,8 +137,8 @@ When using Postgres, the PVC is still needed for worktree storage but the databa
|
|||||||
|
|
||||||
The PVC at `/data` stores:
|
The PVC at `/data` stores:
|
||||||
|
|
||||||
- **SQLite database** (`/data/kandev.db`, `/data/kandev.db-wal`, `/data/kandev.db-shm`)
|
- **SQLite database** (`/data/data/kandev.db`, `/data/data/kandev.db-wal`, `/data/data/kandev.db-shm`)
|
||||||
- **Git worktrees** (`/data/worktrees/`) for workspace isolation
|
- **Git worktrees** (`/data/worktrees/`), **tasks** (`/data/tasks/`), **repos** (`/data/repos/`), **sessions** (`/data/sessions/`), and **LSP servers** (`/data/lsp-servers/`)
|
||||||
|
|
||||||
The PVC uses `ReadWriteOnce` access mode. If your cluster requires a specific StorageClass, add it to `k8s/pvc.yaml`:
|
The PVC uses `ReadWriteOnce` access mode. If your cluster requires a specific StorageClass, add it to `k8s/pvc.yaml`:
|
||||||
|
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ Each Playwright worker spawns an isolated `kandev` backend and a dedicated `next
|
|||||||
**Backend:**
|
**Backend:**
|
||||||
1. Creates a temp directory as `HOME` with its own `.gitconfig`, data dir, and SQLite DB
|
1. Creates a temp directory as `HOME` with its own `.gitconfig`, data dir, and SQLite DB
|
||||||
2. Sets environment variables for isolation:
|
2. Sets environment variables for isolation:
|
||||||
- `KANDEV_DATA_DIR` — temp data directory
|
- `KANDEV_HOME_DIR` — temp data directory
|
||||||
- `KANDEV_SERVER_PORT` — unique port per worker (`18080 + workerIndex`)
|
- `KANDEV_SERVER_PORT` — unique port per worker (`18080 + workerIndex`)
|
||||||
- `KANDEV_DATABASE_PATH` — temp SQLite path
|
- `KANDEV_DATABASE_PATH` — temp SQLite path
|
||||||
- `KANDEV_MOCK_AGENT=only` — only loads mock agent, skips agent discovery (use `"true"` in dev mode to enable mock alongside all agents)
|
- `KANDEV_MOCK_AGENT=only` — only loads mock agent, skips agent discovery (use `"true"` in dev mode to enable mock alongside all agents)
|
||||||
|
|||||||
+5
-5
@@ -5,14 +5,14 @@ metadata:
|
|||||||
labels:
|
labels:
|
||||||
app: kandev
|
app: kandev
|
||||||
data:
|
data:
|
||||||
# Database — SQLite on the PV. Set KANDEV_DATABASE_DRIVER=postgres for production.
|
# Kandev home directory on the PV. Everything kandev persists — the DB
|
||||||
KANDEV_DATABASE_PATH: /data/kandev.db
|
# (under data/), tasks, worktrees, repos, sessions, and LSP servers — lives
|
||||||
|
# under this root. Mount the PV at /data. Set KANDEV_DATABASE_DRIVER=postgres
|
||||||
|
# for production to move the DB off the PV.
|
||||||
|
KANDEV_HOME_DIR: /data
|
||||||
|
|
||||||
# Disable Docker runtime (no Docker-in-Docker in K8s)
|
# Disable Docker runtime (no Docker-in-Docker in K8s)
|
||||||
KANDEV_DOCKER_ENABLED: "false"
|
KANDEV_DOCKER_ENABLED: "false"
|
||||||
|
|
||||||
# Worktree storage on the PV
|
|
||||||
KANDEV_WORKTREE_BASEPATH: /data/worktrees
|
|
||||||
|
|
||||||
# Logging — auto-detects K8s and uses JSON format
|
# Logging — auto-detects K8s and uses JSON format
|
||||||
KANDEV_LOG_LEVEL: info
|
KANDEV_LOG_LEVEL: info
|
||||||
|
|||||||
Reference in New Issue
Block a user