chore(io): migrate remaining packages to io.Local abstraction
Migrate filesystem operations to use the io.Local abstraction for improved security, testability, and consistency: - pkg/cache: Replace os.ReadFile, WriteFile, Remove, RemoveAll with io.Local equivalents. io.Local.Write creates parent dirs automatically. - pkg/agentic: Migrate config.go and context.go to use io.Local for reading config files and gathering file context. - pkg/repos: Use io.Local.Read, Exists, IsDir, List for registry operations and git repo detection. - pkg/release: Use io.Local for config loading, existence checks, and artifact discovery. - pkg/devops/sources: Use io.Local.EnsureDir for CDN download. All paths are converted to absolute using filepath.Abs() before calling io.Local methods to handle relative paths correctly. Closes #104, closes #106, closes #108, closes #111 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
e81804640a
commit
d49683fd01
7 changed files with 102 additions and 64 deletions
|
|
@ -1,12 +1,12 @@
|
|||
package agentic
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/host-uk/core/pkg/errors"
|
||||
"github.com/host-uk/core/pkg/io"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
|
|
@ -95,15 +95,19 @@ func LoadConfig(dir string) (*Config, error) {
|
|||
|
||||
// loadEnvFile reads a .env file and extracts agentic configuration.
|
||||
func loadEnvFile(path string, cfg *Config) error {
|
||||
file, err := os.Open(path)
|
||||
absPath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
content, err := io.Local.Read(absPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lines := strings.Split(content, "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
// Skip empty lines and comments
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
|
|
@ -134,17 +138,22 @@ func loadEnvFile(path string, cfg *Config) error {
|
|||
}
|
||||
}
|
||||
|
||||
return scanner.Err()
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadYAMLConfig reads configuration from a YAML file.
|
||||
func loadYAMLConfig(path string, cfg *Config) error {
|
||||
data, err := os.ReadFile(path)
|
||||
absPath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return yaml.Unmarshal(data, cfg)
|
||||
content, err := io.Local.Read(absPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return yaml.Unmarshal([]byte(content), cfg)
|
||||
}
|
||||
|
||||
// applyEnvOverrides applies environment variable overrides to the config.
|
||||
|
|
@ -170,19 +179,15 @@ func SaveConfig(cfg *Config) error {
|
|||
return errors.E("agentic.SaveConfig", "failed to get home directory", err)
|
||||
}
|
||||
|
||||
configDir := filepath.Join(homeDir, ".core")
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
return errors.E("agentic.SaveConfig", "failed to create config directory", err)
|
||||
}
|
||||
|
||||
configPath := filepath.Join(configDir, configFileName)
|
||||
configPath := filepath.Join(homeDir, ".core", configFileName)
|
||||
|
||||
data, err := yaml.Marshal(cfg)
|
||||
if err != nil {
|
||||
return errors.E("agentic.SaveConfig", "failed to marshal config", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(configPath, data, 0600); err != nil {
|
||||
// io.Local.Write creates parent directories automatically
|
||||
if err := io.Local.Write(configPath, string(data)); err != nil {
|
||||
return errors.E("agentic.SaveConfig", "failed to write config file", err)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/host-uk/core/pkg/errors"
|
||||
"github.com/host-uk/core/pkg/io"
|
||||
)
|
||||
|
||||
// FileContent represents the content of a file for AI context.
|
||||
|
|
@ -95,8 +96,12 @@ func GatherRelatedFiles(task *Task, dir string) ([]FileContent, error) {
|
|||
// Read files explicitly mentioned in the task
|
||||
for _, relPath := range task.Files {
|
||||
fullPath := filepath.Join(dir, relPath)
|
||||
absPath, err := filepath.Abs(fullPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(fullPath)
|
||||
content, err := io.Local.Read(absPath)
|
||||
if err != nil {
|
||||
// Skip files that don't exist
|
||||
continue
|
||||
|
|
@ -104,7 +109,7 @@ func GatherRelatedFiles(task *Task, dir string) ([]FileContent, error) {
|
|||
|
||||
files = append(files, FileContent{
|
||||
Path: relPath,
|
||||
Content: string(content),
|
||||
Content: content,
|
||||
Language: detectLanguage(relPath),
|
||||
})
|
||||
}
|
||||
|
|
@ -154,13 +159,16 @@ func findRelatedCode(task *Task, dir string) ([]FileContent, error) {
|
|||
}
|
||||
|
||||
fullPath := filepath.Join(dir, line)
|
||||
content, err := os.ReadFile(fullPath)
|
||||
absPath, err := filepath.Abs(fullPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
contentStr, err := io.Local.Read(absPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Truncate large files
|
||||
contentStr := string(content)
|
||||
if len(contentStr) > 5000 {
|
||||
contentStr = contentStr[:5000] + "\n... (truncated)"
|
||||
}
|
||||
|
|
|
|||
32
pkg/cache/cache.go
vendored
32
pkg/cache/cache.go
vendored
|
|
@ -6,6 +6,8 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/host-uk/core/pkg/io"
|
||||
)
|
||||
|
||||
// DefaultTTL is the default cache expiry time.
|
||||
|
|
@ -40,11 +42,19 @@ func New(baseDir string, ttl time.Duration) (*Cache, error) {
|
|||
ttl = DefaultTTL
|
||||
}
|
||||
|
||||
// Ensure cache directory exists
|
||||
if err := os.MkdirAll(baseDir, 0755); err != nil {
|
||||
// Convert to absolute path for io.Local
|
||||
absBaseDir, err := filepath.Abs(baseDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Ensure cache directory exists
|
||||
if err := io.Local.EnsureDir(absBaseDir); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
baseDir = absBaseDir
|
||||
|
||||
return &Cache{
|
||||
baseDir: baseDir,
|
||||
ttl: ttl,
|
||||
|
|
@ -60,13 +70,14 @@ func (c *Cache) Path(key string) string {
|
|||
func (c *Cache) Get(key string, dest interface{}) (bool, error) {
|
||||
path := c.Path(key)
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
content, err := io.Local.Read(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
data := []byte(content)
|
||||
|
||||
var entry Entry
|
||||
if err := json.Unmarshal(data, &entry); err != nil {
|
||||
|
|
@ -91,11 +102,6 @@ func (c *Cache) Get(key string, dest interface{}) (bool, error) {
|
|||
func (c *Cache) Set(key string, data interface{}) error {
|
||||
path := c.Path(key)
|
||||
|
||||
// Ensure parent directory exists
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Marshal the data
|
||||
dataBytes, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
|
|
@ -113,13 +119,14 @@ func (c *Cache) Set(key string, data interface{}) error {
|
|||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(path, entryBytes, 0644)
|
||||
// io.Local.Write creates parent directories automatically
|
||||
return io.Local.Write(path, string(entryBytes))
|
||||
}
|
||||
|
||||
// Delete removes an item from the cache.
|
||||
func (c *Cache) Delete(key string) error {
|
||||
path := c.Path(key)
|
||||
err := os.Remove(path)
|
||||
err := io.Local.Delete(path)
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -128,17 +135,18 @@ func (c *Cache) Delete(key string) error {
|
|||
|
||||
// Clear removes all cached items.
|
||||
func (c *Cache) Clear() error {
|
||||
return os.RemoveAll(c.baseDir)
|
||||
return io.Local.DeleteAll(c.baseDir)
|
||||
}
|
||||
|
||||
// Age returns how old a cached item is, or -1 if not cached.
|
||||
func (c *Cache) Age(key string) time.Duration {
|
||||
path := c.Path(key)
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
content, err := io.Local.Read(path)
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
data := []byte(content)
|
||||
|
||||
var entry Entry
|
||||
if err := json.Unmarshal(data, &entry); err != nil {
|
||||
|
|
|
|||
|
|
@ -3,10 +3,12 @@ package sources
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
goio "io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/host-uk/core/pkg/io"
|
||||
)
|
||||
|
||||
// CDNSource downloads images from a CDN or S3 bucket.
|
||||
|
|
@ -71,7 +73,7 @@ func (s *CDNSource) Download(ctx context.Context, dest string, progress func(dow
|
|||
}
|
||||
|
||||
// Ensure dest directory exists
|
||||
if err := os.MkdirAll(dest, 0755); err != nil {
|
||||
if err := io.Local.EnsureDir(dest); err != nil {
|
||||
return fmt.Errorf("cdn.Download: %w", err)
|
||||
}
|
||||
|
||||
|
|
@ -99,7 +101,7 @@ func (s *CDNSource) Download(ctx context.Context, dest string, progress func(dow
|
|||
progress(downloaded, total)
|
||||
}
|
||||
}
|
||||
if err == io.EOF {
|
||||
if err == goio.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/host-uk/core/pkg/io"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
|
|
@ -170,8 +171,12 @@ type ChangelogConfig struct {
|
|||
// Returns an error if the file exists but cannot be parsed.
|
||||
func LoadConfig(dir string) (*Config, error) {
|
||||
configPath := filepath.Join(dir, ConfigDir, ConfigFileName)
|
||||
absPath, err := filepath.Abs(configPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("release.LoadConfig: failed to resolve path: %w", err)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(configPath)
|
||||
content, err := io.Local.Read(absPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
cfg := DefaultConfig()
|
||||
|
|
@ -182,7 +187,7 @@ func LoadConfig(dir string) (*Config, error) {
|
|||
}
|
||||
|
||||
var cfg Config
|
||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||
if err := yaml.Unmarshal([]byte(content), &cfg); err != nil {
|
||||
return nil, fmt.Errorf("release.LoadConfig: failed to parse config file: %w", err)
|
||||
}
|
||||
|
||||
|
|
@ -263,8 +268,12 @@ func ConfigPath(dir string) string {
|
|||
|
||||
// ConfigExists checks if a release config file exists in the given directory.
|
||||
func ConfigExists(dir string) bool {
|
||||
_, err := os.Stat(ConfigPath(dir))
|
||||
return err == nil
|
||||
configPath := ConfigPath(dir)
|
||||
absPath, err := filepath.Abs(configPath)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return io.Local.IsFile(absPath)
|
||||
}
|
||||
|
||||
// GetRepository returns the repository from the config.
|
||||
|
|
@ -280,11 +289,9 @@ func (c *Config) GetProjectName() string {
|
|||
// WriteConfig writes the config to the .core/release.yaml file.
|
||||
func WriteConfig(cfg *Config, dir string) error {
|
||||
configPath := ConfigPath(dir)
|
||||
|
||||
// Ensure directory exists
|
||||
configDir := filepath.Dir(configPath)
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
return fmt.Errorf("release.WriteConfig: failed to create directory: %w", err)
|
||||
absPath, err := filepath.Abs(configPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("release.WriteConfig: failed to resolve path: %w", err)
|
||||
}
|
||||
|
||||
data, err := yaml.Marshal(cfg)
|
||||
|
|
@ -292,7 +299,8 @@ func WriteConfig(cfg *Config, dir string) error {
|
|||
return fmt.Errorf("release.WriteConfig: failed to marshal config: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(configPath, data, 0644); err != nil {
|
||||
// io.Local.Write creates parent directories automatically
|
||||
if err := io.Local.Write(absPath, string(data)); err != nil {
|
||||
return fmt.Errorf("release.WriteConfig: failed to write config file: %w", err)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,12 +6,12 @@ package release
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/host-uk/core/pkg/build"
|
||||
"github.com/host-uk/core/pkg/build/builders"
|
||||
"github.com/host-uk/core/pkg/io"
|
||||
"github.com/host-uk/core/pkg/release/publishers"
|
||||
)
|
||||
|
||||
|
|
@ -103,13 +103,13 @@ func Publish(ctx context.Context, cfg *Config, dryRun bool) (*Release, error) {
|
|||
|
||||
// findArtifacts discovers pre-built artifacts in the dist directory.
|
||||
func findArtifacts(distDir string) ([]build.Artifact, error) {
|
||||
if _, err := os.Stat(distDir); os.IsNotExist(err) {
|
||||
if !io.Local.IsDir(distDir) {
|
||||
return nil, fmt.Errorf("dist/ directory not found")
|
||||
}
|
||||
|
||||
var artifacts []build.Artifact
|
||||
|
||||
entries, err := os.ReadDir(distDir)
|
||||
entries, err := io.Local.List(distDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read dist/: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/host-uk/core/pkg/io"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
|
|
@ -60,10 +61,16 @@ type Repo struct {
|
|||
|
||||
// LoadRegistry reads and parses a repos.yaml file.
|
||||
func LoadRegistry(path string) (*Registry, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
absPath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve path: %w", err)
|
||||
}
|
||||
|
||||
content, err := io.Local.Read(absPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read registry file: %w", err)
|
||||
}
|
||||
data := []byte(content)
|
||||
|
||||
var reg Registry
|
||||
if err := yaml.Unmarshal(data, ®); err != nil {
|
||||
|
|
@ -98,7 +105,7 @@ func FindRegistry() (string, error) {
|
|||
|
||||
for {
|
||||
candidate := filepath.Join(dir, "repos.yaml")
|
||||
if _, err := os.Stat(candidate); err == nil {
|
||||
if io.Local.Exists(candidate) {
|
||||
return candidate, nil
|
||||
}
|
||||
|
||||
|
|
@ -121,7 +128,7 @@ func FindRegistry() (string, error) {
|
|||
}
|
||||
|
||||
for _, p := range commonPaths {
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
if io.Local.Exists(p) {
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
|
|
@ -132,14 +139,19 @@ func FindRegistry() (string, error) {
|
|||
// ScanDirectory creates a Registry by scanning a directory for git repos.
|
||||
// This is used as a fallback when no repos.yaml is found.
|
||||
func ScanDirectory(dir string) (*Registry, error) {
|
||||
entries, err := os.ReadDir(dir)
|
||||
absDir, err := filepath.Abs(dir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve directory path: %w", err)
|
||||
}
|
||||
|
||||
entries, err := io.Local.List(absDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read directory: %w", err)
|
||||
}
|
||||
|
||||
reg := &Registry{
|
||||
Version: 1,
|
||||
BasePath: dir,
|
||||
BasePath: absDir,
|
||||
Repos: make(map[string]*Repo),
|
||||
}
|
||||
|
||||
|
|
@ -149,10 +161,10 @@ func ScanDirectory(dir string) (*Registry, error) {
|
|||
continue
|
||||
}
|
||||
|
||||
repoPath := filepath.Join(dir, entry.Name())
|
||||
repoPath := filepath.Join(absDir, entry.Name())
|
||||
gitPath := filepath.Join(repoPath, ".git")
|
||||
|
||||
if _, err := os.Stat(gitPath); err != nil {
|
||||
if !io.Local.IsDir(gitPath) {
|
||||
continue // Not a git repo
|
||||
}
|
||||
|
||||
|
|
@ -176,14 +188,11 @@ func ScanDirectory(dir string) (*Registry, error) {
|
|||
// detectOrg tries to extract the GitHub org from a repo's origin remote.
|
||||
func detectOrg(repoPath string) string {
|
||||
// Try to read git remote
|
||||
cmd := filepath.Join(repoPath, ".git", "config")
|
||||
data, err := os.ReadFile(cmd)
|
||||
configPath := filepath.Join(repoPath, ".git", "config")
|
||||
content, err := io.Local.Read(configPath)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Simple parse for github.com URLs
|
||||
content := string(data)
|
||||
// Look for patterns like github.com:org/repo or github.com/org/repo
|
||||
for _, line := range strings.Split(content, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
|
|
@ -292,15 +301,13 @@ func (r *Registry) TopologicalOrder() ([]*Repo, error) {
|
|||
|
||||
// Exists checks if the repo directory exists on disk.
|
||||
func (repo *Repo) Exists() bool {
|
||||
info, err := os.Stat(repo.Path)
|
||||
return err == nil && info.IsDir()
|
||||
return io.Local.IsDir(repo.Path)
|
||||
}
|
||||
|
||||
// IsGitRepo checks if the repo directory contains a .git folder.
|
||||
func (repo *Repo) IsGitRepo() bool {
|
||||
gitPath := filepath.Join(repo.Path, ".git")
|
||||
info, err := os.Stat(gitPath)
|
||||
return err == nil && info.IsDir()
|
||||
return io.Local.IsDir(gitPath)
|
||||
}
|
||||
|
||||
// expandPath expands ~ to home directory.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue