11
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func setupEnvDir(t *testing.T) string {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
files := []string{".env", ".env.local", ".env.production"}
|
||||
for _, f := range files {
|
||||
if err := os.WriteFile(filepath.Join(dir, f), []byte("KEY=val"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
// A file that should NOT be matched.
|
||||
if err := os.WriteFile(filepath.Join(dir, "env.txt"), []byte("not env"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Skipped directory.
|
||||
nm := filepath.Join(dir, "node_modules")
|
||||
if err := os.MkdirAll(nm, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(nm, ".env"), []byte("skip"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
func TestFindEnvFiles_MatchesEnvPrefix(t *testing.T) {
|
||||
dir := setupEnvDir(t)
|
||||
found, err := FindEnvFiles(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("FindEnvFiles: %v", err)
|
||||
}
|
||||
if len(found) != 3 {
|
||||
t.Errorf("found %d files, want 3: %v", len(found), found)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindEnvFiles_SkipsNodeModules(t *testing.T) {
|
||||
dir := setupEnvDir(t)
|
||||
found, err := FindEnvFiles(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("FindEnvFiles: %v", err)
|
||||
}
|
||||
for _, f := range found {
|
||||
if filepath.Dir(f) == filepath.Join(dir, "node_modules") {
|
||||
t.Errorf("node_modules .env should be skipped, got %s", f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLockEnvFiles_SetsReadOnly(t *testing.T) {
|
||||
dir := setupEnvDir(t)
|
||||
result, err := LockEnvFiles(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("LockEnvFiles: %v", err)
|
||||
}
|
||||
if result.Total != 3 {
|
||||
t.Errorf("Total = %d, want 3", result.Total)
|
||||
}
|
||||
if len(result.Failed) != 0 {
|
||||
t.Errorf("unexpected failures: %v", result.Failed)
|
||||
}
|
||||
for _, p := range result.OK {
|
||||
info, err := os.Stat(p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if info.Mode()&0o222 != 0 {
|
||||
t.Errorf("file %s should be read-only, got mode %v", p, info.Mode())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnlockEnvFiles_SetsReadWrite(t *testing.T) {
|
||||
dir := setupEnvDir(t)
|
||||
if _, err := LockEnvFiles(dir); err != nil {
|
||||
t.Fatalf("LockEnvFiles: %v", err)
|
||||
}
|
||||
result, err := UnlockEnvFiles(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("UnlockEnvFiles: %v", err)
|
||||
}
|
||||
if len(result.Failed) != 0 {
|
||||
t.Errorf("unexpected failures: %v", result.Failed)
|
||||
}
|
||||
for _, p := range result.OK {
|
||||
info, err := os.Stat(p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if info.Mode()&0o200 == 0 {
|
||||
t.Errorf("file %s should be writable, got mode %v", p, info.Mode())
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user