From e9fc6902b12083cad5c55ef1df15459a44d7d4ea Mon Sep 17 00:00:00 2001 From: Snider Date: Mon, 16 Mar 2026 20:37:25 +0000 Subject: [PATCH] refactor: replace fmt.Errorf/errors.New with coreerr.E() Replace all remaining fmt.Errorf and errors.New calls in production code with coreerr.E("caller.Method", "message", err) from go-log. This standardises error handling across 23 files using the structured error convention already established in the plugin package. Co-Authored-By: Virgil --- agentci/config.go | 8 +++---- agentci/security.go | 7 +++--- cmd/forge/cmd_sync.go | 13 +++++----- cmd/gitea/cmd_sync.go | 13 +++++----- forge/labels.go | 3 +-- jobrunner/handlers/completion.go | 9 +++---- jobrunner/handlers/dispatch.go | 22 ++++++++--------- jobrunner/handlers/resolve_threads.go | 3 ++- jobrunner/handlers/tick_parent.go | 3 ++- jobrunner/journal.go | 33 +++++++++++++------------- manifest/compile.go | 16 ++++++------- manifest/loader.go | 8 +++---- manifest/manifest.go | 5 ++-- manifest/sign.go | 11 ++++----- marketplace/builder.go | 12 +++++----- marketplace/discovery.go | 12 +++++----- marketplace/installer.go | 34 +++++++++++++-------------- marketplace/marketplace.go | 5 ++-- plugin/installer.go | 2 +- repos/gitstate.go | 12 +++++----- repos/kbconfig.go | 11 +++++---- repos/registry.go | 15 ++++++------ repos/workconfig.go | 12 +++++----- 23 files changed, 136 insertions(+), 133 deletions(-) diff --git a/agentci/config.go b/agentci/config.go index 54a7a50..d8d80b0 100644 --- a/agentci/config.go +++ b/agentci/config.go @@ -2,10 +2,10 @@ package agentci import ( - "errors" "fmt" "forge.lthn.ai/core/config" + coreerr "forge.lthn.ai/core/go-log" ) // AgentConfig represents a single agent machine in the config file. @@ -43,7 +43,7 @@ func LoadAgents(cfg *config.Config) (map[string]AgentConfig, error) { continue } if ac.Host == "" { - return nil, fmt.Errorf("agent %q: host is required", name) + return nil, coreerr.E("agentci.LoadAgents", "agent "+name+": host is required", nil) } if ac.QueueDir == "" { ac.QueueDir = "/home/claude/ai-work/queue" @@ -126,10 +126,10 @@ func SaveAgent(cfg *config.Config, name string, ac AgentConfig) error { func RemoveAgent(cfg *config.Config, name string) error { var agents map[string]AgentConfig if err := cfg.Get("agentci.agents", &agents); err != nil { - return errors.New("no agents configured") + return coreerr.E("agentci.RemoveAgent", "no agents configured", nil) } if _, ok := agents[name]; !ok { - return fmt.Errorf("agent %q not found", name) + return coreerr.E("agentci.RemoveAgent", "agent not found: "+name, nil) } delete(agents, name) return cfg.Set("agentci.agents", agents) diff --git a/agentci/security.go b/agentci/security.go index f917b3f..1bb32a3 100644 --- a/agentci/security.go +++ b/agentci/security.go @@ -1,11 +1,12 @@ package agentci import ( - "fmt" "os/exec" "path/filepath" "regexp" "strings" + + coreerr "forge.lthn.ai/core/go-log" ) var safeNameRegex = regexp.MustCompile(`^[a-zA-Z0-9\-\_\.]+$`) @@ -15,10 +16,10 @@ var safeNameRegex = regexp.MustCompile(`^[a-zA-Z0-9\-\_\.]+$`) func SanitizePath(input string) (string, error) { base := filepath.Base(input) if !safeNameRegex.MatchString(base) { - return "", fmt.Errorf("invalid characters in path element: %s", input) + return "", coreerr.E("agentci.SanitizePath", "invalid characters in path element: "+input, nil) } if base == "." || base == ".." || base == "/" { - return "", fmt.Errorf("invalid path element: %s", base) + return "", coreerr.E("agentci.SanitizePath", "invalid path element: "+base, nil) } return base, nil } diff --git a/cmd/forge/cmd_sync.go b/cmd/forge/cmd_sync.go index 578dd7e..ab12798 100644 --- a/cmd/forge/cmd_sync.go +++ b/cmd/forge/cmd_sync.go @@ -10,6 +10,7 @@ import ( forgejo "codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2" "forge.lthn.ai/core/cli/pkg/cli" + coreerr "forge.lthn.ai/core/go-log" fg "forge.lthn.ai/core/go-scm/forge" ) @@ -64,7 +65,7 @@ func runSync(args []string) error { if strings.HasPrefix(basePath, "~/") { home, err := os.UserHomeDir() if err != nil { - return fmt.Errorf("failed to resolve home directory: %w", err) + return coreerr.E("forge.runSync", "failed to resolve home directory", err) } basePath = filepath.Join(home, basePath[2:]) } @@ -287,7 +288,7 @@ func syncConfigureForgeRemote(localPath, remoteURL string) error { if existing != remoteURL { cmd := exec.Command("git", "-C", localPath, "remote", "set-url", "forge", remoteURL) if err := cmd.Run(); err != nil { - return fmt.Errorf("failed to update remote: %w", err) + return coreerr.E("forge.syncConfigureForgeRemote", "failed to update remote", err) } } return nil @@ -295,7 +296,7 @@ func syncConfigureForgeRemote(localPath, remoteURL string) error { cmd := exec.Command("git", "-C", localPath, "remote", "add", "forge", remoteURL) if err := cmd.Run(); err != nil { - return fmt.Errorf("failed to add remote: %w", err) + return coreerr.E("forge.syncConfigureForgeRemote", "failed to add remote", err) } return nil @@ -306,7 +307,7 @@ func syncPushUpstream(localPath, defaultBranch string) error { cmd := exec.Command("git", "-C", localPath, "push", "--force", "forge", refspec) output, err := cmd.CombinedOutput() if err != nil { - return fmt.Errorf("%s: %w", strings.TrimSpace(string(output)), err) + return coreerr.E("forge.syncPushUpstream", strings.TrimSpace(string(output)), err) } return nil @@ -316,7 +317,7 @@ func syncGitFetch(localPath, remote string) error { cmd := exec.Command("git", "-C", localPath, "fetch", remote) output, err := cmd.CombinedOutput() if err != nil { - return fmt.Errorf("%s: %w", strings.TrimSpace(string(output)), err) + return coreerr.E("forge.syncGitFetch", strings.TrimSpace(string(output)), err) } return nil } @@ -327,7 +328,7 @@ func syncCreateMainFromUpstream(client *fg.Client, org, repo string) error { OldBranchName: "upstream", }) if err != nil { - return fmt.Errorf("create branch: %w", err) + return coreerr.E("forge.syncCreateMainFromUpstream", "create branch", err) } return nil diff --git a/cmd/gitea/cmd_sync.go b/cmd/gitea/cmd_sync.go index f6a7c02..6a3e4a4 100644 --- a/cmd/gitea/cmd_sync.go +++ b/cmd/gitea/cmd_sync.go @@ -10,6 +10,7 @@ import ( "code.gitea.io/sdk/gitea" "forge.lthn.ai/core/cli/pkg/cli" + coreerr "forge.lthn.ai/core/go-log" gt "forge.lthn.ai/core/go-scm/gitea" ) @@ -64,7 +65,7 @@ func runSync(args []string) error { if strings.HasPrefix(basePath, "~/") { home, err := os.UserHomeDir() if err != nil { - return fmt.Errorf("failed to resolve home directory: %w", err) + return coreerr.E("gitea.runSync", "failed to resolve home directory", err) } basePath = filepath.Join(home, basePath[2:]) } @@ -299,7 +300,7 @@ func configureGiteaRemote(localPath, remoteURL string) error { if existing != remoteURL { cmd := exec.Command("git", "-C", localPath, "remote", "set-url", "gitea", remoteURL) if err := cmd.Run(); err != nil { - return fmt.Errorf("failed to update remote: %w", err) + return coreerr.E("gitea.configureGiteaRemote", "failed to update remote", err) } } return nil @@ -308,7 +309,7 @@ func configureGiteaRemote(localPath, remoteURL string) error { // Add new remote cmd := exec.Command("git", "-C", localPath, "remote", "add", "gitea", remoteURL) if err := cmd.Run(); err != nil { - return fmt.Errorf("failed to add remote: %w", err) + return coreerr.E("gitea.configureGiteaRemote", "failed to add remote", err) } return nil @@ -321,7 +322,7 @@ func pushUpstream(localPath, defaultBranch string) error { cmd := exec.Command("git", "-C", localPath, "push", "--force", "gitea", refspec) output, err := cmd.CombinedOutput() if err != nil { - return fmt.Errorf("%s: %w", strings.TrimSpace(string(output)), err) + return coreerr.E("gitea.pushUpstream", strings.TrimSpace(string(output)), err) } return nil @@ -332,7 +333,7 @@ func gitFetch(localPath, remote string) error { cmd := exec.Command("git", "-C", localPath, "fetch", remote) output, err := cmd.CombinedOutput() if err != nil { - return fmt.Errorf("%s: %w", strings.TrimSpace(string(output)), err) + return coreerr.E("gitea.gitFetch", strings.TrimSpace(string(output)), err) } return nil } @@ -344,7 +345,7 @@ func createMainFromUpstream(client *gt.Client, org, repo string) error { OldBranchName: "upstream", }) if err != nil { - return fmt.Errorf("create branch: %w", err) + return coreerr.E("gitea.createMainFromUpstream", "create branch", err) } return nil diff --git a/forge/labels.go b/forge/labels.go index e5aa710..77a7b85 100644 --- a/forge/labels.go +++ b/forge/labels.go @@ -1,7 +1,6 @@ package forge import ( - "fmt" "strings" forgejo "codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2" @@ -75,7 +74,7 @@ func (c *Client) GetLabelByName(owner, repo, name string) (*forgejo.Label, error } } - return nil, fmt.Errorf("forge.GetLabelByName: label %s not found in %s/%s", name, owner, repo) + return nil, log.E("forge.GetLabelByName", "label "+name+" not found in "+owner+"/"+repo, nil) } // EnsureLabel checks if a label exists, and creates it if it doesn't. diff --git a/jobrunner/handlers/completion.go b/jobrunner/handlers/completion.go index 0355bda..67598d0 100644 --- a/jobrunner/handlers/completion.go +++ b/jobrunner/handlers/completion.go @@ -5,6 +5,7 @@ import ( "fmt" "time" + coreerr "forge.lthn.ai/core/go-log" "forge.lthn.ai/core/go-scm/forge" "forge.lthn.ai/core/go-scm/jobrunner" ) @@ -47,11 +48,11 @@ func (h *CompletionHandler) Execute(ctx context.Context, signal *jobrunner.Pipel if signal.Success { completeLabel, err := h.forge.EnsureLabel(signal.RepoOwner, signal.RepoName, LabelAgentComplete, ColorAgentComplete) if err != nil { - return nil, fmt.Errorf("ensure label %s: %w", LabelAgentComplete, err) + return nil, coreerr.E("completion.Execute", "ensure label "+LabelAgentComplete, err) } if err := h.forge.AddIssueLabels(signal.RepoOwner, signal.RepoName, int64(signal.ChildNumber), []int64{completeLabel.ID}); err != nil { - return nil, fmt.Errorf("add completed label: %w", err) + return nil, coreerr.E("completion.Execute", "add completed label", err) } if signal.Message != "" { @@ -60,11 +61,11 @@ func (h *CompletionHandler) Execute(ctx context.Context, signal *jobrunner.Pipel } else { failedLabel, err := h.forge.EnsureLabel(signal.RepoOwner, signal.RepoName, LabelAgentFailed, ColorAgentFailed) if err != nil { - return nil, fmt.Errorf("ensure label %s: %w", LabelAgentFailed, err) + return nil, coreerr.E("completion.Execute", "ensure label "+LabelAgentFailed, err) } if err := h.forge.AddIssueLabels(signal.RepoOwner, signal.RepoName, int64(signal.ChildNumber), []int64{failedLabel.ID}); err != nil { - return nil, fmt.Errorf("add failed label: %w", err) + return nil, coreerr.E("completion.Execute", "add failed label", err) } msg := "Agent reported failure." diff --git a/jobrunner/handlers/dispatch.go b/jobrunner/handlers/dispatch.go index 57c33e9..0ea7372 100644 --- a/jobrunner/handlers/dispatch.go +++ b/jobrunner/handlers/dispatch.go @@ -8,10 +8,10 @@ import ( "path/filepath" "time" + coreerr "forge.lthn.ai/core/go-log" "forge.lthn.ai/core/go-scm/agentci" "forge.lthn.ai/core/go-scm/forge" "forge.lthn.ai/core/go-scm/jobrunner" - "forge.lthn.ai/core/go-log" ) const ( @@ -83,23 +83,23 @@ func (h *DispatchHandler) Execute(ctx context.Context, signal *jobrunner.Pipelin agentName, agent, ok := h.spinner.FindByForgejoUser(signal.Assignee) if !ok { - return nil, fmt.Errorf("unknown agent: %s", signal.Assignee) + return nil, coreerr.E("dispatch.Execute", "unknown agent: "+signal.Assignee, nil) } // Sanitize inputs to prevent path traversal. safeOwner, err := agentci.SanitizePath(signal.RepoOwner) if err != nil { - return nil, fmt.Errorf("invalid repo owner: %w", err) + return nil, coreerr.E("dispatch.Execute", "invalid repo owner", err) } safeRepo, err := agentci.SanitizePath(signal.RepoName) if err != nil { - return nil, fmt.Errorf("invalid repo name: %w", err) + return nil, coreerr.E("dispatch.Execute", "invalid repo name", err) } // Ensure in-progress label exists on repo. inProgressLabel, err := h.forge.EnsureLabel(safeOwner, safeRepo, LabelInProgress, ColorInProgress) if err != nil { - return nil, fmt.Errorf("ensure label %s: %w", LabelInProgress, err) + return nil, coreerr.E("dispatch.Execute", "ensure label "+LabelInProgress, err) } // Check if already in progress to prevent double-dispatch. @@ -107,7 +107,7 @@ func (h *DispatchHandler) Execute(ctx context.Context, signal *jobrunner.Pipelin if err == nil { for _, l := range issue.Labels { if l.Name == LabelInProgress || l.Name == LabelAgentComplete { - log.Info("issue already processed, skipping", "issue", signal.ChildNumber, "label", l.Name) + coreerr.Info("issue already processed, skipping", "issue", signal.ChildNumber, "label", l.Name) return &jobrunner.ActionResult{ Action: "dispatch", Success: true, @@ -120,11 +120,11 @@ func (h *DispatchHandler) Execute(ctx context.Context, signal *jobrunner.Pipelin // Assign agent and add in-progress label. if err := h.forge.AssignIssue(safeOwner, safeRepo, int64(signal.ChildNumber), []string{signal.Assignee}); err != nil { - log.Warn("failed to assign agent, continuing", "err", err) + coreerr.Warn("failed to assign agent, continuing", "err", err) } if err := h.forge.AddIssueLabels(safeOwner, safeRepo, int64(signal.ChildNumber), []int64{inProgressLabel.ID}); err != nil { - return nil, fmt.Errorf("add in-progress label: %w", err) + return nil, coreerr.E("dispatch.Execute", "add in-progress label", err) } // Remove agent-ready label if present. @@ -164,13 +164,13 @@ func (h *DispatchHandler) Execute(ctx context.Context, signal *jobrunner.Pipelin ticketJSON, err := json.MarshalIndent(ticket, "", " ") if err != nil { h.failDispatch(signal, "Failed to marshal ticket JSON") - return nil, fmt.Errorf("marshal ticket: %w", err) + return nil, coreerr.E("dispatch.Execute", "marshal ticket", err) } // Check if ticket already exists on agent (dedup). ticketName := fmt.Sprintf("ticket-%s-%s-%d.json", safeOwner, safeRepo, signal.ChildNumber) if h.ticketExists(ctx, agent, ticketName) { - log.Info("ticket already queued, skipping", "ticket", ticketName, "agent", signal.Assignee) + coreerr.Info("ticket already queued, skipping", "ticket", ticketName, "agent", signal.Assignee) return &jobrunner.ActionResult{ Action: "dispatch", RepoOwner: safeOwner, @@ -263,7 +263,7 @@ func (h *DispatchHandler) secureTransfer(ctx context.Context, agent agentci.Agen output, err := cmd.CombinedOutput() if err != nil { - return log.E("dispatch.transfer", fmt.Sprintf("ssh to %s failed: %s", agent.Host, string(output)), err) + return coreerr.E("dispatch.transfer", fmt.Sprintf("ssh to %s failed: %s", agent.Host, string(output)), err) } return nil } diff --git a/jobrunner/handlers/resolve_threads.go b/jobrunner/handlers/resolve_threads.go index acb8477..4abbc6e 100644 --- a/jobrunner/handlers/resolve_threads.go +++ b/jobrunner/handlers/resolve_threads.go @@ -7,6 +7,7 @@ import ( forgejosdk "codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2" + coreerr "forge.lthn.ai/core/go-log" "forge.lthn.ai/core/go-scm/forge" "forge.lthn.ai/core/go-scm/jobrunner" ) @@ -39,7 +40,7 @@ func (h *DismissReviewsHandler) Execute(ctx context.Context, signal *jobrunner.P reviews, err := h.forge.ListPRReviews(signal.RepoOwner, signal.RepoName, int64(signal.PRNumber)) if err != nil { - return nil, fmt.Errorf("dismiss_reviews: list reviews: %w", err) + return nil, coreerr.E("dismiss_reviews.Execute", "list reviews", err) } var dismissErrors []string diff --git a/jobrunner/handlers/tick_parent.go b/jobrunner/handlers/tick_parent.go index fa7db10..6ed0b26 100644 --- a/jobrunner/handlers/tick_parent.go +++ b/jobrunner/handlers/tick_parent.go @@ -8,6 +8,7 @@ import ( forgejosdk "codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2" + coreerr "forge.lthn.ai/core/go-log" "forge.lthn.ai/core/go-scm/forge" "forge.lthn.ai/core/go-scm/jobrunner" ) @@ -41,7 +42,7 @@ func (h *TickParentHandler) Execute(ctx context.Context, signal *jobrunner.Pipel // Fetch the epic issue body. epic, err := h.forge.GetIssue(signal.RepoOwner, signal.RepoName, int64(signal.EpicNumber)) if err != nil { - return nil, fmt.Errorf("tick_parent: fetch epic: %w", err) + return nil, coreerr.E("tick_parent.Execute", "fetch epic", err) } oldBody := epic.Body diff --git a/jobrunner/journal.go b/jobrunner/journal.go index 0a5ba4d..db037dc 100644 --- a/jobrunner/journal.go +++ b/jobrunner/journal.go @@ -2,14 +2,13 @@ package jobrunner import ( "encoding/json" - "errors" - "fmt" "os" "path/filepath" "regexp" "strings" "sync" + coreerr "forge.lthn.ai/core/go-log" coreio "forge.lthn.ai/core/go-io" ) @@ -55,7 +54,7 @@ type Journal struct { // NewJournal creates a new Journal rooted at baseDir. func NewJournal(baseDir string) (*Journal, error) { if baseDir == "" { - return nil, errors.New("journal base directory is required") + return nil, coreerr.E("jobrunner.NewJournal", "base directory is required", nil) } return &Journal{baseDir: baseDir}, nil } @@ -66,12 +65,12 @@ func NewJournal(baseDir string) (*Journal, error) { func sanitizePathComponent(name string) (string, error) { // Reject empty or whitespace-only values. if name == "" || strings.TrimSpace(name) == "" { - return "", fmt.Errorf("invalid path component: %q", name) + return "", coreerr.E("jobrunner.sanitizePathComponent", "invalid path component: "+name, nil) } // Reject inputs containing path separators (directory traversal attempt). if strings.ContainsAny(name, `/\`) { - return "", fmt.Errorf("path component contains directory separator: %q", name) + return "", coreerr.E("jobrunner.sanitizePathComponent", "path component contains directory separator: "+name, nil) } // Use filepath.Clean to normalize (e.g., collapse redundant dots). @@ -79,12 +78,12 @@ func sanitizePathComponent(name string) (string, error) { // Reject traversal components. if clean == "." || clean == ".." { - return "", fmt.Errorf("invalid path component: %q", name) + return "", coreerr.E("jobrunner.sanitizePathComponent", "invalid path component: "+name, nil) } // Validate against the safe character set. if !validPathComponent.MatchString(clean) { - return "", fmt.Errorf("path component contains invalid characters: %q", name) + return "", coreerr.E("jobrunner.sanitizePathComponent", "path component contains invalid characters: "+name, nil) } return clean, nil @@ -93,10 +92,10 @@ func sanitizePathComponent(name string) (string, error) { // Append writes a journal entry for the given signal and result. func (j *Journal) Append(signal *PipelineSignal, result *ActionResult) error { if signal == nil { - return errors.New("signal is required") + return coreerr.E("jobrunner.Journal.Append", "signal is required", nil) } if result == nil { - return errors.New("result is required") + return coreerr.E("jobrunner.Journal.Append", "result is required", nil) } entry := JournalEntry{ @@ -124,18 +123,18 @@ func (j *Journal) Append(signal *PipelineSignal, result *ActionResult) error { data, err := json.Marshal(entry) if err != nil { - return fmt.Errorf("marshal journal entry: %w", err) + return coreerr.E("jobrunner.Journal.Append", "marshal journal entry", err) } data = append(data, '\n') // Sanitize path components to prevent path traversal (CVE: issue #46). owner, err := sanitizePathComponent(signal.RepoOwner) if err != nil { - return fmt.Errorf("invalid repo owner: %w", err) + return coreerr.E("jobrunner.Journal.Append", "invalid repo owner", err) } repo, err := sanitizePathComponent(signal.RepoName) if err != nil { - return fmt.Errorf("invalid repo name: %w", err) + return coreerr.E("jobrunner.Journal.Append", "invalid repo name", err) } date := result.Timestamp.UTC().Format("2006-01-02") @@ -144,27 +143,27 @@ func (j *Journal) Append(signal *PipelineSignal, result *ActionResult) error { // Resolve to absolute path and verify it stays within baseDir. absBase, err := filepath.Abs(j.baseDir) if err != nil { - return fmt.Errorf("resolve base directory: %w", err) + return coreerr.E("jobrunner.Journal.Append", "resolve base directory", err) } absDir, err := filepath.Abs(dir) if err != nil { - return fmt.Errorf("resolve journal directory: %w", err) + return coreerr.E("jobrunner.Journal.Append", "resolve journal directory", err) } if !strings.HasPrefix(absDir, absBase+string(filepath.Separator)) { - return fmt.Errorf("journal path %q escapes base directory %q", absDir, absBase) + return coreerr.E("jobrunner.Journal.Append", "journal path escapes base directory", nil) } j.mu.Lock() defer j.mu.Unlock() if err := coreio.Local.EnsureDir(dir); err != nil { - return fmt.Errorf("create journal directory: %w", err) + return coreerr.E("jobrunner.Journal.Append", "create journal directory", err) } path := filepath.Join(dir, date+".jsonl") f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) if err != nil { - return fmt.Errorf("open journal file: %w", err) + return coreerr.E("jobrunner.Journal.Append", "open journal file", err) } defer func() { _ = f.Close() }() diff --git a/manifest/compile.go b/manifest/compile.go index 30508ed..a15cfe7 100644 --- a/manifest/compile.go +++ b/manifest/compile.go @@ -3,10 +3,10 @@ package manifest import ( "crypto/ed25519" "encoding/json" - "fmt" "path/filepath" "time" + coreerr "forge.lthn.ai/core/go-log" "forge.lthn.ai/core/go-io" ) @@ -35,19 +35,19 @@ type CompileOptions struct { // options. If opts.SignKey is provided the manifest is signed first. func Compile(m *Manifest, opts CompileOptions) (*CompiledManifest, error) { if m == nil { - return nil, fmt.Errorf("manifest.Compile: nil manifest") + return nil, coreerr.E("manifest.Compile", "nil manifest", nil) } if m.Code == "" { - return nil, fmt.Errorf("manifest.Compile: missing code") + return nil, coreerr.E("manifest.Compile", "missing code", nil) } if m.Version == "" { - return nil, fmt.Errorf("manifest.Compile: missing version") + return nil, coreerr.E("manifest.Compile", "missing version", nil) } // Sign if a key is supplied. if opts.SignKey != nil { if err := Sign(m, opts.SignKey); err != nil { - return nil, fmt.Errorf("manifest.Compile: %w", err) + return nil, coreerr.E("manifest.Compile", "sign failed", err) } } @@ -69,7 +69,7 @@ func MarshalJSON(cm *CompiledManifest) ([]byte, error) { func ParseCompiled(data []byte) (*CompiledManifest, error) { var cm CompiledManifest if err := json.Unmarshal(data, &cm); err != nil { - return nil, fmt.Errorf("manifest.ParseCompiled: %w", err) + return nil, coreerr.E("manifest.ParseCompiled", "unmarshal failed", err) } return &cm, nil } @@ -81,7 +81,7 @@ const compiledPath = "core.json" func WriteCompiled(medium io.Medium, root string, cm *CompiledManifest) error { data, err := MarshalJSON(cm) if err != nil { - return fmt.Errorf("manifest.WriteCompiled: %w", err) + return coreerr.E("manifest.WriteCompiled", "marshal failed", err) } path := filepath.Join(root, compiledPath) return medium.Write(path, string(data)) @@ -92,7 +92,7 @@ func LoadCompiled(medium io.Medium, root string) (*CompiledManifest, error) { path := filepath.Join(root, compiledPath) data, err := medium.Read(path) if err != nil { - return nil, fmt.Errorf("manifest.LoadCompiled: %w", err) + return nil, coreerr.E("manifest.LoadCompiled", "read failed", err) } return ParseCompiled([]byte(data)) } diff --git a/manifest/loader.go b/manifest/loader.go index 9bba76c..5f943c4 100644 --- a/manifest/loader.go +++ b/manifest/loader.go @@ -2,9 +2,9 @@ package manifest import ( "crypto/ed25519" - "fmt" "path/filepath" + coreerr "forge.lthn.ai/core/go-log" "forge.lthn.ai/core/go-io" "gopkg.in/yaml.v3" ) @@ -21,7 +21,7 @@ func Load(medium io.Medium, root string) (*Manifest, error) { path := filepath.Join(root, manifestPath) data, err := medium.Read(path) if err != nil { - return nil, fmt.Errorf("manifest.Load: %w", err) + return nil, coreerr.E("manifest.Load", "read failed", err) } return Parse([]byte(data)) } @@ -34,10 +34,10 @@ func LoadVerified(medium io.Medium, root string, pub ed25519.PublicKey) (*Manife } ok, err := Verify(m, pub) if err != nil { - return nil, fmt.Errorf("manifest.LoadVerified: %w", err) + return nil, coreerr.E("manifest.LoadVerified", "verification error", err) } if !ok { - return nil, fmt.Errorf("manifest.LoadVerified: signature verification failed for %q", m.Code) + return nil, coreerr.E("manifest.LoadVerified", "signature verification failed for "+m.Code, nil) } return m, nil } diff --git a/manifest/manifest.go b/manifest/manifest.go index c9e344d..7611220 100644 --- a/manifest/manifest.go +++ b/manifest/manifest.go @@ -1,8 +1,7 @@ package manifest import ( - "fmt" - + coreerr "forge.lthn.ai/core/go-log" "gopkg.in/yaml.v3" ) @@ -66,7 +65,7 @@ type DaemonSpec struct { func Parse(data []byte) (*Manifest, error) { var m Manifest if err := yaml.Unmarshal(data, &m); err != nil { - return nil, fmt.Errorf("manifest.Parse: %w", err) + return nil, coreerr.E("manifest.Parse", "unmarshal failed", err) } return &m, nil } diff --git a/manifest/sign.go b/manifest/sign.go index 857d15c..3edb94c 100644 --- a/manifest/sign.go +++ b/manifest/sign.go @@ -3,9 +3,8 @@ package manifest import ( "crypto/ed25519" "encoding/base64" - "errors" - "fmt" + coreerr "forge.lthn.ai/core/go-log" "gopkg.in/yaml.v3" ) @@ -20,7 +19,7 @@ func signable(m *Manifest) ([]byte, error) { func Sign(m *Manifest, priv ed25519.PrivateKey) error { msg, err := signable(m) if err != nil { - return fmt.Errorf("manifest.Sign: marshal: %w", err) + return coreerr.E("manifest.Sign", "marshal failed", err) } sig := ed25519.Sign(priv, msg) m.Sign = base64.StdEncoding.EncodeToString(sig) @@ -30,15 +29,15 @@ func Sign(m *Manifest, priv ed25519.PrivateKey) error { // Verify checks the ed25519 signature in m.Sign against the public key. func Verify(m *Manifest, pub ed25519.PublicKey) (bool, error) { if m.Sign == "" { - return false, errors.New("manifest.Verify: no signature present") + return false, coreerr.E("manifest.Verify", "no signature present", nil) } sig, err := base64.StdEncoding.DecodeString(m.Sign) if err != nil { - return false, fmt.Errorf("manifest.Verify: decode: %w", err) + return false, coreerr.E("manifest.Verify", "decode failed", err) } msg, err := signable(m) if err != nil { - return false, fmt.Errorf("manifest.Verify: marshal: %w", err) + return false, coreerr.E("manifest.Verify", "marshal failed", err) } return ed25519.Verify(pub, msg, sig), nil } diff --git a/marketplace/builder.go b/marketplace/builder.go index 87104e6..586bce5 100644 --- a/marketplace/builder.go +++ b/marketplace/builder.go @@ -2,12 +2,12 @@ package marketplace import ( "encoding/json" - "fmt" "log" "os" "path/filepath" "sort" + coreerr "forge.lthn.ai/core/go-log" coreio "forge.lthn.ai/core/go-io" "forge.lthn.ai/core/go-scm/manifest" ) @@ -40,7 +40,7 @@ func (b *Builder) BuildFromDirs(dirs ...string) (*Index, error) { if os.IsNotExist(err) { continue } - return nil, fmt.Errorf("marketplace.Builder: read %s: %w", dir, err) + return nil, coreerr.E("marketplace.Builder.BuildFromDirs", "read "+dir, err) } for _, e := range entries { @@ -115,11 +115,11 @@ func BuildFromManifests(manifests []*manifest.Manifest) *Index { // WriteIndex serialises an Index to JSON and writes it to the given path. func WriteIndex(path string, idx *Index) error { if err := coreio.Local.EnsureDir(filepath.Dir(path)); err != nil { - return fmt.Errorf("marketplace.WriteIndex: mkdir: %w", err) + return coreerr.E("marketplace.WriteIndex", "mkdir failed", err) } data, err := json.MarshalIndent(idx, "", " ") if err != nil { - return fmt.Errorf("marketplace.WriteIndex: marshal: %w", err) + return coreerr.E("marketplace.WriteIndex", "marshal failed", err) } return coreio.Local.Write(path, string(data)) } @@ -131,7 +131,7 @@ func (b *Builder) loadFromDir(dir string) (*manifest.Manifest, error) { if raw, err := coreio.Local.Read(coreJSON); err == nil { cm, err := manifest.ParseCompiled([]byte(raw)) if err != nil { - return nil, fmt.Errorf("parse core.json: %w", err) + return nil, coreerr.E("marketplace.Builder.loadFromDir", "parse core.json", err) } return &cm.Manifest, nil } @@ -145,7 +145,7 @@ func (b *Builder) loadFromDir(dir string) (*manifest.Manifest, error) { m, err := manifest.Parse([]byte(raw)) if err != nil { - return nil, fmt.Errorf("parse manifest.yaml: %w", err) + return nil, coreerr.E("marketplace.Builder.loadFromDir", "parse manifest.yaml", err) } return m, nil } diff --git a/marketplace/discovery.go b/marketplace/discovery.go index 08cc4c4..edb92ab 100644 --- a/marketplace/discovery.go +++ b/marketplace/discovery.go @@ -1,11 +1,11 @@ package marketplace import ( - "fmt" "log" "os" "path/filepath" + coreerr "forge.lthn.ai/core/go-log" coreio "forge.lthn.ai/core/go-io" "forge.lthn.ai/core/go-scm/manifest" "gopkg.in/yaml.v3" @@ -30,7 +30,7 @@ func DiscoverProviders(dir string) ([]DiscoveredProvider, error) { if os.IsNotExist(err) { return nil, nil // No providers directory — not an error. } - return nil, fmt.Errorf("marketplace.DiscoverProviders: %w", err) + return nil, coreerr.E("marketplace.DiscoverProviders", "read directory", err) } var providers []DiscoveredProvider @@ -93,12 +93,12 @@ func LoadProviderRegistry(path string) (*ProviderRegistryFile, error) { Providers: make(map[string]ProviderRegistryEntry), }, nil } - return nil, fmt.Errorf("marketplace.LoadProviderRegistry: %w", err) + return nil, coreerr.E("marketplace.LoadProviderRegistry", "read failed", err) } var reg ProviderRegistryFile if err := yaml.Unmarshal([]byte(raw), ®); err != nil { - return nil, fmt.Errorf("marketplace.LoadProviderRegistry: %w", err) + return nil, coreerr.E("marketplace.LoadProviderRegistry", "parse failed", err) } if reg.Providers == nil { @@ -111,12 +111,12 @@ func LoadProviderRegistry(path string) (*ProviderRegistryFile, error) { // SaveProviderRegistry writes the registry to the given path. func SaveProviderRegistry(path string, reg *ProviderRegistryFile) error { if err := coreio.Local.EnsureDir(filepath.Dir(path)); err != nil { - return fmt.Errorf("marketplace.SaveProviderRegistry: %w", err) + return coreerr.E("marketplace.SaveProviderRegistry", "ensure directory", err) } data, err := yaml.Marshal(reg) if err != nil { - return fmt.Errorf("marketplace.SaveProviderRegistry: %w", err) + return coreerr.E("marketplace.SaveProviderRegistry", "marshal failed", err) } return coreio.Local.Write(path, string(data)) diff --git a/marketplace/installer.go b/marketplace/installer.go index 7e1dcca..e581bba 100644 --- a/marketplace/installer.go +++ b/marketplace/installer.go @@ -4,12 +4,12 @@ import ( "context" "encoding/hex" "encoding/json" - "fmt" "os/exec" "path/filepath" "strings" "time" + coreerr "forge.lthn.ai/core/go-log" "forge.lthn.ai/core/go-io" "forge.lthn.ai/core/go-scm/manifest" "forge.lthn.ai/core/go-io/store" @@ -49,15 +49,15 @@ type InstalledModule struct { func (i *Installer) Install(ctx context.Context, mod Module) error { // Check if already installed if _, err := i.store.Get(storeGroup, mod.Code); err == nil { - return fmt.Errorf("marketplace: module %q already installed", mod.Code) + return coreerr.E("marketplace.Installer.Install", "module already installed: "+mod.Code, nil) } dest := filepath.Join(i.modulesDir, mod.Code) if err := i.medium.EnsureDir(i.modulesDir); err != nil { - return fmt.Errorf("marketplace: mkdir: %w", err) + return coreerr.E("marketplace.Installer.Install", "mkdir", err) } if err := gitClone(ctx, mod.Repo, dest); err != nil { - return fmt.Errorf("marketplace: clone %s: %w", mod.Repo, err) + return coreerr.E("marketplace.Installer.Install", "clone "+mod.Repo, err) } // On any error after clone, clean up the directory @@ -70,7 +70,7 @@ func (i *Installer) Install(ctx context.Context, mod Module) error { medium, err := io.NewSandboxed(dest) if err != nil { - return fmt.Errorf("marketplace: medium: %w", err) + return coreerr.E("marketplace.Installer.Install", "medium", err) } m, err := loadManifest(medium, mod.SignKey) @@ -92,11 +92,11 @@ func (i *Installer) Install(ctx context.Context, mod Module) error { data, err := json.Marshal(installed) if err != nil { - return fmt.Errorf("marketplace: marshal: %w", err) + return coreerr.E("marketplace.Installer.Install", "marshal", err) } if err := i.store.Set(storeGroup, mod.Code, string(data)); err != nil { - return fmt.Errorf("marketplace: store: %w", err) + return coreerr.E("marketplace.Installer.Install", "store", err) } cleanup = false @@ -106,7 +106,7 @@ func (i *Installer) Install(ctx context.Context, mod Module) error { // Remove uninstalls a module by deleting its files and store entry. func (i *Installer) Remove(code string) error { if _, err := i.store.Get(storeGroup, code); err != nil { - return fmt.Errorf("marketplace: module %q not installed", code) + return coreerr.E("marketplace.Installer.Remove", "module not installed: "+code, nil) } dest := filepath.Join(i.modulesDir, code) @@ -119,29 +119,29 @@ func (i *Installer) Remove(code string) error { func (i *Installer) Update(ctx context.Context, code string) error { raw, err := i.store.Get(storeGroup, code) if err != nil { - return fmt.Errorf("marketplace: module %q not installed", code) + return coreerr.E("marketplace.Installer.Update", "module not installed: "+code, nil) } var installed InstalledModule if err := json.Unmarshal([]byte(raw), &installed); err != nil { - return fmt.Errorf("marketplace: unmarshal: %w", err) + return coreerr.E("marketplace.Installer.Update", "unmarshal", err) } dest := filepath.Join(i.modulesDir, code) cmd := exec.CommandContext(ctx, "git", "-C", dest, "pull", "--ff-only") if output, err := cmd.CombinedOutput(); err != nil { - return fmt.Errorf("marketplace: pull: %s: %w", strings.TrimSpace(string(output)), err) + return coreerr.E("marketplace.Installer.Update", "pull: "+strings.TrimSpace(string(output)), err) } // Reload and re-verify manifest with the same key used at install time medium, mErr := io.NewSandboxed(dest) if mErr != nil { - return fmt.Errorf("marketplace: medium: %w", mErr) + return coreerr.E("marketplace.Installer.Update", "medium", mErr) } m, mErr := loadManifest(medium, installed.SignKey) if mErr != nil { - return fmt.Errorf("marketplace: reload manifest: %w", mErr) + return coreerr.E("marketplace.Installer.Update", "reload manifest", mErr) } // Update stored metadata @@ -151,7 +151,7 @@ func (i *Installer) Update(ctx context.Context, code string) error { data, err := json.Marshal(installed) if err != nil { - return fmt.Errorf("marketplace: marshal: %w", err) + return coreerr.E("marketplace.Installer.Update", "marshal", err) } return i.store.Set(storeGroup, code, string(data)) @@ -161,7 +161,7 @@ func (i *Installer) Update(ctx context.Context, code string) error { func (i *Installer) Installed() ([]InstalledModule, error) { all, err := i.store.GetAll(storeGroup) if err != nil { - return nil, fmt.Errorf("marketplace: list: %w", err) + return nil, coreerr.E("marketplace.Installer.Installed", "list", err) } var modules []InstalledModule @@ -180,7 +180,7 @@ func loadManifest(medium io.Medium, signKey string) (*manifest.Manifest, error) if signKey != "" { pubBytes, err := hex.DecodeString(signKey) if err != nil { - return nil, fmt.Errorf("marketplace: decode sign key: %w", err) + return nil, coreerr.E("marketplace.loadManifest", "decode sign key", err) } return manifest.LoadVerified(medium, ".", pubBytes) } @@ -191,7 +191,7 @@ func loadManifest(medium io.Medium, signKey string) (*manifest.Manifest, error) func gitClone(ctx context.Context, repo, dest string) error { cmd := exec.CommandContext(ctx, "git", "clone", "--depth=1", repo, dest) if output, err := cmd.CombinedOutput(); err != nil { - return fmt.Errorf("%s: %w", strings.TrimSpace(string(output)), err) + return coreerr.E("marketplace.gitClone", strings.TrimSpace(string(output)), err) } return nil } diff --git a/marketplace/marketplace.go b/marketplace/marketplace.go index 52b4a8f..4cb455c 100644 --- a/marketplace/marketplace.go +++ b/marketplace/marketplace.go @@ -2,8 +2,9 @@ package marketplace import ( "encoding/json" - "fmt" "strings" + + coreerr "forge.lthn.ai/core/go-log" ) // Module is a marketplace entry pointing to a module's Git repo. @@ -26,7 +27,7 @@ type Index struct { func ParseIndex(data []byte) (*Index, error) { var idx Index if err := json.Unmarshal(data, &idx); err != nil { - return nil, fmt.Errorf("marketplace.ParseIndex: %w", err) + return nil, coreerr.E("marketplace.ParseIndex", "unmarshal failed", err) } return &idx, nil } diff --git a/plugin/installer.go b/plugin/installer.go index 8a595bb..4171977 100644 --- a/plugin/installer.go +++ b/plugin/installer.go @@ -159,7 +159,7 @@ func (i *Installer) cloneRepo(ctx context.Context, org, repo, version, dest stri cmd := exec.CommandContext(ctx, "gh", args...) if output, err := cmd.CombinedOutput(); err != nil { - return fmt.Errorf("%w: %s", err, strings.TrimSpace(string(output))) + return coreerr.E("plugin.Installer.cloneRepo", strings.TrimSpace(string(output)), err) } return nil diff --git a/repos/gitstate.go b/repos/gitstate.go index 1f21c71..d08aba7 100644 --- a/repos/gitstate.go +++ b/repos/gitstate.go @@ -1,10 +1,10 @@ package repos import ( - "fmt" "path/filepath" "time" + coreerr "forge.lthn.ai/core/go-log" "forge.lthn.ai/core/go-io" "gopkg.in/yaml.v3" ) @@ -44,12 +44,12 @@ func LoadGitState(m io.Medium, root string) (*GitState, error) { content, err := m.Read(path) if err != nil { - return nil, fmt.Errorf("failed to read git state: %w", err) + return nil, coreerr.E("repos.LoadGitState", "failed to read git state", err) } var gs GitState if err := yaml.Unmarshal([]byte(content), &gs); err != nil { - return nil, fmt.Errorf("failed to parse git state: %w", err) + return nil, coreerr.E("repos.LoadGitState", "failed to parse git state", err) } if gs.Repos == nil { @@ -66,17 +66,17 @@ func LoadGitState(m io.Medium, root string) (*GitState, error) { func SaveGitState(m io.Medium, root string, gs *GitState) error { coreDir := filepath.Join(root, ".core") if err := m.EnsureDir(coreDir); err != nil { - return fmt.Errorf("failed to create .core directory: %w", err) + return coreerr.E("repos.SaveGitState", "failed to create .core directory", err) } data, err := yaml.Marshal(gs) if err != nil { - return fmt.Errorf("failed to marshal git state: %w", err) + return coreerr.E("repos.SaveGitState", "failed to marshal git state", err) } path := filepath.Join(coreDir, "git.yaml") if err := m.Write(path, string(data)); err != nil { - return fmt.Errorf("failed to write git state: %w", err) + return coreerr.E("repos.SaveGitState", "failed to write git state", err) } return nil diff --git a/repos/kbconfig.go b/repos/kbconfig.go index 26c1cba..49b393b 100644 --- a/repos/kbconfig.go +++ b/repos/kbconfig.go @@ -4,6 +4,7 @@ import ( "fmt" "path/filepath" + coreerr "forge.lthn.ai/core/go-log" "forge.lthn.ai/core/go-io" "gopkg.in/yaml.v3" ) @@ -74,12 +75,12 @@ func LoadKBConfig(m io.Medium, root string) (*KBConfig, error) { content, err := m.Read(path) if err != nil { - return nil, fmt.Errorf("failed to read kb config: %w", err) + return nil, coreerr.E("repos.LoadKBConfig", "failed to read kb config", err) } kb := DefaultKBConfig() if err := yaml.Unmarshal([]byte(content), kb); err != nil { - return nil, fmt.Errorf("failed to parse kb config: %w", err) + return nil, coreerr.E("repos.LoadKBConfig", "failed to parse kb config", err) } return kb, nil @@ -89,17 +90,17 @@ func LoadKBConfig(m io.Medium, root string) (*KBConfig, error) { func SaveKBConfig(m io.Medium, root string, kb *KBConfig) error { coreDir := filepath.Join(root, ".core") if err := m.EnsureDir(coreDir); err != nil { - return fmt.Errorf("failed to create .core directory: %w", err) + return coreerr.E("repos.SaveKBConfig", "failed to create .core directory", err) } data, err := yaml.Marshal(kb) if err != nil { - return fmt.Errorf("failed to marshal kb config: %w", err) + return coreerr.E("repos.SaveKBConfig", "failed to marshal kb config", err) } path := filepath.Join(coreDir, "kb.yaml") if err := m.Write(path, string(data)); err != nil { - return fmt.Errorf("failed to write kb config: %w", err) + return coreerr.E("repos.SaveKBConfig", "failed to write kb config", err) } return nil diff --git a/repos/registry.go b/repos/registry.go index e10e2ba..88f5808 100644 --- a/repos/registry.go +++ b/repos/registry.go @@ -4,12 +4,11 @@ package repos import ( - "errors" - "fmt" "os" "path/filepath" "strings" + coreerr "forge.lthn.ai/core/go-log" "forge.lthn.ai/core/go-io" "gopkg.in/yaml.v3" ) @@ -67,13 +66,13 @@ type Repo struct { func LoadRegistry(m io.Medium, path string) (*Registry, error) { content, err := m.Read(path) if err != nil { - return nil, fmt.Errorf("failed to read registry file: %w", err) + return nil, coreerr.E("repos.LoadRegistry", "failed to read registry file", err) } data := []byte(content) var reg Registry if err := yaml.Unmarshal(data, ®); err != nil { - return nil, fmt.Errorf("failed to parse registry file: %w", err) + return nil, coreerr.E("repos.LoadRegistry", "failed to parse registry file", err) } reg.medium = m @@ -147,7 +146,7 @@ func FindRegistry(m io.Medium) (string, error) { } } - return "", errors.New("repos.yaml not found") + return "", coreerr.E("repos.FindRegistry", "repos.yaml not found", nil) } // ScanDirectory creates a Registry by scanning a directory for git repos. @@ -156,7 +155,7 @@ func FindRegistry(m io.Medium) (string, error) { func ScanDirectory(m io.Medium, dir string) (*Registry, error) { entries, err := m.List(dir) if err != nil { - return nil, fmt.Errorf("failed to read directory: %w", err) + return nil, coreerr.E("repos.ScanDirectory", "failed to read directory", err) } reg := &Registry{ @@ -282,12 +281,12 @@ func (r *Registry) TopologicalOrder() ([]*Repo, error) { return nil } if visiting[name] { - return fmt.Errorf("circular dependency detected: %s", name) + return coreerr.E("repos.Registry.TopologicalOrder", "circular dependency detected: "+name, nil) } repo, ok := r.Repos[name] if !ok { - return fmt.Errorf("unknown repo: %s", name) + return coreerr.E("repos.Registry.TopologicalOrder", "unknown repo: "+name, nil) } visiting[name] = true diff --git a/repos/workconfig.go b/repos/workconfig.go index dfe1470..a7a1b58 100644 --- a/repos/workconfig.go +++ b/repos/workconfig.go @@ -1,10 +1,10 @@ package repos import ( - "fmt" "path/filepath" "time" + coreerr "forge.lthn.ai/core/go-log" "forge.lthn.ai/core/go-io" "gopkg.in/yaml.v3" ) @@ -63,12 +63,12 @@ func LoadWorkConfig(m io.Medium, root string) (*WorkConfig, error) { content, err := m.Read(path) if err != nil { - return nil, fmt.Errorf("failed to read work config: %w", err) + return nil, coreerr.E("repos.LoadWorkConfig", "failed to read work config", err) } wc := DefaultWorkConfig() if err := yaml.Unmarshal([]byte(content), wc); err != nil { - return nil, fmt.Errorf("failed to parse work config: %w", err) + return nil, coreerr.E("repos.LoadWorkConfig", "failed to parse work config", err) } return wc, nil @@ -78,17 +78,17 @@ func LoadWorkConfig(m io.Medium, root string) (*WorkConfig, error) { func SaveWorkConfig(m io.Medium, root string, wc *WorkConfig) error { coreDir := filepath.Join(root, ".core") if err := m.EnsureDir(coreDir); err != nil { - return fmt.Errorf("failed to create .core directory: %w", err) + return coreerr.E("repos.SaveWorkConfig", "failed to create .core directory", err) } data, err := yaml.Marshal(wc) if err != nil { - return fmt.Errorf("failed to marshal work config: %w", err) + return coreerr.E("repos.SaveWorkConfig", "failed to marshal work config", err) } path := filepath.Join(coreDir, "work.yaml") if err := m.Write(path, string(data)); err != nil { - return fmt.Errorf("failed to write work config: %w", err) + return coreerr.E("repos.SaveWorkConfig", "failed to write work config", err) } return nil