Ship as tm with install to GOPATH/bin and sandbox env overrides.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Skruecken
2026-10-19 13:55:54 +02:00
co-authored by Cursor
parent 2dcab9e4d8
commit f3a592f3ea
8 changed files with 110 additions and 19 deletions
+13 -7
View File
@@ -3,17 +3,23 @@ package config
import "path/filepath"
const (
AppName = "task-master"
DefaultProject = "inbox"
DefaultTheme = "default"
configFileName = "config.json"
// AppName is used for config/data directory names under XDG paths.
AppName = "task-master"
// CliName is the installed binary / command name.
CliName = "tm"
DefaultProject = "inbox"
DefaultTheme = "default"
configFileName = "config.json"
envHome = "TASK_MASTER_HOME"
envDataDir = "TASK_MASTER_DATA_DIR"
)
// Config holds application settings persisted under ~/.config/task-master.
type Config struct {
ActiveProject string `json:"activeProject"`
DataDir string `json:"dataDir"`
Theme string `json:"theme"`
ActiveProject string `json:"activeProject"`
DataDir string `json:"dataDir"`
Theme string `json:"theme"`
// ProjectLists maps project ID → comma-separated board list ids
// (e.g. "all" or "backlog,in_progress").
ProjectLists map[string]string `json:"projectLists,omitempty"`
+24 -5
View File
@@ -5,14 +5,28 @@ import (
"fmt"
"os"
"path/filepath"
"strings"
)
// ResolveHome returns the effective home for config/data paths.
// TASK_MASTER_HOME overrides the real user home (useful for demos/dev sandboxes).
func ResolveHome() (string, error) {
if h := strings.TrimSpace(os.Getenv(envHome)); h != "" {
return filepath.Clean(h), nil
}
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("resolve home directory: %w", err)
}
return home, nil
}
// LoadOrCreate loads the config from disk, creating defaults if missing.
// It also ensures DataDir and the active project directory exist.
func LoadOrCreate() (*Config, error) {
home, err := os.UserHomeDir()
home, err := ResolveHome()
if err != nil {
return nil, fmt.Errorf("resolve home directory: %w", err)
return nil, err
}
cfgPath := Path(home)
@@ -26,6 +40,10 @@ func LoadOrCreate() (*Config, error) {
return nil, err
}
if d := strings.TrimSpace(os.Getenv(envDataDir)); d != "" {
cfg.DataDir = filepath.Clean(d)
}
if err := ensureDataDirs(cfg); err != nil {
return nil, err
}
@@ -54,11 +72,12 @@ func readOrCreate(cfgPath, home string) (*Config, error) {
return &cfg, nil
}
// Save writes the config to ~/.config/task-master/config.json.
// Save writes the config to ~/.config/task-master/config.json
// (or under TASK_MASTER_HOME when set).
func Save(cfg *Config) error {
home, err := os.UserHomeDir()
home, err := ResolveHome()
if err != nil {
return fmt.Errorf("resolve home directory: %w", err)
return err
}
cfgPath := Path(home)
+39
View File
@@ -30,6 +30,8 @@ func TestDefaultsAndApplyDefaults(t *testing.T) {
func TestSaveRoundTrip(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
t.Setenv("TASK_MASTER_HOME", "")
t.Setenv("TASK_MASTER_DATA_DIR", "")
cfg := config.Defaults(home)
cfg.ActiveProject = "work"
@@ -54,6 +56,8 @@ func TestSaveRoundTrip(t *testing.T) {
func TestLoadOrCreate(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
t.Setenv("TASK_MASTER_HOME", "")
t.Setenv("TASK_MASTER_DATA_DIR", "")
cfg, err := config.LoadOrCreate()
if err != nil {
@@ -81,3 +85,38 @@ func TestLoadOrCreate(t *testing.T) {
t.Fatalf("DataDir mismatch")
}
}
func TestTASK_MASTER_HOME(t *testing.T) {
sandbox := t.TempDir()
t.Setenv("TASK_MASTER_HOME", sandbox)
t.Setenv("TASK_MASTER_DATA_DIR", "")
cfg, err := config.LoadOrCreate()
if err != nil {
t.Fatalf("LoadOrCreate: %v", err)
}
wantCfg := config.Path(sandbox)
if _, err := os.Stat(wantCfg); err != nil {
t.Fatalf("config under sandbox missing: %v", err)
}
wantData := filepath.Join(sandbox, ".local", "share", config.AppName)
if cfg.DataDir != wantData {
t.Fatalf("DataDir = %q, want %q", cfg.DataDir, wantData)
}
}
func TestTASK_MASTER_DATA_DIR(t *testing.T) {
home := t.TempDir()
data := t.TempDir()
t.Setenv("HOME", home)
t.Setenv("TASK_MASTER_HOME", "")
t.Setenv("TASK_MASTER_DATA_DIR", data)
cfg, err := config.LoadOrCreate()
if err != nil {
t.Fatalf("LoadOrCreate: %v", err)
}
if cfg.DataDir != data {
t.Fatalf("DataDir = %q, want %q", cfg.DataDir, data)
}
}
+1 -1
View File
@@ -10,7 +10,7 @@ import (
)
func (m Model) renderHeader() string {
brand := m.styles.HeaderBrand.Render(config.AppName)
brand := m.styles.HeaderBrand.Render(config.CliName)
project := m.styles.HeaderMeta.Render(fmt.Sprintf("project: %s", m.cfg.ActiveProject))
board := m.styles.FilterHint.Render(fmt.Sprintf("list: %s", m.boardFilter.Label()))
count := m.styles.HeaderMeta.Render(fmt.Sprintf("%d tickets", m.ticketCount()))