byob-go-cli

Paths on the Factory as a lazy field

byob-runtime-directories.2 factory-distate

Problem: every command that touches state or cache needs the resolved paths. Computing them in each command re-runs os.UserConfigDir() etc. redundantly and loses a single test injection point. A package-level global makes tests non-parallel-safe.

Idea: put Paths() on the Factory as a lazy sync.OnceValue closure, mirroring byob-config.3 (config), byob-factory-di.1 (Factory shape), and the sync-oncevalue memory. First caller pays the resolution cost; subsequent callers get the cached value. Tests override f.Paths with a struct pointing at t.TempDir() subdirectories, keeping each test hermetic (per the test-tempdir memory).

Tradeoffs: one more lazy field on the Factory. The laziness is load-bearing: it means mytool --version and mytool --help pay no filesystem cost at all (aligned with byob-config.3's cold-start argument).

Design

type Factory struct {
    // ...eager fields (IOStreams, Prompter, Logger)
    Paths func() (*paths.Paths, error)
    // ...other lazy fields
}

func New() *Factory {
    return &Factory{
        // ...
        Paths: sync.OnceValues(func() (*paths.Paths, error) {
            return paths.Resolve("mytool")
        }),
    }
}

// In a command:
p, err := f.Paths()
if err != nil { return err }
if err := paths.EnsureDir(p.Cache); err != nil { return err }
f := filepath.Join(p.Cache, "latest.json")

Tests:

func newTestFactory(t *testing.T) *Factory {
    dir := t.TempDir()
    return &Factory{
        Paths: func() (*paths.Paths, error) {
            return &paths.Paths{
                Config: filepath.Join(dir, "config"),
                Cache:  filepath.Join(dir, "cache"),
                State:  filepath.Join(dir, "state"),
            }, nil
        },
        // ...
    }
}