Vectrum
Sign In
69 lines · 2.6 KB · Go
// 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 sessions and the event log live, relative to the working root.
// 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 — session files, 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
}