Ship as tm with install to GOPATH/bin and sandbox env overrides.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
/bin/
|
||||
.dev-home/
|
||||
*.exe
|
||||
*.test
|
||||
*.out
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
BINARY := task-master
|
||||
BINARY := tm
|
||||
CMD := ./cmd/task-master
|
||||
VERSION_PKG := task/internal/version
|
||||
GIT_HASH := $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
|
||||
LDFLAGS := -X $(VERSION_PKG).BuildVersion=$(GIT_HASH)
|
||||
|
||||
.PHONY: build run version demo
|
||||
# Default: Go workspace bin (e.g. ~/go/bin). Override with: make install PREFIX=...
|
||||
GOPATH ?= $(shell go env GOPATH)
|
||||
PREFIX ?= $(GOPATH)
|
||||
INSTALL_BIN := $(PREFIX)/bin
|
||||
DEV_HOME := $(CURDIR)/.dev-home
|
||||
|
||||
.PHONY: build run run-dev install uninstall test version demo
|
||||
|
||||
build:
|
||||
go build -ldflags "$(LDFLAGS)" -o bin/$(BINARY) $(CMD)
|
||||
@@ -12,6 +18,22 @@ build:
|
||||
run: build
|
||||
./bin/$(BINARY)
|
||||
|
||||
# Sandbox: isolated config/data under ./.dev-home (does not touch ~/.config)
|
||||
run-dev: build
|
||||
TASK_MASTER_HOME=$(DEV_HOME) ./bin/$(BINARY)
|
||||
|
||||
install: build
|
||||
install -d $(INSTALL_BIN)
|
||||
install -m 755 bin/$(BINARY) $(INSTALL_BIN)/$(BINARY)
|
||||
@echo "installed $(INSTALL_BIN)/$(BINARY)"
|
||||
|
||||
uninstall:
|
||||
rm -f $(INSTALL_BIN)/$(BINARY)
|
||||
@echo "removed $(INSTALL_BIN)/$(BINARY)"
|
||||
|
||||
test:
|
||||
go test ./...
|
||||
|
||||
version: build
|
||||
./bin/$(BINARY) version
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ func main() {
|
||||
if len(os.Args) > 1 {
|
||||
switch os.Args[1] {
|
||||
case "version", "--version", "-v":
|
||||
fmt.Printf("%s %s\n", config.AppName, version.BuildVersion)
|
||||
fmt.Printf("%s %s\n", config.CliName, version.BuildVersion)
|
||||
return
|
||||
case "help", "--help", "-h":
|
||||
printHelp()
|
||||
@@ -69,5 +69,9 @@ Usage:
|
||||
%s version Print build version
|
||||
%s help Show this help
|
||||
|
||||
`, config.AppName, config.AppName, config.AppName, config.AppName)
|
||||
Env:
|
||||
TASK_MASTER_HOME Override home for config/data (dev/demo sandbox)
|
||||
TASK_MASTER_DATA_DIR Override data directory only
|
||||
|
||||
`, config.CliName, config.CliName, config.CliName, config.CliName)
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ Set Padding 12
|
||||
Set Framerate 30
|
||||
|
||||
# Isolated config/data so the demo never touches your real ~/.config
|
||||
Env HOME "/tmp/task-master-vhs"
|
||||
Env TASK_MASTER_HOME "/tmp/task-master-vhs"
|
||||
Env TERM "xterm-256color"
|
||||
Env EDITOR "vim"
|
||||
|
||||
@@ -33,7 +33,7 @@ Sleep 5s
|
||||
Show
|
||||
|
||||
# --- launch ------------------------------------------------------------------
|
||||
Type "./bin/task-master"
|
||||
Type "./bin/tm"
|
||||
Enter
|
||||
Sleep 1s
|
||||
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()))
|
||||
|
||||
Reference in New Issue
Block a user