// Package statedir names the directory Codey keeps its per-repository state in, // and moves the one the old name left behind. // // The name lives here rather than in the two packages that write into it, // because a directory name that two packages spell independently is a name // that will one day be spelled two ways — which is exactly what the rename // from harness to Codey would have cost. package statedir import ( "fmt" "os" "path/filepath" ) // Dir is where the event log and the agent's scratch space live, relative to // the working root. Sessions and prompt history used to as well; see Chats. // LegacyDir is what it was called before the rename. const ( Dir = ".codey" LegacyDir = ".harness" ) // Migrate renames a pre-rename state directory under root, reporting whether // it moved one. It is called once at startup, before anything opens a file // inside Dir. // // A rename rather than a read-time fallback, because the event log is // append-only: leaving the old file where it is would split a repository's // history across two names and quietly break the numbers METRICS.md reports. // // Three cases are deliberately left alone. Nothing to move is the common one, // and must not create an empty directory. Both present means both builds have // run here, so the new directory is the live state and the old one is history // — merging them could only lose whichever copy it picked against. And a // plain file of that name is not our state directory at all. func Migrate(root string) (bool, error) { old := filepath.Join(root, LegacyDir) info, err := os.Lstat(old) if err != nil || !info.IsDir() { return false, nil } if _, err := os.Lstat(filepath.Join(root, Dir)); err == nil { return false, nil } if err := os.Rename(old, filepath.Join(root, Dir)); err != nil { return false, fmt.Errorf("state: moving %s to %s: %w", LegacyDir, Dir, err) } return true, nil } // Ensure creates the state directory and makes it ignore itself. The // directory lives inside the user's repository — an event log, scratch space // for the agent — and none of that belongs in their commits. // A .gitignore inside the directory that ignores everything does it without // touching the project's own .gitignore, and git honours it from any depth. func Ensure(root string) error { dir := filepath.Join(root, Dir) if err := os.MkdirAll(dir, 0o755); err != nil { return fmt.Errorf("state: %w", err) } ignore := filepath.Join(dir, ".gitignore") if _, err := os.Lstat(ignore); err == nil { return nil } if err := os.WriteFile(ignore, []byte("*\n"), 0o644); err != nil { return fmt.Errorf("state: %w", err) } return nil }