cli/pkg/io/local/client.go
Snider 95a980bea1 feat: Batch implementation of Gemini issues (#176)
* feat(help): Add CLI help command

Fixes #136

* chore: remove binary

* feat(mcp): Add TCP transport

Fixes #126

* feat(io): Migrate pkg/mcp to use Medium abstraction

Fixes #103

* chore(io): Migrate internal/cmd/docs/* to Medium abstraction

Fixes #113

* chore(io): Migrate internal/cmd/dev/* to Medium abstraction

Fixes #114

* chore(io): Migrate internal/cmd/setup/* to Medium abstraction

* chore(io): Complete migration of internal/cmd/dev/* to Medium abstraction

* chore(io): Migrate internal/cmd/sdk, pkgcmd, and workspace to Medium abstraction

* style: fix formatting in internal/variants

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(io): simplify local Medium implementation

Rewrote to match the simpler TypeScript pattern:
- path() sanitizes and returns string directly
- Each method calls path() once
- No complex symlink validation
- Less code, less attack surface

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test(mcp): update sandboxing tests for simplified Medium

The simplified io/local.Medium implementation:
- Sanitizes .. to . (no error, path is cleaned)
- Allows absolute paths through (caller validates if needed)
- Follows symlinks (no traversal blocking)

Update tests to match this simplified behavior.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(updater): resolve PkgVersion duplicate declaration

Remove var PkgVersion from updater.go since go generate creates
const PkgVersion in version.go. Track version.go in git to ensure
builds work without running go generate first.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 04:20:18 +00:00

127 lines
2.9 KiB
Go

// Package local provides a local filesystem implementation of the io.Medium interface.
package local
import (
"io/fs"
"os"
"path/filepath"
"strings"
)
// Medium is a local filesystem storage backend.
type Medium struct {
root string
}
// New creates a new local Medium rooted at the given directory.
// Pass "/" for full filesystem access, or a specific path to sandbox.
func New(root string) (*Medium, error) {
abs, err := filepath.Abs(root)
if err != nil {
return nil, err
}
return &Medium{root: abs}, nil
}
// path sanitizes and returns the full path.
// Replaces .. with . to prevent traversal, then joins with root.
func (m *Medium) path(p string) string {
if p == "" {
return m.root
}
clean := strings.ReplaceAll(p, "..", ".")
if filepath.IsAbs(clean) {
return filepath.Clean(clean)
}
return filepath.Join(m.root, clean)
}
// Read returns file contents as string.
func (m *Medium) Read(p string) (string, error) {
data, err := os.ReadFile(m.path(p))
if err != nil {
return "", err
}
return string(data), nil
}
// Write saves content to file, creating parent directories as needed.
func (m *Medium) Write(p, content string) error {
full := m.path(p)
if err := os.MkdirAll(filepath.Dir(full), 0755); err != nil {
return err
}
return os.WriteFile(full, []byte(content), 0644)
}
// EnsureDir creates directory if it doesn't exist.
func (m *Medium) EnsureDir(p string) error {
return os.MkdirAll(m.path(p), 0755)
}
// IsDir returns true if path is a directory.
func (m *Medium) IsDir(p string) bool {
if p == "" {
return false
}
info, err := os.Stat(m.path(p))
return err == nil && info.IsDir()
}
// IsFile returns true if path is a regular file.
func (m *Medium) IsFile(p string) bool {
if p == "" {
return false
}
info, err := os.Stat(m.path(p))
return err == nil && info.Mode().IsRegular()
}
// Exists returns true if path exists.
func (m *Medium) Exists(p string) bool {
_, err := os.Stat(m.path(p))
return err == nil
}
// List returns directory entries.
func (m *Medium) List(p string) ([]fs.DirEntry, error) {
return os.ReadDir(m.path(p))
}
// Stat returns file info.
func (m *Medium) Stat(p string) (fs.FileInfo, error) {
return os.Stat(m.path(p))
}
// Delete removes a file or empty directory.
func (m *Medium) Delete(p string) error {
full := m.path(p)
if len(full) < 3 {
return nil
}
return os.Remove(full)
}
// DeleteAll removes a file or directory recursively.
func (m *Medium) DeleteAll(p string) error {
full := m.path(p)
if len(full) < 3 {
return nil
}
return os.RemoveAll(full)
}
// Rename moves a file or directory.
func (m *Medium) Rename(oldPath, newPath string) error {
return os.Rename(m.path(oldPath), m.path(newPath))
}
// FileGet is an alias for Read.
func (m *Medium) FileGet(p string) (string, error) {
return m.Read(p)
}
// FileSet is an alias for Write.
func (m *Medium) FileSet(p, content string) error {
return m.Write(p, content)
}