feat(v0.4.1): add seed directory checks to ctask doctor
Replaces the always-informational seed-dir block with three-state checks: INFO when the env var is unset (defaults will be used), PASS when set and the path exists, FAIL when set but the directory is missing. Only the configured-but-missing state counts as a failed check and raises the doctor exit code. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+41
-17
@@ -178,23 +178,27 @@ func runDoctor(cmd *cobra.Command, args []string) error {
|
||||
failed++
|
||||
}
|
||||
|
||||
// Informational checks (do not affect pass/fail counters).
|
||||
fmt.Println()
|
||||
fmt.Println("Seed directories (informational):")
|
||||
|
||||
seedDir := config.ResolveSeedDir()
|
||||
if info, err := os.Stat(seedDir); err == nil && info.IsDir() {
|
||||
fmt.Printf(" [INFO] General seed directory: %s (present)\n", seedDir)
|
||||
} else {
|
||||
fmt.Printf(" [INFO] General seed directory: %s (not present)\n", seedDir)
|
||||
}
|
||||
|
||||
projectSeedDir := config.ResolveProjectSeedDir()
|
||||
if info, err := os.Stat(projectSeedDir); err == nil && info.IsDir() {
|
||||
fmt.Printf(" [INFO] Project seed directory: %s (present)\n", projectSeedDir)
|
||||
} else {
|
||||
fmt.Printf(" [INFO] Project seed directory: %s (not present)\n", projectSeedDir)
|
||||
}
|
||||
// Checks 6 + 7: Seed directory configuration.
|
||||
// Three states per variable:
|
||||
// - env not set -> informational (built-in defaults will be used)
|
||||
// - env set, path exists -> pass
|
||||
// - env set, path missing -> fail
|
||||
checkSeedDir(
|
||||
"General seed directory",
|
||||
os.Getenv("CTASK_SEED_DIR"),
|
||||
config.ResolveSeedDir(),
|
||||
"CTASK_SEED_DIR",
|
||||
&passed,
|
||||
&failed,
|
||||
)
|
||||
checkSeedDir(
|
||||
"Project seed directory",
|
||||
os.Getenv("CTASK_SEED_PROJECT_DIR"),
|
||||
config.ResolveProjectSeedDir(),
|
||||
"CTASK_SEED_PROJECT_DIR",
|
||||
&passed,
|
||||
&failed,
|
||||
)
|
||||
|
||||
// Summary
|
||||
fmt.Println()
|
||||
@@ -205,3 +209,23 @@ func runDoctor(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
// when envValue is non-empty; for the "not configured" state the built-in
|
||||
// default path is not displayed).
|
||||
func checkSeedDir(label, envValue, resolved, envName string, passed, failed *int) {
|
||||
if envValue == "" {
|
||||
fmt.Printf(" [INFO] %s: not configured (using built-in defaults)\n", label)
|
||||
return
|
||||
}
|
||||
if info, err := os.Stat(resolved); err == nil && info.IsDir() {
|
||||
fmt.Printf(" [PASS] %s: %s\n", label, resolved)
|
||||
*passed++
|
||||
return
|
||||
}
|
||||
fmt.Printf(" [FAIL] %s configured but not found: %s\n", label, resolved)
|
||||
fmt.Printf(" Fix: create the directory or unset %s to use built-in defaults.\n", envName)
|
||||
*failed++
|
||||
}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// This file swaps process-global os.Stdout and env vars. Do not call
|
||||
// t.Parallel() in this file.
|
||||
|
||||
// runDoctorCapture invokes runDoctor with a given CTASK_ROOT (and optional
|
||||
// seed env vars) and returns stdout and the returned error.
|
||||
func runDoctorCapture(t *testing.T, root string, seedDir, seedProjDir string, seedSet, seedProjSet bool) (string, error) {
|
||||
t.Helper()
|
||||
|
||||
prevRoot := os.Getenv("CTASK_ROOT")
|
||||
prevSeed := os.Getenv("CTASK_SEED_DIR")
|
||||
prevSeedProj := os.Getenv("CTASK_SEED_PROJECT_DIR")
|
||||
os.Setenv("CTASK_ROOT", root)
|
||||
if seedSet {
|
||||
os.Setenv("CTASK_SEED_DIR", seedDir)
|
||||
} else {
|
||||
os.Unsetenv("CTASK_SEED_DIR")
|
||||
}
|
||||
if seedProjSet {
|
||||
os.Setenv("CTASK_SEED_PROJECT_DIR", seedProjDir)
|
||||
} else {
|
||||
os.Unsetenv("CTASK_SEED_PROJECT_DIR")
|
||||
}
|
||||
defer func() {
|
||||
if prevRoot == "" {
|
||||
os.Unsetenv("CTASK_ROOT")
|
||||
} else {
|
||||
os.Setenv("CTASK_ROOT", prevRoot)
|
||||
}
|
||||
if prevSeed == "" {
|
||||
os.Unsetenv("CTASK_SEED_DIR")
|
||||
} else {
|
||||
os.Setenv("CTASK_SEED_DIR", prevSeed)
|
||||
}
|
||||
if prevSeedProj == "" {
|
||||
os.Unsetenv("CTASK_SEED_PROJECT_DIR")
|
||||
} else {
|
||||
os.Setenv("CTASK_SEED_PROJECT_DIR", prevSeedProj)
|
||||
}
|
||||
}()
|
||||
|
||||
stdoutR, stdoutW, _ := os.Pipe()
|
||||
prevStdout := os.Stdout
|
||||
os.Stdout = stdoutW
|
||||
defer func() { os.Stdout = prevStdout }()
|
||||
|
||||
err := runDoctor(doctorCmd, nil)
|
||||
|
||||
stdoutW.Close()
|
||||
var buf bytes.Buffer
|
||||
buf.ReadFrom(stdoutR)
|
||||
return buf.String(), err
|
||||
}
|
||||
|
||||
func TestDoctorSeedDirNotConfigured(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
out, _ := runDoctorCapture(t, root, "", "", false, false)
|
||||
|
||||
if !strings.Contains(out, "[INFO] General seed directory: not configured") {
|
||||
t.Errorf("expected 'not configured' INFO line, got:\n%s", out)
|
||||
}
|
||||
if !strings.Contains(out, "[INFO] Project seed directory: not configured") {
|
||||
t.Errorf("expected project 'not configured' INFO line, got:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoctorSeedDirConfiguredAndExists(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
seedDir := t.TempDir()
|
||||
seedProjDir := t.TempDir()
|
||||
out, _ := runDoctorCapture(t, root, seedDir, seedProjDir, true, true)
|
||||
|
||||
if !strings.Contains(out, "[PASS] General seed directory: "+seedDir) {
|
||||
t.Errorf("expected PASS line for general seed dir, got:\n%s", out)
|
||||
}
|
||||
if !strings.Contains(out, "[PASS] Project seed directory: "+seedProjDir) {
|
||||
t.Errorf("expected PASS line for project seed dir, got:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoctorSeedDirConfiguredButMissingFails(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
missing := root + string(os.PathSeparator) + "no-such-seed-dir"
|
||||
out, err := runDoctorCapture(t, root, missing, "", true, false)
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("expected non-nil error (failed check), got nil")
|
||||
}
|
||||
if !strings.Contains(out, "[FAIL] General seed directory configured but not found: "+missing) {
|
||||
t.Errorf("expected FAIL line for missing seed dir, got:\n%s", out)
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -260,7 +260,7 @@ Checks:
|
||||
|
||||
Exits 0 if all checks pass, 1 if any fail. Each failure includes a concrete fix instruction.
|
||||
|
||||
v0.3 also reports the resolved general and project seed directory paths as informational `[INFO]` lines. These do not contribute to the pass/fail count -- absent seed directories are normal and supported.
|
||||
`ctask doctor` also reports seed directory status: `[INFO]` if the `CTASK_SEED_DIR` / `CTASK_SEED_PROJECT_DIR` variable is unset (built-in defaults will be used), `[PASS]` if the variable is set and the path exists, `[FAIL]` if the variable is set but the path is missing. Only the configured-but-missing state counts as a failure.
|
||||
|
||||
**Example output:**
|
||||
|
||||
@@ -270,6 +270,8 @@ v0.3 also reports the resolved general and project seed directory paths as infor
|
||||
[PASS] Status line helper found: C:\Users\Warren\AppData\Local\ctask\bin\ctask-statusline.sh
|
||||
[PASS] Claude Code status line configured
|
||||
[PASS] Workspaces found: 5 tasks (2 archived)
|
||||
[INFO] General seed directory: not configured (using built-in defaults)
|
||||
[INFO] Project seed directory: not configured (using built-in defaults)
|
||||
|
||||
5 checks passed, 0 failed
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user