package files import ( "io/fs" "os" "path/filepath" "strings" ) type EnvLockResult struct { Total int OK []string Failed []struct{ Path, Reason string } } // FindEnvFiles walks workspaceRoot recursively and returns all .env* files, // skipping node_modules, .git, and .claude directories. func FindEnvFiles(workspaceRoot string) ([]string, error) { var found []string err := filepath.WalkDir(workspaceRoot, func(path string, d fs.DirEntry, err error) error { if err != nil { return err } if d.IsDir() { name := d.Name() if name == "node_modules" || name == ".git" || name == ".claude" { return filepath.SkipDir } return nil } if strings.HasPrefix(d.Name(), ".env") { found = append(found, path) } return nil }) return found, err } // LockEnvFiles sets all .env* files under workspaceRoot to read-only (0444). func LockEnvFiles(workspaceRoot string) (EnvLockResult, error) { return chmodEnvFiles(workspaceRoot, 0o444) } // UnlockEnvFiles sets all .env* files under workspaceRoot to read-write (0644). func UnlockEnvFiles(workspaceRoot string) (EnvLockResult, error) { return chmodEnvFiles(workspaceRoot, 0o644) } func chmodEnvFiles(workspaceRoot string, mode os.FileMode) (EnvLockResult, error) { paths, err := FindEnvFiles(workspaceRoot) if err != nil { return EnvLockResult{}, err } result := EnvLockResult{Total: len(paths)} for _, p := range paths { if cErr := os.Chmod(p, mode); cErr != nil { result.Failed = append(result.Failed, struct{ Path, Reason string }{Path: p, Reason: cErr.Error()}) } else { result.OK = append(result.OK, p) } } return result, nil }