diff --git a/cmd/doctor.go b/cmd/doctor.go index ef4f1e2..0795e8b 100644 --- a/cmd/doctor.go +++ b/cmd/doctor.go @@ -200,6 +200,9 @@ func runDoctor(cmd *cobra.Command, args []string) error { &failed, ) + // Check 8: CTASK_PROJECT_ROOT (v0.5). + checkProjectRoot(&passed, &failed) + // Summary fmt.Println() fmt.Printf("%d checks passed, %d failed\n", passed, failed) @@ -210,6 +213,28 @@ func runDoctor(cmd *cobra.Command, args []string) error { return nil } +// checkProjectRoot prints the three-state CTASK_PROJECT_ROOT doctor line: +// - unset -> INFO pointing at the default location under CTASK_ROOT +// - set and exists -> INFO with custom-root advisory +// - set and missing -> FAIL (increments failed counter) +func checkProjectRoot(passed, failed *int) { + _ = passed // reserved for future symmetry with checkSeedDir + envValue := os.Getenv("CTASK_PROJECT_ROOT") + if envValue == "" { + defaultProj := filepath.Join(config.ResolveRoot(), "projects") + fmt.Printf(" [INFO] CTASK_PROJECT_ROOT: not set (projects discovered under %s)\n", defaultProj) + return + } + resolved := config.ResolveProjectRoot() + if info, err := os.Stat(resolved); err == nil && info.IsDir() { + fmt.Printf(" [INFO] CTASK_PROJECT_ROOT: %s (custom -- recommended: set at user scope so all terminals can discover these workspaces)\n", resolved) + return + } + fmt.Printf(" [FAIL] CTASK_PROJECT_ROOT configured but not found: %s\n", resolved) + fmt.Printf(" Fix: create the directory or unset CTASK_PROJECT_ROOT.\n") + *failed++ +} + // checkSeedDir prints one of the three seed-directory doctor states. // envValue is the raw value of the CTASK_SEED_* variable (empty means unset). // resolved is the absolute path returned by the config resolver (used only diff --git a/cmd/doctor_test.go b/cmd/doctor_test.go index ec2bbb3..4b50c4e 100644 --- a/cmd/doctor_test.go +++ b/cmd/doctor_test.go @@ -3,6 +3,7 @@ package cmd import ( "bytes" "os" + "path/filepath" "strings" "testing" ) @@ -98,3 +99,65 @@ func TestDoctorSeedDirConfiguredButMissingFails(t *testing.T) { t.Errorf("expected FAIL line for missing seed dir, got:\n%s", out) } } + +func TestDoctorProjectRootNotSetShowsDefault(t *testing.T) { + root := t.TempDir() + prevProj := os.Getenv("CTASK_PROJECT_ROOT") + os.Unsetenv("CTASK_PROJECT_ROOT") + defer func() { + if prevProj != "" { + os.Setenv("CTASK_PROJECT_ROOT", prevProj) + } + }() + + out, _ := runDoctorCapture(t, root, "", "", false, false) + if !strings.Contains(out, "[INFO] CTASK_PROJECT_ROOT: not set") { + t.Errorf("expected 'not set' INFO line, got:\n%s", out) + } +} + +func TestDoctorProjectRootSetAndExistsShowsCustom(t *testing.T) { + root := t.TempDir() + projRoot := t.TempDir() + + prevProj := os.Getenv("CTASK_PROJECT_ROOT") + os.Setenv("CTASK_PROJECT_ROOT", projRoot) + defer func() { + if prevProj == "" { + os.Unsetenv("CTASK_PROJECT_ROOT") + } else { + os.Setenv("CTASK_PROJECT_ROOT", prevProj) + } + }() + + out, _ := runDoctorCapture(t, root, "", "", false, false) + if !strings.Contains(out, "[INFO] CTASK_PROJECT_ROOT: "+projRoot) { + t.Errorf("expected custom INFO line, got:\n%s", out) + } + if !strings.Contains(out, "user scope") { + t.Errorf("expected user-scope advice, got:\n%s", out) + } +} + +func TestDoctorProjectRootSetButMissingFails(t *testing.T) { + root := t.TempDir() + missing := filepath.Join(root, "no-such-project-root") + + prevProj := os.Getenv("CTASK_PROJECT_ROOT") + os.Setenv("CTASK_PROJECT_ROOT", missing) + defer func() { + if prevProj == "" { + os.Unsetenv("CTASK_PROJECT_ROOT") + } else { + os.Setenv("CTASK_PROJECT_ROOT", prevProj) + } + }() + + out, err := runDoctorCapture(t, root, "", "", false, false) + if err == nil { + t.Fatal("expected doctor to fail when CTASK_PROJECT_ROOT set but missing") + } + if !strings.Contains(out, "[FAIL] CTASK_PROJECT_ROOT") { + t.Errorf("expected FAIL line, got:\n%s", out) + } +}