diff --git a/internal/cmd/dev/cmd_file_sync.go b/internal/cmd/dev/cmd_file_sync.go index 4886683a..9eb44fbc 100644 --- a/internal/cmd/dev/cmd_file_sync.go +++ b/internal/cmd/dev/cmd_file_sync.go @@ -15,10 +15,10 @@ import ( "strings" "github.com/host-uk/core/pkg/cli" - "github.com/host-uk/core/pkg/errors" "github.com/host-uk/core/pkg/git" "github.com/host-uk/core/pkg/i18n" coreio "github.com/host-uk/core/pkg/io" + "github.com/host-uk/core/pkg/log" "github.com/host-uk/core/pkg/repos" ) @@ -59,7 +59,7 @@ func runFileSync(source string) error { // Security: Reject path traversal attempts if strings.Contains(source, "..") { - return errors.E("dev.sync", "path traversal not allowed", nil) + return log.E("dev.sync", "path traversal not allowed", nil) } // Validate source exists @@ -82,7 +82,7 @@ func runFileSync(source string) error { // Let's stick to os.Stat for source properties finding as typically allowed for CLI args. if err != nil { - return errors.E("dev.sync", i18n.T("cmd.dev.file_sync.error.source_not_found", map[string]interface{}{"Path": source}), err) + return log.E("dev.sync", i18n.T("cmd.dev.file_sync.error.source_not_found", map[string]interface{}{"Path": source}), err) } // Find target repos @@ -131,7 +131,11 @@ func runFileSync(source string) error { } } else { // Ensure dir exists - coreio.Local.EnsureDir(filepath.Dir(destPath)) + if err := coreio.Local.EnsureDir(filepath.Dir(destPath)); err != nil { + cli.Print(" %s %s: copy failed: %s\n", errorStyle.Render("x"), repoName, err) + failed++ + continue + } if err := coreio.Copy(coreio.Local, source, coreio.Local, destPath); err != nil { cli.Print(" %s %s: copy failed: %s\n", errorStyle.Render("x"), repoName, err) failed++ @@ -205,12 +209,12 @@ func resolveTargetRepos(pattern string) ([]*repos.Repo, error) { // Load registry registryPath, err := repos.FindRegistry() if err != nil { - return nil, errors.E("dev.sync", "failed to find registry", err) + return nil, log.E("dev.sync", "failed to find registry", err) } registry, err := repos.LoadRegistry(registryPath) if err != nil { - return nil, errors.E("dev.sync", "failed to load registry", err) + return nil, log.E("dev.sync", "failed to load registry", err) } // Match pattern against repo names diff --git a/internal/cmd/docs/cmd_sync.go b/internal/cmd/docs/cmd_sync.go index a1611056..d7799ac7 100644 --- a/internal/cmd/docs/cmd_sync.go +++ b/internal/cmd/docs/cmd_sync.go @@ -140,7 +140,10 @@ func runDocsSync(registryPath string, outputDir string, dryRun bool) error { src := filepath.Join(docsDir, f) dst := filepath.Join(repoOutDir, f) // Ensure parent dir - io.Local.EnsureDir(filepath.Dir(dst)) + if err := io.Local.EnsureDir(filepath.Dir(dst)); err != nil { + cli.Print(" %s %s: %s\n", errorStyle.Render("✗"), f, err) + continue + } if err := io.Copy(io.Local, src, io.Local, dst); err != nil { cli.Print(" %s %s: %s\n", errorStyle.Render("✗"), f, err) diff --git a/internal/cmd/sdk/generators/go.go b/internal/cmd/sdk/generators/go.go index 0aff5279..b7902900 100644 --- a/internal/cmd/sdk/generators/go.go +++ b/internal/cmd/sdk/generators/go.go @@ -8,6 +8,7 @@ import ( "path/filepath" coreio "github.com/host-uk/core/pkg/io" + "github.com/host-uk/core/pkg/log" ) // GoGenerator generates Go SDKs from OpenAPI specs. @@ -37,7 +38,7 @@ func (g *GoGenerator) Install() string { // Generate creates SDK from OpenAPI spec. func (g *GoGenerator) Generate(ctx context.Context, opts Options) error { if err := coreio.Local.EnsureDir(opts.OutputDir); err != nil { - return fmt.Errorf("go.Generate: failed to create output dir: %w", err) + return log.E("go.Generate", "failed to create output dir", err) } if g.Available() { @@ -59,7 +60,7 @@ func (g *GoGenerator) generateNative(ctx context.Context, opts Options) error { cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { - return fmt.Errorf("go.generateNative: %w", err) + return log.E("go.generateNative", "oapi-codegen failed", err) } goMod := fmt.Sprintf("module %s\n\ngo 1.21\n", opts.PackageName) diff --git a/internal/cmd/setup/cmd_registry.go b/internal/cmd/setup/cmd_registry.go index d9714329..a06e10ef 100644 --- a/internal/cmd/setup/cmd_registry.go +++ b/internal/cmd/setup/cmd_registry.go @@ -117,7 +117,7 @@ func runRegistrySetupWithReg(ctx context.Context, reg *repos.Registry, registryP // Check if already exists repoPath := filepath.Join(basePath, repo.Name) - // Check .git dir existence via List + // Check .git dir existence via Exists if coreio.Local.Exists(filepath.Join(repoPath, ".git")) { exists++ continue diff --git a/pkg/cli/daemon.go b/pkg/cli/daemon.go index ccd3678b..e43df9f1 100644 --- a/pkg/cli/daemon.go +++ b/pkg/cli/daemon.go @@ -74,14 +74,13 @@ func IsStderrTTY() bool { // PIDFile manages a process ID file for single-instance enforcement. type PIDFile struct { - medium io.Medium - path string - mu sync.Mutex + path string + mu sync.Mutex } // NewPIDFile creates a PID file manager. -func NewPIDFile(m io.Medium, path string) *PIDFile { - return &PIDFile{medium: m, path: path} +func NewPIDFile(path string) *PIDFile { + return &PIDFile{path: path} } // Acquire writes the current PID to the file. @@ -91,7 +90,7 @@ func (p *PIDFile) Acquire() error { defer p.mu.Unlock() // Check if PID file exists - if data, err := p.medium.Read(p.path); err == nil { + if data, err := io.Local.Read(p.path); err == nil { pid, err := strconv.Atoi(data) if err == nil && pid > 0 { // Check if process is still running @@ -102,19 +101,19 @@ func (p *PIDFile) Acquire() error { } } // Stale PID file, remove it - _ = p.medium.Delete(p.path) + _ = io.Local.Delete(p.path) } // Ensure directory exists if dir := filepath.Dir(p.path); dir != "." { - if err := p.medium.EnsureDir(dir); err != nil { + if err := io.Local.EnsureDir(dir); err != nil { return fmt.Errorf("failed to create PID directory: %w", err) } } // Write current PID pid := os.Getpid() - if err := p.medium.Write(p.path, strconv.Itoa(pid)); err != nil { + if err := io.Local.Write(p.path, strconv.Itoa(pid)); err != nil { return fmt.Errorf("failed to write PID file: %w", err) } @@ -125,7 +124,7 @@ func (p *PIDFile) Acquire() error { func (p *PIDFile) Release() error { p.mu.Lock() defer p.mu.Unlock() - return p.medium.Delete(p.path) + return io.Local.Delete(p.path) } // Path returns the PID file path. @@ -219,7 +218,7 @@ func (h *HealthServer) Start() error { go func() { if err := h.server.Serve(listener); err != http.ErrServerClosed { - LogError("health server error", "err", err) + LogError(fmt.Sprintf("health server error: %v", err)) } }() @@ -247,10 +246,6 @@ func (h *HealthServer) Addr() string { // DaemonOptions configures daemon mode execution. type DaemonOptions struct { - // Medium is the storage backend for PID files. - // Defaults to io.Local if not set. - Medium io.Medium - // PIDFile path for single-instance enforcement. // Leave empty to skip PID file management. PIDFile string @@ -287,9 +282,6 @@ func NewDaemon(opts DaemonOptions) *Daemon { if opts.ShutdownTimeout == 0 { opts.ShutdownTimeout = 30 * time.Second } - if opts.Medium == nil { - opts.Medium = io.Local - } d := &Daemon{ opts: opts, @@ -297,7 +289,7 @@ func NewDaemon(opts DaemonOptions) *Daemon { } if opts.PIDFile != "" { - d.pid = NewPIDFile(opts.Medium, opts.PIDFile) + d.pid = NewPIDFile(opts.PIDFile) } if opts.HealthAddr != "" { diff --git a/pkg/mcp/transport_tcp.go b/pkg/mcp/transport_tcp.go index f7b5f1e5..0e6e0f7e 100644 --- a/pkg/mcp/transport_tcp.go +++ b/pkg/mcp/transport_tcp.go @@ -4,6 +4,7 @@ import ( "bufio" "context" "fmt" + "io" "net" "os" @@ -11,6 +12,9 @@ import ( "github.com/modelcontextprotocol/go-sdk/mcp" ) +// maxMCPMessageSize is the maximum size for MCP JSON-RPC messages (10 MB). +const maxMCPMessageSize = 10 * 1024 * 1024 + // TCPTransport manages a TCP listener for MCP. type TCPTransport struct { addr string @@ -36,6 +40,12 @@ func (s *Service) ServeTCP(ctx context.Context, addr string) error { } defer t.listener.Close() + // Close listener when context is cancelled to unblock Accept + go func() { + <-ctx.Done() + t.listener.Close() + }() + if addr == "" { addr = t.listener.Addr().String() } @@ -84,9 +94,11 @@ type connTransport struct { } func (t *connTransport) Connect(ctx context.Context) (mcp.Connection, error) { + scanner := bufio.NewScanner(t.conn) + scanner.Buffer(make([]byte, 64*1024), maxMCPMessageSize) return &connConnection{ conn: t.conn, - scanner: bufio.NewScanner(t.conn), + scanner: scanner, }, nil } @@ -102,10 +114,8 @@ func (c *connConnection) Read(ctx context.Context) (jsonrpc.Message, error) { if err := c.scanner.Err(); err != nil { return nil, err } - // EOF - // Return error to signal closure, as per Scanner contract? - // SDK usually expects error on close. - return nil, fmt.Errorf("EOF") + // EOF - connection closed cleanly + return nil, io.EOF } line := c.scanner.Bytes() return jsonrpc.DecodeMessage(line)