feat(devops): migrate filesystem operations to io.Local abstraction

Migrate config.go:
- os.ReadFile → io.Local.Read

Migrate devops.go:
- os.Stat → io.Local.IsFile

Migrate images.go:
- os.MkdirAll → io.Local.EnsureDir
- os.Stat → io.Local.IsFile
- os.ReadFile → io.Local.Read
- os.WriteFile → io.Local.Write

Migrate test.go:
- os.ReadFile → io.Local.Read
- os.Stat → io.Local.IsFile

Migrate claude.go:
- os.Stat → io.Local.IsDir

Updated tests to reflect improved behavior:
- Manifest.Save() now creates parent directories
- hasFile() correctly returns false for directories

Part of #101 (io.Medium migration tracking issue).

Closes #107

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Snider 2026-02-02 05:00:10 +00:00
parent 68f2f658f4
commit e081869ba2
7 changed files with 52 additions and 25 deletions

View file

@ -7,6 +7,8 @@ import (
"os/exec"
"path/filepath"
"strings"
"github.com/host-uk/core/pkg/io"
)
// ClaudeOptions configures the Claude sandbox session.
@ -124,7 +126,7 @@ func (d *DevOps) CopyGHAuth(ctx context.Context) error {
}
ghConfigDir := filepath.Join(home, ".config", "gh")
if _, err := os.Stat(ghConfigDir); os.IsNotExist(err) {
if !io.Local.IsDir(ghConfigDir) {
return nil // No gh config to copy
}

View file

@ -4,6 +4,7 @@ import (
"os"
"path/filepath"
"github.com/host-uk/core/pkg/io"
"gopkg.in/yaml.v3"
)
@ -69,7 +70,7 @@ func LoadConfig() (*Config, error) {
return DefaultConfig(), nil
}
data, err := os.ReadFile(configPath)
content, err := io.Local.Read(configPath)
if err != nil {
if os.IsNotExist(err) {
return DefaultConfig(), nil
@ -78,7 +79,7 @@ func LoadConfig() (*Config, error) {
}
cfg := DefaultConfig()
if err := yaml.Unmarshal(data, cfg); err != nil {
if err := yaml.Unmarshal([]byte(content), cfg); err != nil {
return nil, err
}

View file

@ -10,6 +10,7 @@ import (
"time"
"github.com/host-uk/core/pkg/container"
"github.com/host-uk/core/pkg/io"
)
// DevOps manages the portable development environment.
@ -75,8 +76,7 @@ func (d *DevOps) IsInstalled() bool {
if err != nil {
return false
}
_, err = os.Stat(path)
return err == nil
return io.Local.IsFile(path)
}
// Install downloads and installs the dev image.

View file

@ -9,6 +9,7 @@ import (
"time"
"github.com/host-uk/core/pkg/devops/sources"
"github.com/host-uk/core/pkg/io"
)
// ImageManager handles image downloads and updates.
@ -40,7 +41,7 @@ func NewImageManager(cfg *Config) (*ImageManager, error) {
}
// Ensure images directory exists
if err := os.MkdirAll(imagesDir, 0755); err != nil {
if err := io.Local.EnsureDir(imagesDir); err != nil {
return nil, err
}
@ -86,8 +87,7 @@ func (m *ImageManager) IsInstalled() bool {
if err != nil {
return false
}
_, err = os.Stat(path)
return err == nil
return io.Local.IsFile(path)
}
// Install downloads and installs the dev image.
@ -167,7 +167,7 @@ func loadManifest(path string) (*Manifest, error) {
path: path,
}
data, err := os.ReadFile(path)
content, err := io.Local.Read(path)
if err != nil {
if os.IsNotExist(err) {
return m, nil
@ -175,7 +175,7 @@ func loadManifest(path string) (*Manifest, error) {
return nil, err
}
if err := json.Unmarshal(data, m); err != nil {
if err := json.Unmarshal([]byte(content), m); err != nil {
return nil, err
}
m.path = path
@ -189,5 +189,5 @@ func (m *Manifest) Save() error {
if err != nil {
return err
}
return os.WriteFile(m.path, data, 0644)
return io.Local.Write(m.path, string(data))
}

View file

@ -192,10 +192,13 @@ func TestManifest_Save_Good_CreatesDirs(t *testing.T) {
}
m.Images["test.img"] = ImageInfo{Version: "1.0.0"}
// Should fail because nested directories don't exist
// (Save doesn't create parent directories, it just writes to path)
// Save creates parent directories automatically via io.Local.Write
err := m.Save()
assert.Error(t, err)
assert.NoError(t, err)
// Verify file was created
_, err = os.Stat(nestedPath)
assert.NoError(t, err)
}
func TestManifest_Save_Good_Overwrite(t *testing.T) {

View file

@ -131,6 +131,6 @@ func TestHasFile_Bad_Directory(t *testing.T) {
err := os.Mkdir(subDir, 0755)
assert.NoError(t, err)
// hasFile returns true for directories too (it's just checking existence)
assert.True(t, hasFile(tmpDir, "subdir"))
// hasFile correctly returns false for directories (only true for regular files)
assert.False(t, hasFile(tmpDir, "subdir"))
}

View file

@ -4,10 +4,10 @@ import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/host-uk/core/pkg/io"
"gopkg.in/yaml.v3"
)
@ -114,13 +114,18 @@ func DetectTestCommand(projectDir string) string {
// LoadTestConfig loads .core/test.yaml.
func LoadTestConfig(projectDir string) (*TestConfig, error) {
path := filepath.Join(projectDir, ".core", "test.yaml")
data, err := os.ReadFile(path)
absPath, err := filepath.Abs(path)
if err != nil {
return nil, err
}
content, err := io.Local.Read(absPath)
if err != nil {
return nil, err
}
var cfg TestConfig
if err := yaml.Unmarshal(data, &cfg); err != nil {
if err := yaml.Unmarshal([]byte(content), &cfg); err != nil {
return nil, err
}
@ -128,12 +133,22 @@ func LoadTestConfig(projectDir string) (*TestConfig, error) {
}
func hasFile(dir, name string) bool {
_, err := os.Stat(filepath.Join(dir, name))
return err == nil
path := filepath.Join(dir, name)
absPath, err := filepath.Abs(path)
if err != nil {
return false
}
return io.Local.IsFile(absPath)
}
func hasPackageScript(projectDir, script string) bool {
data, err := os.ReadFile(filepath.Join(projectDir, "package.json"))
path := filepath.Join(projectDir, "package.json")
absPath, err := filepath.Abs(path)
if err != nil {
return false
}
content, err := io.Local.Read(absPath)
if err != nil {
return false
}
@ -141,7 +156,7 @@ func hasPackageScript(projectDir, script string) bool {
var pkg struct {
Scripts map[string]string `json:"scripts"`
}
if err := json.Unmarshal(data, &pkg); err != nil {
if err := json.Unmarshal([]byte(content), &pkg); err != nil {
return false
}
@ -150,7 +165,13 @@ func hasPackageScript(projectDir, script string) bool {
}
func hasComposerScript(projectDir, script string) bool {
data, err := os.ReadFile(filepath.Join(projectDir, "composer.json"))
path := filepath.Join(projectDir, "composer.json")
absPath, err := filepath.Abs(path)
if err != nil {
return false
}
content, err := io.Local.Read(absPath)
if err != nil {
return false
}
@ -158,7 +179,7 @@ func hasComposerScript(projectDir, script string) bool {
var pkg struct {
Scripts map[string]interface{} `json:"scripts"`
}
if err := json.Unmarshal(data, &pkg); err != nil {
if err := json.Unmarshal([]byte(content), &pkg); err != nil {
return false
}