ci: improve docker image and KANDEV_DATA_DIR env var (#370)
This commit is contained in:
@@ -129,6 +129,7 @@ jobs:
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=sha
|
||||
type=raw,value=latest,enable=${{ github.ref_type == 'tag' && startsWith(github.ref_name, 'v') }}
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
|
||||
+13
-7
@@ -60,9 +60,13 @@ RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
git \
|
||||
ca-certificates \
|
||||
gosu \
|
||||
tini && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Create kandev user (uid 1000) — not tied to base image's built-in node user
|
||||
RUN groupadd -r kandev && useradd -r -g kandev -u 1000 -m kandev
|
||||
|
||||
# Create app directory structure matching what the CLI expects:
|
||||
# /app/apps/backend/bin/kandev
|
||||
# /app/apps/web/.next/standalone/web/server.js
|
||||
@@ -92,20 +96,22 @@ VOLUME ["/data"]
|
||||
|
||||
# Environment defaults for containerized operation
|
||||
ENV KANDEV_NO_BROWSER=1 \
|
||||
KANDEV_DATABASE_PATH=/data/kandev.db \
|
||||
KANDEV_WORKTREE_BASEPATH=/data/worktrees \
|
||||
KANDEV_DATA_DIR=/data \
|
||||
KANDEV_DOCKER_ENABLED=false \
|
||||
HOSTNAME=0.0.0.0 \
|
||||
NODE_ENV=production
|
||||
|
||||
# Run as non-root
|
||||
RUN chown -R node:node /app /data
|
||||
USER node
|
||||
# Copy entrypoint script
|
||||
COPY docker-entrypoint.sh /usr/local/bin/
|
||||
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
||||
|
||||
# Set build-time ownership of /app and /data
|
||||
RUN chown -R kandev:kandev /app /data
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
EXPOSE 8080 3000
|
||||
|
||||
# tini as PID 1 for signal handling; CLI manages backend + web processes
|
||||
ENTRYPOINT ["tini", "--"]
|
||||
# tini as PID 1 for signal handling; entrypoint handles privilege drop
|
||||
ENTRYPOINT ["tini", "--", "docker-entrypoint.sh"]
|
||||
CMD ["kandev", "start", "--backend-port", "8080", "--web-port", "3000", "--verbose"]
|
||||
|
||||
@@ -95,6 +95,7 @@ func provideLifecycleManager(
|
||||
profileResolver,
|
||||
mcpService,
|
||||
lifecycle.ExecutorFallbackWarn,
|
||||
cfg.ResolvedDataDir(),
|
||||
log,
|
||||
)
|
||||
|
||||
|
||||
@@ -79,6 +79,7 @@ func provideGateway(
|
||||
notificationRepo notificationstore.Repository,
|
||||
taskRepo *sqliterepo.Repository,
|
||||
githubSvc *github.Service,
|
||||
dataDir string,
|
||||
) (*gateways.Gateway, *notificationservice.Service, *notificationcontroller.Controller, error) {
|
||||
gateway, err := gateways.Provide(log)
|
||||
if err != nil {
|
||||
@@ -89,7 +90,7 @@ func provideGateway(
|
||||
scriptSvc := &scriptServiceAdapter{taskSvc: taskSvc}
|
||||
if lifecycleMgr != nil {
|
||||
gateway.SetLifecycleManager(lifecycleMgr, userSvc, scriptSvc)
|
||||
gateway.SetLSPHandler(lifecycleMgr, userSvc, lspinstaller.NewRegistry(log))
|
||||
gateway.SetLSPHandler(lifecycleMgr, userSvc, lspinstaller.NewRegistry(dataDir, log))
|
||||
gateway.SetVscodeProxy(lifecycleMgr)
|
||||
}
|
||||
|
||||
|
||||
@@ -285,7 +285,7 @@ func startAgentInfrastructure(
|
||||
// ============================================
|
||||
repoCloner := repoclone.NewCloner(repoclone.Config{
|
||||
BasePath: cfg.RepoClone.BasePath,
|
||||
}, repoclone.DetectGitProtocol(), log)
|
||||
}, repoclone.DetectGitProtocol(), cfg.ResolvedDataDir(), log)
|
||||
log.Info("Repository cloner configured",
|
||||
zap.String("base_path", cfg.RepoClone.BasePath))
|
||||
|
||||
@@ -341,6 +341,7 @@ func startGatewayAndServe(
|
||||
ctx, log, eventBus, services.Task, services.User,
|
||||
orchestratorSvc, lifecycleMgr, agentRegistry,
|
||||
repos.Notification, repos.Task, services.GitHub,
|
||||
cfg.ResolvedDataDir(),
|
||||
)
|
||||
if err != nil {
|
||||
log.Error("Failed to initialize WebSocket gateway", zap.Error(err))
|
||||
|
||||
@@ -2,8 +2,6 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
analyticsrepository "github.com/kandev/kandev/internal/analytics/repository"
|
||||
"github.com/kandev/kandev/internal/common/config"
|
||||
@@ -89,11 +87,7 @@ func provideRepositories(cfg *config.Config, log *logger.Logger) (*db.Pool, *Rep
|
||||
cleanups = append(cleanups, cleanup)
|
||||
|
||||
// Initialize master key and secrets store
|
||||
kandevDir := os.Getenv("KANDEV_DATA_DIR")
|
||||
if kandevDir == "" {
|
||||
kandevDir = filepath.Join(os.Getenv("HOME"), ".kandev")
|
||||
}
|
||||
masterKeyProvider, err := secrets.NewMasterKeyProvider(kandevDir)
|
||||
masterKeyProvider, err := secrets.NewMasterKeyProvider(cfg.ResolvedDataDir())
|
||||
if err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("master key: %w", err)
|
||||
}
|
||||
|
||||
@@ -184,7 +184,7 @@ func newTestManager() *lifecycle.Manager {
|
||||
credsMgr := &MockCredentialsManager{}
|
||||
profileResolver := &MockProfileResolver{}
|
||||
// Pass nil for runtime - tests don't need them
|
||||
return lifecycle.NewManager(reg, eventBus, nil, credsMgr, profileResolver, nil, lifecycle.ExecutorFallbackWarn, log)
|
||||
return lifecycle.NewManager(reg, eventBus, nil, credsMgr, profileResolver, nil, lifecycle.ExecutorFallbackWarn, "", log)
|
||||
}
|
||||
|
||||
func TestWsShellStatus_NoInstanceFound(t *testing.T) {
|
||||
|
||||
@@ -269,7 +269,7 @@ func TestDockerExecutor_Close_AfterFailedInit(t *testing.T) {
|
||||
|
||||
func TestDockerClientProvider_NilRegistry(t *testing.T) {
|
||||
log := newTestDockerLogger()
|
||||
mgr := NewManager(nil, nil, nil, nil, nil, nil, ExecutorFallbackWarn, log)
|
||||
mgr := NewManager(nil, nil, nil, nil, nil, nil, ExecutorFallbackWarn, "", log)
|
||||
|
||||
provider := mgr.DockerClientProvider()
|
||||
if provider == nil {
|
||||
@@ -284,7 +284,7 @@ func TestDockerClientProvider_NoDockerExecutor(t *testing.T) {
|
||||
log := newTestDockerLogger()
|
||||
registry := NewExecutorRegistry(log)
|
||||
registry.Register(&MockExecutor{name: executor.NameStandalone})
|
||||
mgr := NewManager(nil, nil, registry, nil, nil, nil, ExecutorFallbackWarn, log)
|
||||
mgr := NewManager(nil, nil, registry, nil, nil, nil, ExecutorFallbackWarn, "", log)
|
||||
|
||||
provider := mgr.DockerClientProvider()
|
||||
if client := provider(); client != nil {
|
||||
@@ -298,7 +298,7 @@ func TestDockerClientProvider_WithDockerExecutor(t *testing.T) {
|
||||
dockerExec := NewDockerExecutor(config.DockerConfig{}, log)
|
||||
dockerExec.newClientFunc = failingClientFactory("docker unavailable")
|
||||
registry.Register(dockerExec)
|
||||
mgr := NewManager(nil, nil, registry, nil, nil, nil, ExecutorFallbackWarn, log)
|
||||
mgr := NewManager(nil, nil, registry, nil, nil, nil, ExecutorFallbackWarn, "", log)
|
||||
|
||||
provider := mgr.DockerClientProvider()
|
||||
if client := provider(); client != nil {
|
||||
@@ -311,7 +311,7 @@ func TestDockerClientProvider_WithWorkingDocker(t *testing.T) {
|
||||
registry := NewExecutorRegistry(log)
|
||||
dockerExec := NewDockerExecutor(config.DockerConfig{}, log)
|
||||
registry.Register(dockerExec)
|
||||
mgr := NewManager(nil, nil, registry, nil, nil, nil, ExecutorFallbackWarn, log)
|
||||
mgr := NewManager(nil, nil, registry, nil, nil, nil, ExecutorFallbackWarn, "", log)
|
||||
|
||||
provider := mgr.DockerClientProvider()
|
||||
client := provider()
|
||||
|
||||
@@ -86,6 +86,7 @@ func NewManager(
|
||||
profileResolver ProfileResolver,
|
||||
mcpProvider McpConfigProvider,
|
||||
fallbackPolicy ExecutorFallbackPolicy,
|
||||
dataDir string,
|
||||
log *logger.Logger,
|
||||
) *Manager {
|
||||
componentLogger := log.WithFields(zap.String("component", "lifecycle-manager"))
|
||||
@@ -106,7 +107,7 @@ func NewManager(
|
||||
executionStore := NewExecutionStore()
|
||||
|
||||
// Initialize session history manager for fork_session pattern (context injection)
|
||||
historyManager, err := NewSessionHistoryManager("", log)
|
||||
historyManager, err := NewSessionHistoryManager("", dataDir, log)
|
||||
if err != nil {
|
||||
log.Warn("failed to create session history manager, context injection disabled", zap.Error(err))
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ func createTestManagerWithTracking() (*Manager, *MockEventBusWithTracking) {
|
||||
eventBus := &MockEventBusWithTracking{}
|
||||
credsMgr := &MockCredentialsManager{}
|
||||
profileResolver := &MockProfileResolver{}
|
||||
mgr := NewManager(reg, eventBus, nil, credsMgr, profileResolver, nil, ExecutorFallbackWarn, log)
|
||||
mgr := NewManager(reg, eventBus, nil, credsMgr, profileResolver, nil, ExecutorFallbackWarn, "", log)
|
||||
return mgr, eventBus
|
||||
}
|
||||
|
||||
|
||||
@@ -208,7 +208,7 @@ func newTestManager() *Manager {
|
||||
credsMgr := &MockCredentialsManager{}
|
||||
profileResolver := &MockProfileResolver{}
|
||||
// Pass nil for runtime - tests don't need them
|
||||
return NewManager(reg, eventBus, nil, credsMgr, profileResolver, nil, ExecutorFallbackWarn, log)
|
||||
return NewManager(reg, eventBus, nil, credsMgr, profileResolver, nil, ExecutorFallbackWarn, "", log)
|
||||
}
|
||||
|
||||
func TestNewManager(t *testing.T) {
|
||||
|
||||
@@ -49,14 +49,10 @@ type HistoryEntry struct {
|
||||
|
||||
// NewSessionHistoryManager creates a new SessionHistoryManager.
|
||||
// The baseDir is the directory where session history files will be stored.
|
||||
// If baseDir is empty, defaults to ~/.kandev/sessions/
|
||||
func NewSessionHistoryManager(baseDir string, log *logger.Logger) (*SessionHistoryManager, error) {
|
||||
// If baseDir is empty, it defaults to dataDir+"/sessions".
|
||||
func NewSessionHistoryManager(baseDir string, dataDir string, log *logger.Logger) (*SessionHistoryManager, error) {
|
||||
if baseDir == "" {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get home directory: %w", err)
|
||||
}
|
||||
baseDir = filepath.Join(homeDir, ".kandev", "sessions")
|
||||
baseDir = filepath.Join(dataDir, "sessions")
|
||||
}
|
||||
|
||||
// Ensure directory exists
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
|
||||
// Config holds all configuration sections for Kandev.
|
||||
type Config struct {
|
||||
DataDir string `mapstructure:"dataDir"`
|
||||
Server ServerConfig `mapstructure:"server"`
|
||||
Database DatabaseConfig `mapstructure:"database"`
|
||||
NATS NATSConfig `mapstructure:"nats"`
|
||||
@@ -28,6 +29,25 @@ type Config struct {
|
||||
RepoClone RepoCloneConfig `mapstructure:"repoClone"`
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (c *Config) ResolvedDataDir() 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")
|
||||
}
|
||||
|
||||
// ServerConfig holds HTTP server configuration.
|
||||
type ServerConfig struct {
|
||||
Host string `mapstructure:"host"`
|
||||
@@ -176,9 +196,12 @@ func setDefaults(v *viper.Viper) {
|
||||
v.SetDefault("server.readTimeout", 30)
|
||||
v.SetDefault("server.writeTimeout", 30)
|
||||
|
||||
// DataDir default — empty means resolve from KANDEV_DATA_DIR env or ~/.kandev
|
||||
v.SetDefault("dataDir", "")
|
||||
|
||||
// Database defaults
|
||||
v.SetDefault("database.driver", "sqlite")
|
||||
v.SetDefault("database.path", "./kandev.db")
|
||||
v.SetDefault("database.path", "")
|
||||
v.SetDefault("database.host", "localhost")
|
||||
v.SetDefault("database.port", 5432)
|
||||
v.SetDefault("database.user", "kandev")
|
||||
@@ -227,12 +250,12 @@ func setDefaults(v *viper.Viper) {
|
||||
|
||||
// Worktree defaults
|
||||
v.SetDefault("worktree.enabled", true)
|
||||
v.SetDefault("worktree.basePath", "~/.kandev/worktrees")
|
||||
v.SetDefault("worktree.basePath", "")
|
||||
v.SetDefault("worktree.defaultBranch", "main")
|
||||
v.SetDefault("worktree.cleanupOnRemove", true)
|
||||
|
||||
// RepoClone defaults
|
||||
v.SetDefault("repoClone.basePath", "~/.kandev/repos")
|
||||
v.SetDefault("repoClone.basePath", "")
|
||||
}
|
||||
|
||||
// DefaultDockerHost returns the platform-appropriate Docker socket path.
|
||||
@@ -285,6 +308,7 @@ func LoadWithPath(configPath string) (*Config, error) {
|
||||
_ = v.BindEnv("agent.standaloneHost", "KANDEV_AGENT_STANDALONE_HOST")
|
||||
_ = v.BindEnv("agent.mcpServerPort", "KANDEV_AGENT_MCP_SERVER_PORT")
|
||||
_ = v.BindEnv("agent.mcpServerUrl", "KANDEV_AGENT_MCP_SERVER_URL")
|
||||
_ = v.BindEnv("dataDir", "KANDEV_DATA_DIR")
|
||||
_ = v.BindEnv("logging.level", "KANDEV_LOG_LEVEL")
|
||||
_ = v.BindEnv("events.namespace", "KANDEV_EVENTS_NAMESPACE")
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestResolvedDataDir_ExplicitConfig(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{}
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
t.Skip("cannot determine home directory")
|
||||
}
|
||||
want := filepath.Join(home, ".kandev")
|
||||
if got := cfg.ResolvedDataDir(); got != want {
|
||||
t.Errorf("ResolvedDataDir() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolvedDataDir_ConfigTakesPrecedence(t *testing.T) {
|
||||
t.Setenv("KANDEV_DATA_DIR", "/env/data")
|
||||
// Even with env set, explicit Config.DataDir wins
|
||||
cfg := &Config{DataDir: "/explicit/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 {
|
||||
t.Errorf("ResolvedDataDir() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
@@ -68,13 +68,21 @@ type Registry struct {
|
||||
}
|
||||
|
||||
// NewRegistry creates a new installer registry.
|
||||
func NewRegistry(log *logger.Logger) *Registry {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
home = "."
|
||||
// If dataDir is non-empty, LSP binaries are stored under dataDir+"/lsp-servers".
|
||||
// Otherwise falls back to ~/.kandev/lsp-servers.
|
||||
func NewRegistry(dataDir string, log *logger.Logger) *Registry {
|
||||
var binDir string
|
||||
if dataDir != "" {
|
||||
binDir = filepath.Join(dataDir, "lsp-servers")
|
||||
} else {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
home = "."
|
||||
}
|
||||
binDir = filepath.Join(home, DefaultBinDir)
|
||||
}
|
||||
return &Registry{
|
||||
binDir: filepath.Join(home, DefaultBinDir),
|
||||
binDir: binDir,
|
||||
logger: log.WithFields(zap.String("component", "lsp-installer")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ func TestBinaryName(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestStrategyFor(t *testing.T) {
|
||||
r := NewRegistry(testLogger())
|
||||
r := NewRegistry("", testLogger())
|
||||
|
||||
// Supported languages should return a strategy
|
||||
for _, lang := range []string{"typescript", "go", "rust", "python"} {
|
||||
@@ -133,7 +133,7 @@ func TestStrategyFor(t *testing.T) {
|
||||
|
||||
func TestBinaryPath_InPATH(t *testing.T) {
|
||||
// "ls" should always be in PATH
|
||||
r := NewRegistry(testLogger())
|
||||
r := NewRegistry("", testLogger())
|
||||
|
||||
// Override the languages map temporarily — we can't do that directly,
|
||||
// so we test that BinaryPath returns an error for a language whose binary
|
||||
@@ -217,7 +217,7 @@ func TestBinaryPath_NotFound(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestBinaryPath_UnsupportedLanguage(t *testing.T) {
|
||||
r := NewRegistry(testLogger())
|
||||
r := NewRegistry("", testLogger())
|
||||
_, err := r.BinaryPath("java")
|
||||
if err == nil {
|
||||
t.Error("BinaryPath(\"java\") should return error for unsupported language")
|
||||
|
||||
@@ -2,6 +2,7 @@ package persistence
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"go.uber.org/zap"
|
||||
@@ -34,7 +35,7 @@ func Provide(cfg *config.Config, log *logger.Logger) (*db.Pool, func() error, er
|
||||
func provideSQLite(cfg *config.Config, log *logger.Logger) (*db.Pool, func() error, error) {
|
||||
dbPath := cfg.Database.Path
|
||||
if dbPath == "" {
|
||||
dbPath = "./kandev.db"
|
||||
dbPath = filepath.Join(cfg.ResolvedDataDir(), "kandev.db")
|
||||
}
|
||||
|
||||
// Writer: single connection, owns WAL/journal_mode setup.
|
||||
|
||||
@@ -33,10 +33,11 @@ type Cloner struct {
|
||||
repoMus sync.Map
|
||||
}
|
||||
|
||||
// NewCloner creates a new Cloner with the given config and git protocol.
|
||||
func NewCloner(cfg Config, protocol string, log *logger.Logger) *Cloner {
|
||||
if cfg.BasePath == "" {
|
||||
cfg.BasePath = "~/.kandev/repos"
|
||||
// NewCloner creates a new Cloner with the given config, git protocol, and data directory.
|
||||
// If cfg.BasePath is empty, it defaults to dataDir+"/repos".
|
||||
func NewCloner(cfg Config, protocol string, dataDir string, log *logger.Logger) *Cloner {
|
||||
if cfg.BasePath == "" && dataDir != "" {
|
||||
cfg.BasePath = filepath.Join(dataDir, "repos")
|
||||
}
|
||||
return &Cloner{config: cfg, protocol: protocol, logger: log}
|
||||
}
|
||||
|
||||
@@ -462,7 +462,7 @@ func getExecutionStore(t *testing.T, mgr *lifecycle.Manager) *lifecycle.Executio
|
||||
func newLifecycleManager(t *testing.T, log *logger.Logger) *lifecycle.Manager {
|
||||
t.Helper()
|
||||
reg := registry.NewRegistry(log)
|
||||
return lifecycle.NewManager(reg, nil, nil, nil, nil, nil, lifecycle.ExecutorFallbackWarn, log)
|
||||
return lifecycle.NewManager(reg, nil, nil, nil, nil, nil, lifecycle.ExecutorFallbackWarn, "", log)
|
||||
}
|
||||
|
||||
func newAgentctlClient(t *testing.T, serverURL string, log *logger.Logger) *agentctlclient.Client {
|
||||
|
||||
@@ -33,12 +33,16 @@ func (c *Config) Validate() error {
|
||||
if c.BranchPrefix == "" {
|
||||
c.BranchPrefix = DefaultBranchPrefix
|
||||
}
|
||||
if c.BasePath == "" {
|
||||
c.BasePath = "~/.kandev/worktrees"
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetDataDirFallback sets the BasePath from the data directory if not already configured.
|
||||
func (c *Config) SetDataDirFallback(dataDir string) {
|
||||
if c.BasePath == "" && dataDir != "" {
|
||||
c.BasePath = filepath.Join(dataDir, "worktrees")
|
||||
}
|
||||
}
|
||||
|
||||
// ExpandedBasePath returns the base path with ~ expanded to the user's home directory.
|
||||
func (c *Config) ExpandedBasePath() (string, error) {
|
||||
path := c.BasePath
|
||||
|
||||
@@ -13,11 +13,13 @@ func Provide(writer, reader *sqlx.DB, cfg *config.Config, log *logger.Logger) (*
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
manager, err := NewManager(Config{
|
||||
wtCfg := Config{
|
||||
Enabled: cfg.Worktree.Enabled,
|
||||
BasePath: cfg.Worktree.BasePath,
|
||||
BranchPrefix: "kandev/",
|
||||
}, store, log)
|
||||
}
|
||||
wtCfg.SetDataDirFallback(cfg.ResolvedDataDir())
|
||||
manager, err := NewManager(wtCfg, store, log)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
if [ "$(id -u)" = '0' ]; then
|
||||
chown -R kandev:kandev /data
|
||||
exec gosu kandev "$@"
|
||||
fi
|
||||
|
||||
exec "$@"
|
||||
+4
-2
@@ -10,15 +10,17 @@ SQLite requires no external services and works out of the box.
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `KANDEV_DATA_DIR` | `~/.kandev` | Base directory for all data (DB, worktrees, sessions, etc.) |
|
||||
| `KANDEV_DATABASE_DRIVER` | `sqlite` | Database driver |
|
||||
| `KANDEV_DATABASE_PATH` | `./kandev.db` | Path to the SQLite database file |
|
||||
| `KANDEV_DATABASE_PATH` | `$KANDEV_DATA_DIR/kandev.db` | Path to the SQLite database file (override) |
|
||||
|
||||
### Config file (`config.yaml`)
|
||||
|
||||
```yaml
|
||||
dataDir: "" # empty = resolve from KANDEV_DATA_DIR or ~/.kandev
|
||||
database:
|
||||
driver: sqlite
|
||||
path: ./kandev.db
|
||||
path: "" # empty = $dataDir/kandev.db
|
||||
```
|
||||
|
||||
No additional setup is needed - the database file is created automatically on first run.
|
||||
|
||||
+2
-2
@@ -63,12 +63,12 @@ docker run -p 8080:8080 -p 3000:3000 \
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `KANDEV_DATA_DIR` | `/data` | Base directory for all data (DB, worktrees, sessions, etc.) |
|
||||
| `KANDEV_DATABASE_DRIVER` | `sqlite` | Database driver (`sqlite` or `postgres`) |
|
||||
| `KANDEV_DATABASE_PATH` | `/data/kandev.db` | SQLite database file path |
|
||||
| `KANDEV_DATABASE_PATH` | `$KANDEV_DATA_DIR/kandev.db` | SQLite database file path (override) |
|
||||
| `KANDEV_LOG_LEVEL` | `info` | Log level: `debug`, `info`, `warn`, `error` |
|
||||
| `KANDEV_LOGGING_FORMAT` | `text` | Log format: `text` or `json` |
|
||||
| `KANDEV_DOCKER_ENABLED` | `false` | Enable Docker runtime for agents (see below) |
|
||||
| `KANDEV_WORKTREE_BASEPATH` | `/data/worktrees` | Git worktree storage directory |
|
||||
|
||||
### PostgreSQL
|
||||
|
||||
|
||||
+2
-2
@@ -81,12 +81,12 @@ Kandev reads configuration via `KANDEV_`-prefixed environment variables (Viper).
|
||||
| Env Var | Default | Description |
|
||||
|---------|---------|-------------|
|
||||
| `KANDEV_SERVER_PORT` | `8080` | Backend API port |
|
||||
| `KANDEV_DATA_DIR` | `/data` | Base directory for all data (DB, worktrees, sessions, etc.) |
|
||||
| `KANDEV_DATABASE_DRIVER` | `sqlite` | Database driver (`sqlite` or `postgres`) |
|
||||
| `KANDEV_DATABASE_PATH` | `/data/kandev.db` | SQLite database file path |
|
||||
| `KANDEV_DATABASE_PATH` | `$KANDEV_DATA_DIR/kandev.db` | SQLite database file path (override) |
|
||||
| `KANDEV_DOCKER_ENABLED` | `false` | Enable Docker runtime for agents (requires DinD) |
|
||||
| `KANDEV_LOG_LEVEL` | `info` | Log level: `debug`, `info`, `warn`, `error` |
|
||||
| `KANDEV_LOGGING_FORMAT` | auto | Log format: `json` (auto-detected in K8s) or `text` |
|
||||
| `KANDEV_WORKTREE_BASEPATH` | `/data/worktrees` | Git worktree storage directory |
|
||||
|
||||
### PostgreSQL Settings (when `KANDEV_DATABASE_DRIVER=postgres`)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user