From d0c3874c1172c271d31c7bd0513e1023cb48d0a9 Mon Sep 17 00:00:00 2001 From: Snider Date: Mon, 2 Feb 2026 00:52:29 +0000 Subject: [PATCH] chore(io): migrate internal/cmd/docs and internal/cmd/dev to Medium - internal/cmd/docs: Replace os.Stat, os.ReadFile, os.WriteFile, os.MkdirAll, os.RemoveAll with io.Local equivalents - internal/cmd/dev: Replace os.Stat, os.ReadFile, os.WriteFile, os.MkdirAll, os.ReadDir with io.Local equivalents - Fix local.Medium to allow absolute paths when root is "/" for full filesystem access (io.Local use case) Refs #113, #114 Co-Authored-By: Claude Opus 4.5 --- internal/cmd/dev/cmd_apply.go | 5 +++-- internal/cmd/dev/cmd_workflow.go | 16 ++++++++-------- internal/cmd/docs/cmd_scan.go | 15 ++++++--------- internal/cmd/docs/cmd_sync.go | 8 ++++---- pkg/io/local/client.go | 14 +++++++++++--- 5 files changed, 32 insertions(+), 26 deletions(-) diff --git a/internal/cmd/dev/cmd_apply.go b/internal/cmd/dev/cmd_apply.go index 25a9646f..7f955f7c 100644 --- a/internal/cmd/dev/cmd_apply.go +++ b/internal/cmd/dev/cmd_apply.go @@ -18,6 +18,7 @@ import ( "github.com/host-uk/core/pkg/errors" "github.com/host-uk/core/pkg/git" "github.com/host-uk/core/pkg/i18n" + "github.com/host-uk/core/pkg/io" "github.com/host-uk/core/pkg/repos" ) @@ -76,8 +77,8 @@ func runApply() error { // Validate script exists if applyScript != "" { - if _, err := os.Stat(applyScript); err != nil { - return errors.E("dev.apply", "script not found: "+applyScript, err) + if !io.Local.Exists(applyScript) { + return errors.E("dev.apply", "script not found: "+applyScript, nil) } } diff --git a/internal/cmd/dev/cmd_workflow.go b/internal/cmd/dev/cmd_workflow.go index 354f9387..4f4ca263 100644 --- a/internal/cmd/dev/cmd_workflow.go +++ b/internal/cmd/dev/cmd_workflow.go @@ -1,13 +1,13 @@ package dev import ( - "os" "path/filepath" "sort" "strings" "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/i18n" + "github.com/host-uk/core/pkg/io" ) // Workflow command flags @@ -156,7 +156,7 @@ func runWorkflowSync(registryPath string, workflowFile string, dryRun bool) erro } // Read template content - templateContent, err := os.ReadFile(templatePath) + templateContent, err := io.Local.Read(templatePath) if err != nil { return cli.Wrap(err, i18n.T("cmd.dev.workflow.read_template_error")) } @@ -189,8 +189,8 @@ func runWorkflowSync(registryPath string, workflowFile string, dryRun bool) erro destPath := filepath.Join(destDir, workflowFile) // Check if workflow already exists and is identical - if existingContent, err := os.ReadFile(destPath); err == nil { - if string(existingContent) == string(templateContent) { + if existingContent, err := io.Local.Read(destPath); err == nil { + if existingContent == templateContent { cli.Print(" %s %s %s\n", dimStyle.Render("-"), repoNameStyle.Render(repo.Name), @@ -210,7 +210,7 @@ func runWorkflowSync(registryPath string, workflowFile string, dryRun bool) erro } // Create .github/workflows directory if needed - if err := os.MkdirAll(destDir, 0755); err != nil { + if err := io.Local.EnsureDir(destDir); err != nil { cli.Print(" %s %s %s\n", errorStyle.Render(cli.Glyph(":cross:")), repoNameStyle.Render(repo.Name), @@ -220,7 +220,7 @@ func runWorkflowSync(registryPath string, workflowFile string, dryRun bool) erro } // Write workflow file - if err := os.WriteFile(destPath, templateContent, 0644); err != nil { + if err := io.Local.Write(destPath, templateContent); err != nil { cli.Print(" %s %s %s\n", errorStyle.Render(cli.Glyph(":cross:")), repoNameStyle.Render(repo.Name), @@ -264,7 +264,7 @@ func findWorkflows(dir string) []string { workflowsDir = dir } - entries, err := os.ReadDir(workflowsDir) + entries, err := io.Local.List(workflowsDir) if err != nil { return nil } @@ -298,7 +298,7 @@ func findTemplateWorkflow(registryDir, workflowFile string) string { } for _, candidate := range candidates { - if _, err := os.Stat(candidate); err == nil { + if io.Local.Exists(candidate) { return candidate } } diff --git a/internal/cmd/docs/cmd_scan.go b/internal/cmd/docs/cmd_scan.go index 8257c944..99244072 100644 --- a/internal/cmd/docs/cmd_scan.go +++ b/internal/cmd/docs/cmd_scan.go @@ -9,6 +9,7 @@ import ( "github.com/host-uk/core/internal/cmd/workspace" "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/i18n" + "github.com/host-uk/core/pkg/io" "github.com/host-uk/core/pkg/repos" ) @@ -93,28 +94,28 @@ func scanRepoDocs(repo *repos.Repo) RepoDocInfo { // Check for README.md readme := filepath.Join(repo.Path, "README.md") - if _, err := os.Stat(readme); err == nil { + if io.Local.Exists(readme) { info.Readme = readme info.HasDocs = true } // Check for CLAUDE.md claudeMd := filepath.Join(repo.Path, "CLAUDE.md") - if _, err := os.Stat(claudeMd); err == nil { + if io.Local.Exists(claudeMd) { info.ClaudeMd = claudeMd info.HasDocs = true } // Check for CHANGELOG.md changelog := filepath.Join(repo.Path, "CHANGELOG.md") - if _, err := os.Stat(changelog); err == nil { + if io.Local.Exists(changelog) { info.Changelog = changelog info.HasDocs = true } // Recursively scan docs/ directory for .md files docsDir := filepath.Join(repo.Path, "docs") - if _, err := os.Stat(docsDir); err == nil { + if io.Local.IsDir(docsDir) { filepath.WalkDir(docsDir, func(path string, d fs.DirEntry, err error) error { if err != nil { return nil @@ -139,9 +140,5 @@ func scanRepoDocs(repo *repos.Repo) RepoDocInfo { } func copyFile(src, dst string) error { - data, err := os.ReadFile(src) - if err != nil { - return err - } - return os.WriteFile(dst, data, 0644) + return io.Copy(io.Local, src, io.Local, dst) } diff --git a/internal/cmd/docs/cmd_sync.go b/internal/cmd/docs/cmd_sync.go index b4ce4888..79bdac02 100644 --- a/internal/cmd/docs/cmd_sync.go +++ b/internal/cmd/docs/cmd_sync.go @@ -1,12 +1,12 @@ package docs import ( - "os" "path/filepath" "strings" "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/i18n" + "github.com/host-uk/core/pkg/io" ) // Flag variables for sync command @@ -127,9 +127,9 @@ func runDocsSync(registryPath string, outputDir string, dryRun bool) error { repoOutDir := filepath.Join(outputDir, outName) // Clear existing directory - os.RemoveAll(repoOutDir) + _ = io.Local.DeleteAll(repoOutDir) - if err := os.MkdirAll(repoOutDir, 0755); err != nil { + if err := io.Local.EnsureDir(repoOutDir); err != nil { cli.Print(" %s %s: %s\n", errorStyle.Render("✗"), info.Name, err) continue } @@ -139,7 +139,7 @@ func runDocsSync(registryPath string, outputDir string, dryRun bool) error { for _, f := range info.DocsFiles { src := filepath.Join(docsDir, f) dst := filepath.Join(repoOutDir, f) - os.MkdirAll(filepath.Dir(dst), 0755) + _ = io.Local.EnsureDir(filepath.Dir(dst)) if err := copyFile(src, dst); err != nil { cli.Print(" %s %s: %s\n", errorStyle.Render("✗"), f, err) } diff --git a/pkg/io/local/client.go b/pkg/io/local/client.go index 16989b80..ad90e590 100644 --- a/pkg/io/local/client.go +++ b/pkg/io/local/client.go @@ -43,12 +43,20 @@ func (m *Medium) path(relativePath string) (string, error) { return "", errors.New("path traversal attempt detected") } - // Reject absolute paths - they bypass the sandbox - if filepath.IsAbs(cleanPath) { + // When root is "/" (full filesystem access), allow absolute paths + isRootFS := m.root == "/" || m.root == string(filepath.Separator) + + // Reject absolute paths unless we're the root filesystem + if filepath.IsAbs(cleanPath) && !isRootFS { return "", errors.New("path traversal attempt detected") } - fullPath := filepath.Join(m.root, cleanPath) + var fullPath string + if filepath.IsAbs(cleanPath) { + fullPath = cleanPath + } else { + fullPath = filepath.Join(m.root, cleanPath) + } // Verify the resulting path is still within root (boundary-aware check) // Must use separator to prevent /tmp/root matching /tmp/root2