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/
|
/bin/
|
||||||
|
.dev-home/
|
||||||
*.exe
|
*.exe
|
||||||
*.test
|
*.test
|
||||||
*.out
|
*.out
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
BINARY := task-master
|
BINARY := tm
|
||||||
CMD := ./cmd/task-master
|
CMD := ./cmd/task-master
|
||||||
VERSION_PKG := task/internal/version
|
VERSION_PKG := task/internal/version
|
||||||
GIT_HASH := $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
|
GIT_HASH := $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
|
||||||
LDFLAGS := -X $(VERSION_PKG).BuildVersion=$(GIT_HASH)
|
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:
|
build:
|
||||||
go build -ldflags "$(LDFLAGS)" -o bin/$(BINARY) $(CMD)
|
go build -ldflags "$(LDFLAGS)" -o bin/$(BINARY) $(CMD)
|
||||||
@@ -12,6 +18,22 @@ build:
|
|||||||
run: build
|
run: build
|
||||||
./bin/$(BINARY)
|
./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
|
version: build
|
||||||
./bin/$(BINARY) version
|
./bin/$(BINARY) version
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ func main() {
|
|||||||
if len(os.Args) > 1 {
|
if len(os.Args) > 1 {
|
||||||
switch os.Args[1] {
|
switch os.Args[1] {
|
||||||
case "version", "--version", "-v":
|
case "version", "--version", "-v":
|
||||||
fmt.Printf("%s %s\n", config.AppName, version.BuildVersion)
|
fmt.Printf("%s %s\n", config.CliName, version.BuildVersion)
|
||||||
return
|
return
|
||||||
case "help", "--help", "-h":
|
case "help", "--help", "-h":
|
||||||
printHelp()
|
printHelp()
|
||||||
@@ -69,5 +69,9 @@ Usage:
|
|||||||
%s version Print build version
|
%s version Print build version
|
||||||
%s help Show this help
|
%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
|
Set Framerate 30
|
||||||
|
|
||||||
# Isolated config/data so the demo never touches your real ~/.config
|
# 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 TERM "xterm-256color"
|
||||||
Env EDITOR "vim"
|
Env EDITOR "vim"
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ Sleep 5s
|
|||||||
Show
|
Show
|
||||||
|
|
||||||
# --- launch ------------------------------------------------------------------
|
# --- launch ------------------------------------------------------------------
|
||||||
Type "./bin/task-master"
|
Type "./bin/tm"
|
||||||
Enter
|
Enter
|
||||||
Sleep 1s
|
Sleep 1s
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,16 @@ package config
|
|||||||
import "path/filepath"
|
import "path/filepath"
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
// AppName is used for config/data directory names under XDG paths.
|
||||||
AppName = "task-master"
|
AppName = "task-master"
|
||||||
|
// CliName is the installed binary / command name.
|
||||||
|
CliName = "tm"
|
||||||
DefaultProject = "inbox"
|
DefaultProject = "inbox"
|
||||||
DefaultTheme = "default"
|
DefaultTheme = "default"
|
||||||
configFileName = "config.json"
|
configFileName = "config.json"
|
||||||
|
|
||||||
|
envHome = "TASK_MASTER_HOME"
|
||||||
|
envDataDir = "TASK_MASTER_DATA_DIR"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config holds application settings persisted under ~/.config/task-master.
|
// Config holds application settings persisted under ~/.config/task-master.
|
||||||
|
|||||||
@@ -5,14 +5,28 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"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.
|
// LoadOrCreate loads the config from disk, creating defaults if missing.
|
||||||
// It also ensures DataDir and the active project directory exist.
|
// It also ensures DataDir and the active project directory exist.
|
||||||
func LoadOrCreate() (*Config, error) {
|
func LoadOrCreate() (*Config, error) {
|
||||||
home, err := os.UserHomeDir()
|
home, err := ResolveHome()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("resolve home directory: %w", err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
cfgPath := Path(home)
|
cfgPath := Path(home)
|
||||||
@@ -26,6 +40,10 @@ func LoadOrCreate() (*Config, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if d := strings.TrimSpace(os.Getenv(envDataDir)); d != "" {
|
||||||
|
cfg.DataDir = filepath.Clean(d)
|
||||||
|
}
|
||||||
|
|
||||||
if err := ensureDataDirs(cfg); err != nil {
|
if err := ensureDataDirs(cfg); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -54,11 +72,12 @@ func readOrCreate(cfgPath, home string) (*Config, error) {
|
|||||||
return &cfg, nil
|
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 {
|
func Save(cfg *Config) error {
|
||||||
home, err := os.UserHomeDir()
|
home, err := ResolveHome()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("resolve home directory: %w", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
cfgPath := Path(home)
|
cfgPath := Path(home)
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ func TestDefaultsAndApplyDefaults(t *testing.T) {
|
|||||||
func TestSaveRoundTrip(t *testing.T) {
|
func TestSaveRoundTrip(t *testing.T) {
|
||||||
home := t.TempDir()
|
home := t.TempDir()
|
||||||
t.Setenv("HOME", home)
|
t.Setenv("HOME", home)
|
||||||
|
t.Setenv("TASK_MASTER_HOME", "")
|
||||||
|
t.Setenv("TASK_MASTER_DATA_DIR", "")
|
||||||
|
|
||||||
cfg := config.Defaults(home)
|
cfg := config.Defaults(home)
|
||||||
cfg.ActiveProject = "work"
|
cfg.ActiveProject = "work"
|
||||||
@@ -54,6 +56,8 @@ func TestSaveRoundTrip(t *testing.T) {
|
|||||||
func TestLoadOrCreate(t *testing.T) {
|
func TestLoadOrCreate(t *testing.T) {
|
||||||
home := t.TempDir()
|
home := t.TempDir()
|
||||||
t.Setenv("HOME", home)
|
t.Setenv("HOME", home)
|
||||||
|
t.Setenv("TASK_MASTER_HOME", "")
|
||||||
|
t.Setenv("TASK_MASTER_DATA_DIR", "")
|
||||||
|
|
||||||
cfg, err := config.LoadOrCreate()
|
cfg, err := config.LoadOrCreate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -81,3 +85,38 @@ func TestLoadOrCreate(t *testing.T) {
|
|||||||
t.Fatalf("DataDir mismatch")
|
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 {
|
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))
|
project := m.styles.HeaderMeta.Render(fmt.Sprintf("project: %s", m.cfg.ActiveProject))
|
||||||
board := m.styles.FilterHint.Render(fmt.Sprintf("list: %s", m.boardFilter.Label()))
|
board := m.styles.FilterHint.Render(fmt.Sprintf("list: %s", m.boardFilter.Label()))
|
||||||
count := m.styles.HeaderMeta.Render(fmt.Sprintf("%d tickets", m.ticketCount()))
|
count := m.styles.HeaderMeta.Render(fmt.Sprintf("%d tickets", m.ticketCount()))
|
||||||
|
|||||||
Reference in New Issue
Block a user